Projects

A project is to a campaign what a Git repository is to a source tree: a directory marked at its root by a control directory that commands discover by walking upward from wherever they are run. In httk that directory is the visible, deliberately non-hidden httk_project/; its versioned manifest is httk_project/project.json. The Git analogy describes discovery, not naming: the anchor is visible so it is easy to inspect and manage.

The anchor lives in httk.core.project, so an httk-core installation has a working project on its own. Capability modules layer their own project data on top of the anchor without making the anchor depend on them.

The command line

The core-owned httk project command has five subcommands: init, show, import-v1, seal, and verify-seal.

httk project init
httk project init PATH --name NAME --description TEXT
httk project show
httk project show PATH --json
httk project import-v1 PATH
httk project import-v1 PATH --source DIR --name NAME
httk project seal OUT.ZIP
httk project verify-seal ZIP [--expect-key FINGERPRINT] [--trusted-key FINGERPRINT ...]

init makes PATH a project, or uses the current directory when PATH is omitted. --name defaults to the directory name and --description defaults to an empty string. It refuses an existing project and creates httk_project/project.json, the project’s Ed25519 key under httk_project/keys/, and httk_project/remotes/. It creates no workflow workspace. To populate a new project from a plugin or an explicit template, use httk project init --template; see the Project templates section below.

show describes the nearest project, or the project named by PATH. --json emits one machine-readable document. import-v1 imports the legacy v1 project in PATH/ht.project by default; --source DIR selects another v1 directory.

seal packages the nearest project tree into a signed redistribution ZIP, excluding the private key, and signs it with the project’s Ed25519 key. verify-seal checks such a ZIP and prints the signer’s public key and fingerprint; --expect-key requires a specific signer fingerprint and --trusted-key (repeatable) supplies fingerprints to trust. The programmatic equivalents are seal_project and verify_seal in httk.core.project.

Root options are processed before command dispatch. -C DIR changes directory first, so any httk command can target a project from elsewhere:

httk -C ~/proj project show
httk -C ~/proj project show --json

See The extensible command line for the root command-line rules. The httk project namespace belongs to httk-core; modules provide their own namespaces, for example httk workflow project .... There is no project-subcommand extension mechanism.

Project templates

An httk₂ project template supplies files and, optionally, a hook that generates more project content. Templates can be bundled by plugins or used directly from a directory containing httk_project_template.toml.

Install a plugin, then initialize a project from one of its templates:

httk plugin install ./my-plugin
httk project init my-project --template my-plugin:starter --parameter n=3

List installed templates and their plugin-qualified selectors with:

httk project init --list-templates

An explicit template directory works without a plugin:

httk project init my-project --template ./templates/starter

Templates can also be selected by a bare template ID when exactly one installed template has that ID; a bare ID shared by multiple plugins is ambiguous and must be qualified.

The httk_project_template.toml manifest

The manifest is strict: its only top-level table is [template], and unknown keys are errors.

[template]
id = "starter"
description = "A small starter project"
files = ["README.md", "src"]

[template.instantiate]
file = "instantiate.py"

[template.parameters.name]
type = "string"
description = "Project display name"

[template.parameters.count]
type = "integer"
default = 1

[template]

id is required and must match [a-z0-9._-]+; it must not be . or .., and must not start with -. description is an optional string.

files is an optional array of relative POSIX members. Each member may be a regular file or a directory. Members must remain below the template root, must not be absolute, contain empty components, ., or .., or traverse a symlink. They must be unique and may not overlap by containment. The manifest itself, httk_project_template.toml, and the instantiate hook may not be listed. Directory members are copied recursively; symlinks and special files inside them are rejected during instantiation.

[template.instantiate]

The table is optional and accepts the required file key. A .py file is run with the current Python interpreter. A non-Python file must be an executable regular member. The hook is required when the template declares any parameters.

[template.parameters.<name>]

Parameter names must match [A-Za-z_][A-Za-z0-9_]*. Each parameter table accepts:

  • type, required and one of string, number, integer, boolean, array, or object;

  • description, an optional string;

  • default, an optional JSON-compatible value matching type.

A parameter with no default is mandatory. A parameter with a default is optional and receives that default when it is not supplied. number accepts integers and floating-point numbers but not booleans; integer also excludes booleans. array and object mean JSON arrays and objects.

Parameters are supplied by repeating --parameter NAME=VALUE. VALUE is parsed as JSON when possible and is otherwise treated as a literal string. Quote a literal string as JSON, for example:

httk project init demo --template starter \
  --parameter name='"Ada"' --parameter count=3 --parameter enabled=true

The quotes around "Ada" are needed because an unquoted Ada is the JSON fallback string, while a value such as 3 is parsed as an integer. Unknown parameters, missing mandatory parameters, and values of the wrong type fail before the project directory is created.

The instantiate hook

The hook receives one JSON request on standard input. It runs with the project directory as its current working directory (cwd) and with the selected template files already copied. The request envelope is:

{
  "format": "httk-project-template-instantiate",
  "format_version": 1,
  "template": "starter",
  "parameters": {"name": "Ada", "count": 1},
  "project": {
    "name": "demo",
    "description": "",
    "project_id": "...",
    "root": "/absolute/path/to/demo"
  }
}

The project object contains the project information supplied by the caller, plus root, which is the absolute project root. The httk project init command supplies name, description, and project_id.

The hook inherits the environment except that variables beginning with HTTK_ are removed. HTTK_CONFIG_HOME and HTTK_DATA_HOME are retained. The default hook timeout is 3600 seconds. A Python hook is invoked as sys.executable HOOK.py; another hook is executed directly.

Write one JSON object to standard output. {} is the empty response. The optional notes member must be an array of strings; those notes are printed after initialization. Other response members are accepted but ignored by the core. Nonzero exit status, invalid JSON, invalid notes, or a timeout fails instantiation.

The template_instantiate_main helper validates the request envelope and writes the response for a Python hook. A minimal hook is:

from httk.core.project.templates import TemplateInstantiateRequest, template_instantiate_main


def handle(request: TemplateInstantiateRequest) -> dict[str, object]:
    with open("created.txt", "w", encoding="utf-8") as output:
        output.write(str(request.parameters["name"]))
    return {"notes": ["created project file"]}


template_instantiate_main(handle)

Returning None writes {}. Exceptions are reported as hook failure.

Instantiation and rollback

httk project init --template performs the following sequence:

  1. Resolve and validate the template, then validate parameters and apply defaults.

  2. Create the project anchor.

  3. Preflight destination collisions and copy the declared files and directories.

  4. Run the optional instantiate hook.

Copying preserves file modes. Existing project members cause a collision error, and the copy preflight prevents a partial copy from that error. Hook changes are not transactional. If the target directory was fresh, any failure removes it. If the target directory already contained entries, the anchor and any copied or hook-created files remain and the command reports that partial state.

Discovering and reading a project

from httk.core.project import discover_project, read_project, require_project

root = discover_project()       # nearest project, or None
root = require_project()        # nearest project, or an error
metadata = read_project(root)   # validated httk_project/project.json

discover_project(start) resolves start and checks it and each parent for httk_project/project.json. It returns the project root as a Path, or None; require_project(start) has the same behavior but raises when no project exists. Both refuse legacy project directories; see the Legacy project directories section below.

Initializing and importing

The API equivalents of the two creating commands are initialize_project and import_v1_project:

from httk.core.project import import_v1_project, initialize_project

metadata = initialize_project("campaign", name="My campaign")
metadata = import_v1_project("campaign", name="My campaign")

initialize_project creates only the anchor, its project.json, its Ed25519 key, and its remotes/ directory. import_v1_project reads the v1 ht.project/config, copies its public-key files into httk_project/keys/legacy-public/, records the source, and makes readable legacy keys trusted. It also creates the new project’s own key and pins it in httk_project/project.json; queue data is not imported by this function.

Legacy project directories

Discovery refuses a legacy directory by raising httk.core.project.LegacyProjectError. The refusal includes the remedy.

A v1 project containing ht.project produces:

$ httk project show
httk project: found an httk v1 project ('ht.project') at /path/to/project; create the httk v2 anchor with: httk project import-v1 /path/to/project

Run the shown httk project import-v1 /path/to/project. It reads /path/to/project/ht.project/config, copies the public keys, pins the readable legacy keys as trusted, and creates /path/to/project/httk_project/.

A pre-release v2 project containing .httk-project/project.json produces:

$ httk project show
httk project: found a project anchor from a pre-release httk v2 ('.httk-project') at /path/to/project; rename it: mv /path/to/project/.httk-project /path/to/project/httk_project

Apply the shown remedy from the project root:

mv .httk-project httk_project

Identity keys, pinning, and trust

A project owns one Ed25519 signing key. Its public half is recorded in httk_project/project.json as public_key and is the trust anchor against which a signed manifest is checked; verification does not trust a key merely because a manifest carries it in its own header.

from httk.core.project import (
    pin_project_key,        # pin httk_project/keys/project.pub
    trust_project_key,      # adopt another public key
    pinned_project_key,     # the project's pinned key, or None
    trusted_project_keys,   # the pinned key and adopted keys
    key_fingerprint,        # stable sha256: display fingerprint
)

pin_project_key(root) explicitly adopts the current httk_project/keys/project.pub as the project’s pinned trust anchor. trust_project_key(root, key) adds another public key to trusted_keys. pinned_project_key(metadata) reads the project’s own pin, while trusted_project_keys(metadata) returns it together with adopted keys.

Public keys are recorded as ed25519:BASE64. format_public_key, parse_public_key, canonical_public_key, and read_public_key_file convert between that spelling, raw 32-byte keys, and *.pub files. project_public_key_path(root) returns the path to the project’s public key.