# 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`. ```console 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: ```console httk -C ~/proj project show httk -C ~/proj project show --json ``` See {doc}`cli` 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: ```console 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: ```console httk project init --list-templates ``` An explicit template directory works without a plugin: ```console 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. ```toml [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.]` 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: ```console 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: ```json { "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: ```python 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 ```python 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`: ```python 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 {class}`httk.core.project.LegacyProjectError`. The refusal includes the remedy. A v1 project containing `ht.project` produces: ```console $ 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: ```console $ 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: ```console 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. ```python 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.