Flow Kit Repository Contract#
A Flow Kit repository is a local folder with a root kit.json. Flow Agents validates the repository shape, declared paths, declared assets, fixture behavior, and diagnostics. Flow validates Flow Definition semantics.
Use this command before adding a kit to a catalog or sharing it with another maintainer:
npm run validate:source -- --kit path/to/local-kit
The full source-tree command validates every kit listed in kits/catalog.json through the same repository contract. An unreadable or empty catalog, or an entry without a path, fails validation:
npm run validate:source --
Install From A Local Path Or Git#
Flow Agents accepts either a local kit repository path or a Git URL:
npm run kit -- install path/to/local-kit --dest /path/to/installed-flow-agents
npm run kit -- install https://github.com/example/example-kit.git#v1.0.0 --dest /path/to/installed-flow-agents
npm run kit -- list --dest /path/to/installed-flow-agents
npm run kit -- status --dest /path/to/installed-flow-agents
npm run kit -- status example-kit --dest /path/to/installed-flow-agents
npm run kit -- activate --dest /path/to/installed-flow-agents --format json
--dest is the installed bundle, workspace root, isolated Codex home, or test fixture destination. When omitted, Codex-oriented kit commands install into the normal Codex home: CODEX_HOME when it is set, otherwise ~/.codex. Pass --dest to override that destination for workspace installs, isolated homes, or tests.
Install always runs the same repository validation used by npm run validate:source -- --kit before it creates or updates local install state. A validation failure exits nonzero and leaves the destination registry and copied kits unchanged.
Local installs are runtime overlay state. The command writes registry metadata to:
<dest>/kits/local/installed-kits.json
and copies validated repositories under:
<dest>/kits/local/repositories/<kit-id>/
It does not edit the source Kit Catalog at kits/catalog.json.
Each registry entry records:
id: kit id fromkit.json.source: local source path used for the install, or caller-declared logical provenance supplied with local-only--record-source <locator>.--record-sourceis metadata and is not independently acquired or verified.hash:sha256:content hash of the installed repository bytes; this is the verified byte identity, notsourcemetadata.version: optional manifest version when a future contract permits one.installed_at: UTC ISO timestamp.installed_path: canonical POSIX repository-relative path, alwayskits/local/repositories/<kit-id>.state: install state recorded at install time. Read-only status observation reports the current state separately and never rewrites this registry field.
Reinstalling the same kit id from the same source metadata with the same content is idempotent and leaves the registry unchanged. An explicit --update bypasses that idempotency decision and runs the full copied-tree and registry transaction even when the source, hash, and target are unchanged; it replaces the entry and records a new installed_at timestamp. This is the supported migration for an invalid historical absolute installed_path: rerun flow-agents kit install <source> --dest <dest> --update. --record-source <locator> is available only for local paths, so callers can preserve logical provenance across source-directory moves; it must be trimmed and non-blank, at most 1024 characters, and contain no Unicode control (Cc), format (Cf), line-separator (Zl), or paragraph-separator (Zp) characters. This includes C0/C1 controls (U+0000–U+001F, U+007F–U+009F), U+2028, U+2029, and bidirectional format controls such as U+202E. Git installs reject --record-source and retain their normalized URL/ref provenance. Installing a different source with an existing kit id fails with a conflict unless --update is passed. --force re-copies an existing same-source install after validation.
Git sources are shallow-cloned into a temporary directory and validated at the clone root. A
repository installed from Git must therefore place kit.json at its root; subdirectory selection
is not supported. Use a URL #ref fragment or --ref <branch|tag|sha> to pin the source.
Full 40-character SHA-1 or 64-character SHA-256 commit references are fetched
directly and checked out detached; the fetched commit identity must match exactly.
An unavailable commit fails without falling back to a branch or changing the
installed copy. Branches and tags retain shallow-clone behavior. HTTP credentials
belong in Git credential helpers rather than URL userinfo, which is rejected
before any fetch or provenance write. Install records the normalized URL/ref and
content hash, and never executes scripts from the cloned
repository.
list and status are read-only. list prints one summary line per installed kit. status prints JSON provenance plus recorded_hash and observed_hash, and reports copied-kit state as one of:
installed: the safely observed tree exactly matches the recorded hash.drifted: the safely observed tree is present but its canonical content hash differs from the recorded hash.missing: the registered copied tree is absent.invalid: the registry entry or copied tree cannot be safely observed (including symlinks, special files, unreadable paths, and paths replaced during observation).
The canonical tree hash sorts relative paths in locale-independent code-unit order and hashes each regular file's path and bytes, while excluding the VCS/cache paths excluded by installation (.git, __pycache__, and .pytest_cache). Observation derives the copied Kit target only from the current trusted destination plus kit id; it never follows a registry-supplied target. It accepts only the canonical relative installed_path; every absolute or alternate value, including an exact-current legacy absolute value, is invalid. The invalid diagnostic is a migration instruction: reinstall with flow-agents kit install <source> --dest <dest> --update to replace the registry entry. Observation never follows symlinks: for an installed Kit it checks every component from the requested destination root through the copied Kit root, so intermediate and dangling links are invalid too. Status and activation do not repair or rewrite registry metadata; reinstall (or --force for the same source) is the explicit refresh path.
Runtime Activation#
activate reads the built-in Kit Catalog and local install overlay, selects a runtime adapter, and writes generated non-durable projection files into the destination workspace. When --adapter is omitted, Flow Agents selects the only implemented adapter:
codex-local
Unknown adapter ids fail with JSON diagnostics that include the available adapters. Git fetching belongs to install, not activation; activation still does not perform npm module extraction or execute kit setup code.
The codex-local adapter supports assets declared in flows, skills, and docs. It activates the built-in Builder Kit Flow Definitions, including builder.shape and builder.build, plus supported assets from locally installed kit copies under:
<dest>/kits/local/repositories/<kit-id>/
Activation reuses the installed local kit registry at <dest>/kits/local/installed-kits.json; it does not duplicate installed kit state and does not edit kits/catalog.json. It performs the same read-only integrity observation as kit status: missing and invalid local copies are skipped with a warning, and a drifted but safely readable copy emits a warning with both hashes and remains activatable for this warning-only rollout. Before generating runtime files, activation copies a safely observed local Kit to a private staging snapshot and generates only from that snapshot, so a later replacement cannot change the activated bytes. Malformed registry entries, including missing or non-string ids, are warnings and are skipped. Drift is therefore never represented as registry-matched, but it is not yet an activation blocker.
Generated adapter projections are written under:
<dest>/.kontourai/flow-agents/projections/codex/
Flow Definition copies are placed under flows/<kit-id>/<flow-id>.flow.json, and activation writes an activation.json manifest in the same projection area. These files are regenerable from the Kit Catalog plus kits/local; they are not the durable run state for a workflow.
The stable activation diagnostics include:
selected_adapter: selected adapter id, currentlycodex-local.supported_asset_classes: asset classes the selected adapter activates, currently["flows", "skills", "docs"].generated_runtime_files: generated runtime-local files with asset class, path, kit id, asset id, and source path.skipped_assets: unsupported declared assets with asset class, path, kit id, asset id when present, and reason.warnings: recoverable catalog, registry, or asset discovery problems.errors: blocking discovery or activation problems.
Declared skills and docs are copied into the runtime projection alongside flows. Declared adapters, evals, generic assets, and provisions are diagnostic-only for this adapter. They are skipped with explicit skipped_assets entries; they are not copied, installed, invoked, or treated as active runtime behavior.
Root Manifest#
kit.json must be valid JSON at the repository root.
{
"schema_version": "1.0",
"id": "example-kit",
"name": "Example Kit",
"product_name": "Example Kit",
"description": "A local kit used to validate the repository contract.",
"flows": [
{
"id": "example.review",
"path": "flows/review.flow.json",
"description": "Review a small change."
}
],
"docs": [
{
"id": "example.readme",
"path": "docs/README.md"
}
],
"provisions": [
{
"id": "example-kit.editor-policy",
"path": "provisions/editor-policy.json",
"target": ".editor/policy.json",
"description": "Initial repository policy."
}
]
}
Required fields:
schema_version: must be"1.0".id: stable kebab-case kit id, such asbuilderorexample-kit.name: non-empty display name.flows: non-empty list of Flow Definition entries. Each entry must be an object withidandpath.
Optional fields:
product_name: non-empty display name when the product name differs fromname.description: non-empty summary.skills,docs,adapters,evals,assets: lists of relative asset paths or objects withid,path, and optionaldescription.provisions: a list of objects with kit-id-prefixedid, sourcepath, consumer-repository-relativetarget, and optionaldescription. A host asset also declareshost(codex,claude-code,opencode,kiro, orpi) and Conduitkind(skill,agent,hook,prompt,command, orcontext) together. Neither field is inferred from a target path. A host hook may opt intomerge: "hooks-json"for an idempotent merge into a hosthooksobject, and may then list earlier commands insupersedes.
Repository Provisioning#
Provision entries declare inert files; kits do not execute installer code. Source paths follow the other extension-asset rules: they must be relative, stay inside the kit directory, and name an existing regular file. Targets must be non-empty relative paths, must not contain traversal segments or resolve outside the consumer repository, must not be inside .git, and must be unique after normalization.
For entries with explicit host and kind, Flow Agents passes the verified
UTF-8 asset (at most 1,000,000 bytes, including any merged result) and
caller-supplied target mapping through that host's Conduit
adapter. A host profile that reports the kind unavailable refuses the entire
provision before writing any file. The provision manifest includes Conduit's
installation receipt (asset id, kind, digest, skipped status) without asset
contents or resolved filesystem paths. The receipt proves projection and copy,
not that a host has trusted or executed a hook; use host-bound conformance for
that claim. Ordinary entries without host and kind retain the existing
byte-for-byte inert copy path.
hooks-json merges by event and command handler: it preserves other handlers,
adds each declared group once, and refuses malformed JSON or the same command
with different settings before any provision writes. Conduit receives the
resulting bytes, so its receipt digest names what was actually installed.
Re-running the provision leaves the handler set unchanged; a surrounding host
installer may reorder unrelated groups, in which case the next Conduit receipt
names the newly installed bytes.
The provision manifest records each handler the kit added as hook_handlers:
the provision id, target, event, command, the group's own fields (such as
matcher), and the exact handler object. When the kit is provisioned again,
Flow Agents first removes the handlers that match those records, then merges
the kit's current declaration. A kit that changes or drops a hook command
therefore replaces its earlier handler instead of leaving both to run. A group
that is already identical to the current declaration stays in place.
- Replacement by provenance never removes user handlers. That includes a user handler in the kit's group, and a user handler that reuses the kit's command under another matcher or with other settings. An identical group that existed before the kit was first provisioned is not recorded as the kit's.
- A command that another kit's provision manifest records for the same target and event is never removed. When two kits declare an identical group, both record it, and it stays until neither declares it.
- Before writing any host file, Flow Agents records the previous and the new
handlers together, and it narrows the record once every write succeeds. If a
later write fails or the process stops, the next provisioning still recognises
whichever handlers reached the host file as the kit's. Legacy handlers adopted
from the existing file, including commands selected by
supersedes, are also recorded before writing their replacements. Thesuperseded_handlersaudit trail is updated only after provisioning completes. - A manifest written before
hook_handlersexisted proves only that the kit provisioned the target. Re-provisioning adopts the handlers that sit in a group with the same fields as one the kit declares now and carry a command the kit declares now, and records them. It cannot recognise a handler whose command has since changed; usesupersedesfor that. - If the kit stops declaring a hooks-json target, its records for that target are kept, because provisioned content is never removed and a later provisioning of that target must still replace them.
A hooks-json provision may also declare supersedes: 1 to 32 unique, non-blank
command strings of at most 4,096 characters each. Re-provisioning removes
handlers whose command exactly equals one of them from the provision's target,
only in the events the provision declares, and never a command another kit
records there. Each removal is added to the manifest's superseded_handlers.
List only commands this kit shipped in earlier versions: supersedes removes a
matching handler whoever wrote it. A command the provision still declares is
refused.
{
"id": "example.governance-hook",
"path": "hooks/codex.json",
"target": ".codex/hooks.json",
"host": "codex",
"kind": "hook",
"merge": "hooks-json",
"supersedes": ["npm exec -- example hooks codex pre-tool-use"]
}
For a kit with a hooks-json provision, an unreadable provision manifest (its own
or another kit's), malformed hook_handlers, or a manifest naming another kit
fails provisioning before any write. This includes --dry-run, and an
init --activate-kit rerun. Repair or remove the named manifest to continue.
A kit that merges no hooks replaces its own unreadable manifest, as before. A
host settings file with no hooks key yet, such as a Claude settings.json
holding only permissions, is merged into as empty.
Provision a catalog kit, an installed-registry kit, or a kit at a direct local path with:
flow-agents kit provision <kit-id-or-path> [--target <consumer-repo>] [--force] [--dry-run]
--target defaults to the current working directory and must already be a directory. The engine resolves the real path of the deepest existing destination ancestor before writing, so a symlink cannot redirect a provision outside the target repository. It preflights the whole declaration: an existing destination for an ordinary copy reports a conflict and writes nothing; a declared hooks-json merge accepts an existing hook config after validating it. --force replaces declared ordinary-copy destinations. --dry-run prints every source-to-target mapping and writes nothing.
After a successful non-dry-run copy, Flow Agents writes or replaces this bookkeeping manifest:
<target>/.kontourai/flow-agents/provisions/<kit-id>.json
The schema is { schema_version: "1.0", kit_id, kit_hash, provisioned_at, files: [{ id, target }], hook_handlers: [{ id, target, event, command, group, handler }], superseded_handlers: [{ id, target, event, command }], conduit_receipts }. Provisioned files become consumer-repository content; uninstalling or deactivating the kit does not remove them.
When flow-agents init --activate-kit <id> successfully activates a selected kit, init invokes the same provisioning engine against its destination with create-only semantics. Existing destinations are reported as skipped warnings and do not fail an init rerun. Provisions never enter the runtime projection directory.
Path Rules#
All declared paths are local to the kit directory. A path must be relative, must not contain .., and must point at an existing file or folder. Absolute paths are rejected because a kit must be portable between worktrees and machines.
Valid:
{ "id": "example.review", "path": "flows/review.flow.json" }
Invalid:
{ "id": "example.review", "path": "../flow/review.flow.json" }
Diagnostic:
kit.json: flows[0].path must stay inside the kit directory; '..' path traversal is not allowed
Flow Boundary#
Flow Agents checks that a declared Flow Definition path is present and delegates Flow Definition validation to Flow with flow validate-definition <path> --json or the local Flow CLI fallback when available. If the Flow validation surface is unavailable, the source validator keeps only the existing minimal shape fallback and verification must record that gap as NOT_VERIFIED.
Flow Agents must not duplicate Flow gate semantics, trusted-producer rules, route-back behavior, or claim validation. Those belong to Flow.
Scope#
This contract covers local and Git-backed repository installation, local runtime bookkeeping, and Codex-local Flow Definition activation. Git sources are shallow-cloned only during an explicit kit install; activation never fetches repositories or executes setup code from the kit. Flow Agents does not split npm packages, select adapter packages, invoke the full Builder Kit provider graph, or implement Claude, Kiro, framework, API, or provider adapters.
Common Failures#
kit.json: .schema_version must be "1.0": update the manifest schema version.kit.json: .id must be a stable kebab-case string: use a lowercase id such asreview-kit.kit.json: .flows must be a non-empty list: declare at least one Flow Definition.kit.json: flows[0].path points at missing Flow Definition: add the file or fix the relative path.kit.json: docs[0].path points at missing asset: add the asset or remove the entry.
Native pending-action review#
A kit can declare optional review metadata alongside (or without) execution:
{"review":{"contract":"kontour.kit.review_request@1.0","module":"scripts/review.mjs","export":"prepareKitReview"}}
The declared async producer accepts {input, host, services} and returns
{sessionPath, binding, apply, presentation?}. sessionPath is absolute; binding is Survey's
producer-created ReviewQueueBinding; apply() returns a durable {reference, summary?} receipt. The host supplies frozen services.survey.bindReviewQueue and
services.survey.deriveReviewSessionApplyResultForSnapshot from its own Survey
dependency. Installed producers should use these instead of importing Survey
from the copied kit tree: a pnpm or global installation need not expose the SDK's
transitive dependency at the bootstrap root. Services cannot be supplied by JSON.
The producer owns current subject/basis validation, deadlines,
identity resolution and idempotent domain application under its own lock. A local
actor string is not authenticated human approval.
The optional presentation uses Survey's exported ReviewQueuePresentation
(@kontourai/survey/review-workbench). It provides a queue title and context,
and item hints keyed by the original item name: title, kind
(acknowledgment or choice) and candidateLabels keyed by original candidate
ID. The producer can explain why review is needed, label an artifact Mark
reviewed, and name decision candidates Approve, Request changes or
Decline without changing canonical item values, events or queue binding.
Flow Agents passes these display hints directly to Survey's console. They do not
supply workflow authority, waive findings or resume execution. Request JSON cannot
configure host-level presentation; the installed producer owns these hints.
The request must be a regular non-symlink JSON file at most 1 MiB.
The public schema is schemas/kit-review-request.schema.json.
Create a private request with only schema: "kontour.kit.review_request",
version: "1.0", kit_id and domain-specific input, then run:
flow-agents kit review <kit-id> --dest <installed-root> --request <private-review-request.json> --port 0
Flow Agents verifies installed content and jailed .mjs entry, opens Survey on
loopback, and prints the URL, session path, kit ID and source hash. It rechecks
installed integrity before application. Changed kit bytes reject an open review;
Standalone installed bundles include the exact published Survey server, queue
services, original browser assets, license notices and digest provenance. They
require no node_modules; ordinary kit commands load no Survey runtime.
Restart a long-lived SDK host after updating a previously imported kit so cached
ES modules cannot supply old dependencies. SIGINT/SIGTERM close only this console.
The host creates no model worker and performs no gate continuation. Survey derives
persisted events against the original binding and refuses unresolved or consumed
sessions. The producer remains responsible for cross-store write-loss recovery.
SDK consumers can import openInstalledKitReview or serveSurveyReview from
@kontourai/flow-agents/kit-review. Trusted host applications may pass
host.resolveAuthority directly in memory; JSON requests cannot supply host
capabilities. Kits with protected human decisions must require their own trusted
resolver and durable authority artifact. Existing paused-gate Survey APIs continue
to handle Flow-owned gates; a kit-owned pending action does not become a paused gate.