diff --git a/README.md b/README.md index 78efb98..0b4983d 100644 --- a/README.md +++ b/README.md @@ -598,15 +598,19 @@ The agent loop is gated (the PreToolUse install-gate across 11 harnesses), but a human typing `npm i x` / `npx y` / `brew install z` in their own terminal reaches the machine ungated. `kit guard install` writes PATH shims for the install + fetch-and-run family (`npm npx pnpm yarn bun bunx pip pip3 pipx uv uvx brew gem -cargo`) that run the **same hardened parser + triage verdict** the agent gate +cargo git`) that run the **same hardened parser + triage verdict** the agent gate uses — and log what it WOULD decide to `~/.kit/guard-observe.jsonl`. v1 is **observe-only** by the exec-broker discipline (observe → evidence → enforce): a shim never blocks and never breaks the tool — kit missing or crashing means unchanged behavior, non-install subcommands pass silently, and `KIT_GUARD_BYPASS=1` skips observation for one call. `kit guard status` shows -what has passed through and what enforce mode would have stopped; `kit guard -uninstall` removes everything. Notable: the shims also see the `npx`-spawned +what has passed through, what enforce mode would have stopped, and whether a +fresh login shell actually resolves each guarded tool to kit's shim (so a later +PATH prepend cannot silently displace it); `kit guard uninstall` removes +everything. `git clone` is **observe-only**, not hard-gated: the shim records +repo intake and points at `kit triage repo ` for third-party dependencies. +Notable: the shims also see the `npx`-spawned MCP servers agent harnesses launch **outside** any Bash gate — coverage the PreToolUse hook can't reach. diff --git a/contracts/kit.opencli.json b/contracts/kit.opencli.json index 26bfad1..dd5863f 100644 --- a/contracts/kit.opencli.json +++ b/contracts/kit.opencli.json @@ -3,7 +3,65 @@ "add": { "kind": "command", "summary": "Provision a service (kit add --list to see all adapters)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--category", + "--cf-worker", + "--dry-run", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--github-repo", + "--help", + "--host", + "--install", + "--json", + "--keep-commented", + "--key", + "--key-id", + "--lenient", + "--list", + "--minimal", + "--mode", + "--no-clean", + "--no-recommended", + "--no-setup", + "--no-triage", + "--non-interactive", + "--path", + "--pin", + "--plan", + "--project", + "--purge", + "--railway-service", + "--read-only", + "--readonly", + "--recommended", + "--retry-count", + "--secrets-only", + "--service", + "--services", + "--stdin", + "--store", + "--strict", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--verify", + "--version", + "--via", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -11,7 +69,15 @@ "adr": { "kind": "command", "summary": "Enforce architecture decisions (ADR → gate): 'kit adr check' gates the repo on accepted ADRs' deterministic kit-enforce rules, cited to the ADR; 'kit adr list' shows enforced/documented. Zero-LLM (prose is never interpreted).", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -19,7 +85,19 @@ "agent-audit": { "kind": "command", "summary": "Audit agent / MCP / hook configs for plaintext secrets + malware-shaped hooks", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -27,7 +105,20 @@ "agent-config": { "kind": "command", "summary": "Inject a managed 'use kit' block into CLAUDE.md / AGENTS.md / .cursorrules / .clinerules / .github/copilot-instructions.md", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--install-gate", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -37,7 +128,16 @@ "verify": { "kind": "command", "summary": "Prove zero-egress: assert every scanner that would run air-gapped resolves to a local artifact (no cloud-only, no registry semgrep config)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -45,7 +145,16 @@ }, "kind": "group", "summary": "Air-gap posture tools (verify)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -53,7 +162,26 @@ "analyze": { "kind": "command", "summary": "Analyze repo + emit draft CLAUDE.md / RULES.md", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--claude", + "--env", + "--fail-on-warning", + "--format", + "--help", + "--json", + "--list-rules", + "--list-standards", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--rules", + "--verify", + "--version", + "--write" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -63,7 +191,25 @@ "anchor": { "kind": "command", "summary": "Seal the audit log with the machine-local HMAC key (--external also gets a receipt from KIT_EXTERNAL_ANCHOR_CMD — closes the same-UID gap)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--env", + "--external", + "--format", + "--help", + "--json", + "--key", + "--limit", + "--non-interactive", + "--operation", + "--read-only", + "--readonly", + "--require-external", + "--since-days", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -71,7 +217,25 @@ "export": { "kind": "command", "summary": "Emit the audit log for a SIEM (--format cef|syslog|json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--env", + "--external", + "--format", + "--help", + "--json", + "--key", + "--limit", + "--non-interactive", + "--operation", + "--read-only", + "--readonly", + "--require-external", + "--since-days", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -79,7 +243,25 @@ "secrets": { "kind": "command", "summary": "Forensics: who/what touched each key + when (reads audit log)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--env", + "--external", + "--format", + "--help", + "--json", + "--key", + "--limit", + "--non-interactive", + "--operation", + "--read-only", + "--readonly", + "--require-external", + "--since-days", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -87,7 +269,25 @@ "verify": { "kind": "command", "summary": "Verify the audit log's keyless hash chain + HMAC anchor (--require-external demands a TSA receipt; exit 1 on break/forge/truncation)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--env", + "--external", + "--format", + "--help", + "--json", + "--key", + "--limit", + "--non-interactive", + "--operation", + "--read-only", + "--readonly", + "--require-external", + "--since-days", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -95,7 +295,25 @@ }, "kind": "group", "summary": "View audit log of kit operations", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--env", + "--external", + "--format", + "--help", + "--json", + "--key", + "--limit", + "--non-interactive", + "--operation", + "--read-only", + "--readonly", + "--require-external", + "--since-days", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -105,7 +323,22 @@ "elevate": { "kind": "command", "summary": "Mint elevation marker for destructive secret ops (TOTP/yes-prompt); --list-scopes to see what each scope unlocks first (--json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--account", + "--env", + "--help", + "--issuer", + "--json", + "--list-scopes", + "--non-interactive", + "--overwrite", + "--read-only", + "--readonly", + "--scope", + "--ttl-minutes", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -113,7 +346,22 @@ "revoke": { "kind": "command", "summary": "Drop the elevation marker", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--account", + "--env", + "--help", + "--issuer", + "--json", + "--list-scopes", + "--non-interactive", + "--overwrite", + "--read-only", + "--readonly", + "--scope", + "--ttl-minutes", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -121,7 +369,22 @@ "setup-totp": { "kind": "command", "summary": "Enroll TOTP secret (writes ~/.kit/totp-secret 0600)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--account", + "--env", + "--help", + "--issuer", + "--json", + "--list-scopes", + "--non-interactive", + "--overwrite", + "--read-only", + "--readonly", + "--scope", + "--ttl-minutes", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -129,7 +392,22 @@ "status": { "kind": "command", "summary": "Show active elevation", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--account", + "--env", + "--help", + "--issuer", + "--json", + "--list-scopes", + "--non-interactive", + "--overwrite", + "--read-only", + "--readonly", + "--scope", + "--ttl-minutes", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -137,7 +415,22 @@ }, "kind": "group", "summary": "TOTP-gated elevation for destructive secret ops (elevate|status|revoke|setup-totp)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--account", + "--env", + "--help", + "--issuer", + "--json", + "--list-scopes", + "--non-interactive", + "--overwrite", + "--read-only", + "--readonly", + "--scope", + "--ttl-minutes", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -145,7 +438,18 @@ "baseline": { "kind": "command", "summary": "Freeze current warnings into .kit-baseline.json so future runs gate only net-new findings", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--category", + "--enforce", + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -153,7 +457,19 @@ "bootstrap": { "kind": "command", "summary": "Cold-start an ephemeral environment in one command: setup → identity → policy pull → profile import → memory restore, from one platform-injected seed (fail-closed floor, fail-open fuel, --json receipt) (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--minimal", + "--no-memory", + "--non-interactive", + "--profile", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -163,7 +479,18 @@ "enforce": { "kind": "command", "summary": "Guided observe→enforce flip: readiness pre-flight (refuses unless ready; --force overrides), set [scope].enforce_runtime = true, re-sign the profile scope, and audit the transition", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--gate", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -171,7 +498,18 @@ "enforce-readiness": { "kind": "command", "summary": "Read the recorded observe window (.kit-audit.jsonl) and report whether it's safe to flip exec-broker to enforce: ready | would-block (+ exactly what breaks) | untested (--gate fails CI on any not-ready verdict)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--gate", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -179,7 +517,18 @@ }, "kind": "group", "summary": "exec-broker runtime posture — graduate observe→enforce on evidence: enforce-readiness reports ready | would-block (+ what breaks) | untested; enforce does the guided, signed flip (refuses unless ready, --force overrides) (--json, --gate, --force)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--gate", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -189,7 +538,16 @@ "cdp-url": { "kind": "command", "summary": "Print the detected Chrome DevTools Protocol URL, if one is configured or reachable (--json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -197,7 +555,16 @@ "doctor": { "kind": "command", "summary": "Diagnose browser-verification readiness from [browser], selecting Playwright, system Chrome, CDP, or a blocker (--json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -205,7 +572,16 @@ "playwright-env": { "kind": "command", "summary": "Print shell exports for browser test runners (--json emits the same env object)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -213,7 +589,16 @@ "status": { "kind": "command", "summary": "Print the selected browser strategy and top-level verdict (--json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -221,7 +606,16 @@ }, "kind": "group", "summary": "Browser-verification diagnostics: select Playwright/system Chrome/CDP strategy from [browser] without guessing (doctor|status|cdp-url|playwright-env; --json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -231,7 +625,27 @@ "compare": { "kind": "command", "summary": "Diff two `kit check --json` runs — what changed, with lost coverage ranked above a regression", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--category", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--help", + "--json", + "--key", + "--lenient", + "--no-auto-install", + "--non-interactive", + "--pin", + "--read-only", + "--readonly", + "--self", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -239,7 +653,27 @@ "verify-attestation": { "kind": "command", "summary": "Verify a signed .kit-check-attestation.json receipt", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--category", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--help", + "--json", + "--key", + "--lenient", + "--no-auto-install", + "--non-interactive", + "--pin", + "--read-only", + "--readonly", + "--self", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -247,7 +681,27 @@ }, "kind": "group", "summary": "Check status of tools, services, secrets, hooks, deploy env, security, tests, and lock files", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--category", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--help", + "--json", + "--key", + "--lenient", + "--no-auto-install", + "--non-interactive", + "--pin", + "--read-only", + "--readonly", + "--self", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -255,7 +709,24 @@ "ci": { "kind": "command", "summary": "CI-native check: GitHub Actions annotations, GitLab JUnit, JSON (--init gitlab|bitbucket scaffolds a pipeline)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--fail-on-warning", + "--format", + "--help", + "--init", + "--json", + "--lenient", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--strict", + "--version", + "--write" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -263,7 +734,28 @@ "clone": { "kind": "command", "summary": "Clone a Git repository and run kit setup", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--install", + "--list", + "--minimal", + "--mode", + "--no-recommended", + "--no-setup", + "--non-interactive", + "--read-only", + "--readonly", + "--recommended", + "--services", + "--skip-install", + "--store", + "--verify", + "--version", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -273,7 +765,20 @@ "knobs": { "kind": "command", "summary": "List power-user env vars + .kit.toml fields kit honors (--json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--allow-comment-loss", + "--check", + "--dry-run", + "--env", + "--force", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -281,7 +786,20 @@ "migrate": { "kind": "command", "summary": "Migrate .kit.toml to the current schema version (--dry-run inspect, --check CI gate, --force overwrite backup)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--allow-comment-loss", + "--check", + "--dry-run", + "--env", + "--force", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -289,7 +807,20 @@ }, "kind": "group", "summary": "Inspect + migrate the .kit.toml schema version", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--allow-comment-loss", + "--check", + "--dry-run", + "--env", + "--force", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -299,7 +830,19 @@ "--prompt": { "kind": "command", "summary": "Print a compact active-gcloud indicator for your shell prompt (PS1)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--format", + "--help", + "--json", + "--non-interactive", + "--prompt", + "--read-only", + "--readonly", + "--require-declaration", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -307,7 +850,19 @@ "check": { "kind": "command", "summary": "Verify each CLI's live account+project matches .kit.toml [context] (exits non-zero on mismatch)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--format", + "--help", + "--json", + "--non-interactive", + "--prompt", + "--read-only", + "--readonly", + "--require-declaration", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -315,7 +870,19 @@ "use": { "kind": "command", "summary": "Activate the declared context: gcloud config + repo git identity", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--format", + "--help", + "--json", + "--non-interactive", + "--prompt", + "--read-only", + "--readonly", + "--require-declaration", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -323,7 +890,19 @@ }, "kind": "group", "summary": "Show project context: tools, services, secrets, environment", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--format", + "--help", + "--json", + "--non-interactive", + "--prompt", + "--read-only", + "--readonly", + "--require-declaration", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -331,7 +910,27 @@ "coverage": { "kind": "command", "summary": "Evidence map: which standard's controls kit's deterministic checks auto-verify vs gap/manual/n-a — --standard=asvs|llm-top10|ssdf|agentic-top10|mcp-top10|aiuc-1|gcp-waf-security|all (default asvs); --list-standards to enumerate, [coverage].standards to toggle on/off (NOT a compliance attestation; --json for GRC tools)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--claude", + "--env", + "--fail-on-warning", + "--format", + "--help", + "--json", + "--list-rules", + "--list-standards", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--rules", + "--standard", + "--verify", + "--version", + "--write" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -339,7 +938,28 @@ "create-plugin": { "kind": "command", "summary": "Scaffold a new kit plugin package", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--install", + "--list", + "--minimal", + "--mode", + "--no-recommended", + "--no-setup", + "--non-interactive", + "--read-only", + "--readonly", + "--recommended", + "--services", + "--skip-install", + "--store", + "--verify", + "--version", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -347,7 +967,21 @@ "decisions": { "kind": "command", "summary": "Decision ledger (the review surface when nobody reads the diff): 'kit decisions add' records a choice made where the spec was silent, 'list' shows them, 'verify' is the gate's verdict. kit requires the artifact and checks its SHAPE — never its content.", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--assumed", + "--confidence", + "--decision", + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--unreviewed", + "--version", + "--would-have-asked" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -355,7 +989,17 @@ "design": { "kind": "command", "summary": "Check design quality (a11y, design tokens) against the baseline", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--enforce", + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -363,7 +1007,65 @@ "doctor": { "kind": "command", "summary": "Deep diagnostics — checks environment health in detail", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--category", + "--cf-worker", + "--dry-run", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--github-repo", + "--help", + "--host", + "--install", + "--json", + "--keep-commented", + "--key", + "--key-id", + "--lenient", + "--list", + "--minimal", + "--mode", + "--no-clean", + "--no-recommended", + "--no-setup", + "--no-triage", + "--non-interactive", + "--path", + "--pin", + "--plan", + "--project", + "--purge", + "--railway-service", + "--read-only", + "--readonly", + "--recommended", + "--retry-count", + "--secrets-only", + "--service", + "--services", + "--stdin", + "--store", + "--strict", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--verify", + "--version", + "--via", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -373,7 +1075,19 @@ "current": { "kind": "command", "summary": "Show active environment marker", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--compare", + "--env", + "--help", + "--json", + "--missing", + "--non-interactive", + "--read-only", + "--readonly", + "--show-values", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -381,7 +1095,19 @@ "switch": { "kind": "command", "summary": "Switch active environment (dev/staging/prod). Gates prod-key reads.", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--compare", + "--env", + "--help", + "--json", + "--missing", + "--non-interactive", + "--read-only", + "--readonly", + "--show-values", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -389,7 +1115,19 @@ }, "kind": "group", "summary": "Show current environment info", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--compare", + "--env", + "--help", + "--json", + "--missing", + "--non-interactive", + "--read-only", + "--readonly", + "--show-values", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -397,7 +1135,19 @@ "escalate": { "kind": "command", "summary": "List what needs human action", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -405,7 +1155,15 @@ "fix": { "kind": "command", "summary": "Auto-fix what is possible", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -413,7 +1171,24 @@ "gate-bash": { "kind": "command", "summary": "PreToolUse install-gate: read an agent's pending Bash command on stdin, block (exit 2) un-triaged installs", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--format", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -421,7 +1196,24 @@ "gate-egress": { "kind": "command", "summary": "PreToolUse egress-gate (exec-broker): block (exit 2) Bash network targets outside the signed [scope].egress — fail-closed without a verified scope", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--format", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -429,7 +1221,24 @@ "gate-env": { "kind": "command", "summary": "PreToolUse env-gate: read an agent's pending Write/Edit on stdin, block (exit 2) plaintext secrets aimed at .env* files", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--format", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -437,7 +1246,24 @@ "gate-fs": { "kind": "command", "summary": "PreToolUse fs-gate (exec-broker): block (exit 2) Write/Edit outside the signed [scope].fs — fail-closed without a verified scope", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--format", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -445,7 +1271,16 @@ "gha-audit": { "kind": "command", "summary": "CI hardening lint — unpinned actions/images + pwn-request/remote-include (GitHub Actions, GitLab CI, Bitbucket Pipelines)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -453,7 +1288,19 @@ "governance": { "kind": "command", "summary": "View governance status and agent access controls", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -461,7 +1308,23 @@ "guard": { "kind": "command", "summary": "Install gate for YOUR terminal (observe mode): PATH shims for npm/npx/brew/pip/bun/… log what the gate would decide — install | status | uninstall", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -469,7 +1332,23 @@ "guard-observe": { "kind": "command", "summary": "Shim protocol (invoked by ~/.kit/shims wrappers, not by you): record what the install gate would decide for a package-manager call", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--env", + "--help", + "--non-interactive", + "--output", + "--output-document", + "--read-only", + "--readonly", + "--version", + "--with" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -477,7 +1356,17 @@ "heal": { "kind": "command", "summary": "Loop: auto-fix safe findings, re-scan until green; gate destructive, fail-closed on tamper (--dry-run, --agent)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--agent", + "--dry-run", + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -485,7 +1374,19 @@ "health": { "kind": "command", "summary": "Deep environment health diagnostics — granular pass/fail across tools, services, config", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -495,7 +1396,16 @@ "add": { "kind": "command", "summary": "Install a built-in hook (secret-scan, post-pull-audit, context-check)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -503,7 +1413,16 @@ "uninstall": { "kind": "command", "summary": "Remove the configured git hooks (enforcement off until re-installed)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -511,7 +1430,16 @@ }, "kind": "group", "summary": "Manage git hooks", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -519,7 +1447,16 @@ "identity": { "kind": "command", "summary": "Manage this machine/agent's Ed25519 identity (init/show/rotate) — asymmetric, attributable signing for audit/policy (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--non-interactive", + "--public", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -527,7 +1464,19 @@ "ingest": { "kind": "command", "summary": "Ingest external SARIF / OSV reports into kit's consolidated verdict (kit ingest )", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -535,7 +1484,65 @@ "init": { "kind": "command", "summary": "Detect stack, generate .kit.toml, and run full setup (--no-setup: config + lock files only; offers ~/.kit/defaults.toml [init] known_services, --services a,b answers without a prompt)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--category", + "--cf-worker", + "--dry-run", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--github-repo", + "--help", + "--host", + "--install", + "--json", + "--keep-commented", + "--key", + "--key-id", + "--lenient", + "--list", + "--minimal", + "--mode", + "--no-clean", + "--no-recommended", + "--no-setup", + "--no-triage", + "--non-interactive", + "--path", + "--pin", + "--plan", + "--project", + "--purge", + "--railway-service", + "--read-only", + "--readonly", + "--recommended", + "--retry-count", + "--secrets-only", + "--service", + "--services", + "--stdin", + "--store", + "--strict", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--verify", + "--version", + "--via", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -543,7 +1550,16 @@ "insight": { "kind": "command", "summary": "Deterministic lifecycle insight (unused: loaded-but-never-called MCP servers, from the transcript index; --json)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -551,7 +1567,16 @@ "install": { "kind": "command", "summary": "Install missing tools via mise", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--no-triage", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -561,7 +1586,19 @@ "--plan": { "kind": "command", "summary": "Show the resolved auth strategy per service (vault/interactive/capture + passkey) without logging in", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--plan", + "--read-only", + "--readonly", + "--retry-count", + "--service", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -569,7 +1606,19 @@ }, "kind": "group", "summary": "Guided login to all configured services", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--non-interactive", + "--plan", + "--read-only", + "--readonly", + "--retry-count", + "--service", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -577,7 +1626,21 @@ "map": { "kind": "command", "summary": "Deterministic repo-map: the relevant slice of files around a seed (import graph, --depth, --budget, --co-change, --json) — load part of a growing repo, not all of it (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--budget", + "--co-change", + "--depth", + "--env", + "--help", + "--json", + "--name-only", + "--no-merges", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "experimental" @@ -585,7 +1648,17 @@ "mcp": { "kind": "command", "summary": "MCP server over stdio (Claude Code/Cursor/Codex); 'kit mcp list|auth|set-token|clear' manages declared servers", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--from-env", + "--help", + "--non-interactive", + "--paste", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -595,7 +1668,59 @@ "area": { "kind": "command", "summary": "Show shared entries for one area (decisions, how-built, status, security)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -603,7 +1728,59 @@ "areas": { "kind": "command", "summary": "List shared responsibility areas (stripe, whatsapp, …)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -611,7 +1788,59 @@ "backup": { "kind": "command", "summary": "Encrypted backup of the memory store (AES-256-GCM; KIT_MEMORY_PASSPHRASE)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -619,7 +1848,59 @@ "context": { "kind": "command", "summary": "Push-surface active decisions for the area(s) whose files you're touching (deterministic, path→cluster)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -627,7 +1908,59 @@ "index": { "kind": "command", "summary": "Index every supported agent transcript into the SQLite memory store", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -635,7 +1968,59 @@ "install": { "kind": "command", "summary": "Wire Claude Code prompt/start/end hooks and silent Codex start/end hooks into lifecycle config", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -643,7 +2028,59 @@ "merge": { "kind": "command", "summary": "Merge another machine's memory.db into this one (dedup by uuid)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -651,7 +2088,59 @@ "pal": { "kind": "command", "summary": "Pending action ledger — list/add/done/snooze/verify/import 'blocked-on-you' items", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -659,7 +2148,59 @@ "restore": { "kind": "command", "summary": "Restore an encrypted memory backup (e.g. on a new machine)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -667,7 +2208,59 @@ "resume": { "kind": "command", "summary": "Print the resume command for a saved copilot (by name or number)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -675,7 +2268,59 @@ "save": { "kind": "command", "summary": "Bookmark the current session as a named copilot", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -683,7 +2328,59 @@ "scan": { "kind": "command", "summary": "Scan the memory store for stored secrets, or prompt-injection patterns with --injection (exit 1 on a high-confidence finding)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -691,7 +2388,59 @@ "search": { "kind": "command", "summary": "Full-text search memory (current project; --global for all; --fresh = recency-aware ranking)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -699,7 +2448,59 @@ "share": { "kind": "command", "summary": "Promote a curated, secret-scanned entry to the shared (team) memory", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -707,7 +2508,59 @@ "stats": { "kind": "command", "summary": "Show what the local memory store contains", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -715,7 +2568,59 @@ "suggest": { "kind": "command", "summary": "Emit a BYO-LLM review prompt (recent activity + open items) — pipe to your own model", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -723,7 +2628,59 @@ "sync": { "kind": "command", "summary": "Sync from a memory export or encrypted backup (mergeDb; last-write-wins, file_index excluded)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -731,7 +2688,59 @@ "threads": { "kind": "command", "summary": "List saved copilots (current project; --global for all)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -739,7 +2748,59 @@ }, "kind": "group", "summary": "Local conversation memory — index transcripts + show stats", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--all", + "--area", + "--auto", + "--body", + "--branch", + "--brief", + "--changed", + "--command", + "--confidence", + "--env", + "--expect", + "--force", + "--fresh", + "--global", + "--heatmap", + "--help", + "--include-quarantined", + "--injection", + "--json", + "--kind", + "--limit", + "--no-statusline", + "--non-interactive", + "--obsidian", + "--passphrase", + "--project", + "--provenance", + "--pull-cmd", + "--push-cmd", + "--quarantine", + "--read-only", + "--readonly", + "--reason", + "--ref", + "--remap-project", + "--remote", + "--reverses", + "--scaffold", + "--scope", + "--session", + "--stale", + "--status", + "--strict", + "--supersedes", + "--title", + "--to", + "--tokens", + "--verify-file", + "--verify-http", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -747,7 +2808,28 @@ "open": { "kind": "command", "summary": "Open service dashboard in browser (stripe, vercel, railway, etc.)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--install", + "--list", + "--minimal", + "--mode", + "--no-recommended", + "--no-setup", + "--non-interactive", + "--read-only", + "--readonly", + "--recommended", + "--services", + "--skip-install", + "--store", + "--verify", + "--version", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -755,7 +2837,17 @@ "panic": { "kind": "command", "summary": "Compromise response: rotate identity + emit a signed revocation + audit it + print the platform-revocation checklist (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--no-checklist", + "--non-interactive", + "--read-only", + "--readonly", + "--reason", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -763,7 +2855,17 @@ "pkg": { "kind": "command", "summary": "Install package with mandatory triage (kit pkg npm:express)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--agent", + "--dry-run", + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -771,7 +2873,16 @@ "plugin": { "kind": "command", "summary": "Discover and manage kit plugins (search, list, scaffold, install)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--tag", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -779,7 +2890,23 @@ "policy": { "kind": "command", "summary": "Signable org policy-as-code in .kit-policy.toml (init/show/validate/sign/verify/check/trust/pull/pull-revocations/approve) — identity-signed standard, org-distributable + enforced offline (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--help", + "--json", + "--key", + "--label", + "--list", + "--non-interactive", + "--read-only", + "--readonly", + "--remove", + "--strict", + "--ttl", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -789,7 +2916,19 @@ "check": { "kind": "command", "summary": "Report declared-vs-discovered drift (--gate fails CI on any drift; honest skip when no profile declared)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -797,7 +2936,19 @@ "export": { "kind": "command", "summary": "Export a portable signed bundle (profile + signature + signer key) to --out or stdout", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -805,7 +2956,19 @@ "freeze": { "kind": "command", "summary": "Snapshot the discovered toolchain into .kit-profile.toml (preserves operator-authored workflows/plugins/scope/gates)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -813,7 +2976,19 @@ "import": { "kind": "command", "summary": "Import a portable bundle on a fresh host — integrity-verify offline, fail-closed on tamper/revoked (authoritative only once the signer is anchored)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -821,7 +2996,19 @@ "show": { "kind": "command", "summary": "Render the declared project profile with per-line reconciliation marks", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -829,7 +3016,19 @@ "sign": { "kind": "command", "summary": "Sign the profile (scope/RoE) into .kit-profile.sig via your identity/keystore — offline-verifiable", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -837,7 +3036,19 @@ "verify": { "kind": "command", "summary": "Verify .kit-profile.sig offline (--key pin → local identity → org .kit-policy.signers)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -845,7 +3056,19 @@ }, "kind": "group", "summary": "Versioned, traveling project profile — declare {skills, mcp, workflows, plugins, vault, gates, scope}, audit declared-vs-discovered drift, sign the scope/RoE, and export/import a portable signed bundle to a fresh host (show|freeze|check|sign|verify|export|import; --json, --gate, --key, --out)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--key", + "--non-interactive", + "--out", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -853,7 +3076,25 @@ "review": { "kind": "command", "summary": "Full repo audit — runs check + design + standards + adr in one gate (for agents / PR checks; --json emits one structured report; --stages check,standards scopes the run, --category scopes the standards stage)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--category", + "--enforce", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--help", + "--json", + "--lenient", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--stages", + "--strict", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -861,7 +3102,28 @@ "run": { "kind": "command", "summary": "Execute a command with project env vars loaded", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--install", + "--list", + "--minimal", + "--mode", + "--no-recommended", + "--no-setup", + "--non-interactive", + "--read-only", + "--readonly", + "--recommended", + "--services", + "--skip-install", + "--store", + "--verify", + "--version", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -869,7 +3131,17 @@ "sbom": { "kind": "command", "summary": "Generate a CycloneDX / SPDX SBOM from the lockfile (SARIF emit via kit scan --sarif)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--agent", + "--env", + "--format", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -877,7 +3149,20 @@ "scan": { "kind": "command", "summary": "Run external scanners (snyk/trivy/grype/semgrep/osv) and merge them into one local verdict; --list-delegates shows the toggleable scanner library, [scan].delegates picks which run (--strict / [governance.scan] required_scanners gate non-running scanners)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--list-delegates", + "--non-interactive", + "--read-only", + "--readonly", + "--sarif", + "--strict", + "--update-baseline", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -887,7 +3172,47 @@ "migrate": { "kind": "command", "summary": "Migrate plaintext secrets in .env* → configured vault", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -895,7 +3220,47 @@ "onecli": { "kind": "command", "summary": "Register a key with OneCLI gateway so agent never sees the real value", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -903,7 +3268,47 @@ "propagate": { "kind": "command", "summary": "Push a value to deploy targets only (skips vault-write). --stdin safer than --value", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -911,7 +3316,47 @@ "purge-history": { "kind": "command", "summary": "Destructive: rewrite git history to remove a leaked value (--force-history)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -919,7 +3364,47 @@ "revoke-old": { "kind": "command", "summary": "Revoke a previously-minted scoped key (Supabase Mgmt API)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -927,7 +3412,47 @@ "rotate": { "kind": "command", "summary": "Rotate a key: write new value to vault (explicit / random)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -935,7 +3460,47 @@ "set": { "kind": "command", "summary": "Capture a value to the vault: kit secrets set --stdin (safer) | --value ", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -943,7 +3508,47 @@ "sync": { "kind": "command", "summary": "Push resolved secrets to GitHub Actions / .env.ci / stdout", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -951,7 +3556,47 @@ }, "kind": "group", "summary": "Generate .env.local from template + secret store", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--cf-worker", + "--dry-run", + "--env", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--from-cli", + "--github-repo", + "--help", + "--host", + "--keep-commented", + "--key-id", + "--mode", + "--no-clean", + "--non-interactive", + "--path", + "--project", + "--propagate", + "--purge", + "--railway-service", + "--random", + "--read-only", + "--readonly", + "--secrets-only", + "--stdin", + "--store", + "--target", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--version", + "--via", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -961,7 +3606,27 @@ "check-gitignore": { "kind": "command", "summary": "Verify .gitignore covers sensitive paths (--fix to auto-patch)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -969,7 +3634,27 @@ "clear-cache": { "kind": "command", "summary": "Clear cached scanner binary (after intentional rebuild)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -977,7 +3662,27 @@ "costs": { "kind": "command", "summary": "Snapshot per-key spend vs policy cap (Stripe live; others stubbed)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -985,7 +3690,27 @@ "policy": { "kind": "command", "summary": "Dependency allowlist enforcement (init|add|check)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -993,7 +3718,27 @@ "prescan": { "kind": "command", "summary": "Multi-repo baseline sweep (secrets, gitignore, branch-protect; --deep adds CVE/workflow-drift/bumblebee; --format=json + --vs-baseline= for CI drift)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1001,7 +3746,27 @@ "prescan-diff": { "kind": "command", "summary": "Diff two prescan reports — surface new regressions + fixed findings since baseline", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1009,7 +3774,27 @@ "scan-build": { "kind": "command", "summary": "Scan build artifacts (.next/, dist/) for inlined secrets", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1017,7 +3802,27 @@ "scan-staged": { "kind": "command", "summary": "Pre-commit: scan staged files for credential patterns", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1025,7 +3830,27 @@ "scan-transcripts": { "kind": "command", "summary": "Scan agent transcripts + prompt caches for leaked credentials", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1033,7 +3858,27 @@ "verify-pull": { "kind": "command", "summary": "After git pull: audit new deps, gitignore drops, introduced secrets", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1041,7 +3886,27 @@ }, "kind": "group", "summary": "Security policy + scanners (policy | scan-staged | scan-build | verify-pull | prescan | …)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--accept", + "--base", + "--deep", + "--env", + "--exclude", + "--fix", + "--format", + "--head", + "--help", + "--json", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--recursive", + "--skip", + "--version", + "--vs-baseline" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1049,7 +3914,27 @@ "self-audit": { "kind": "command", "summary": "Audit kit's own source against its 12 self-hardening rules (--list-rules, --only=, --format)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--claude", + "--env", + "--fail-on-warning", + "--format", + "--help", + "--json", + "--list-rules", + "--list-standards", + "--no-auto-install", + "--non-interactive", + "--only", + "--read-only", + "--readonly", + "--rules", + "--verify", + "--version", + "--write" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1057,7 +3942,21 @@ "sentinel": { "kind": "command", "summary": "Autonomous redline watcher — propose/apply guarded remediations (run|install|status)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--force", + "--help", + "--json", + "--label", + "--limit", + "--non-interactive", + "--read-only", + "--readonly", + "--schedule", + "--state", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1067,7 +3966,65 @@ "--recommended": { "kind": "command", "summary": "Opinionated profile: setup + memory hooks + git secret-scan/context-check gates", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--category", + "--cf-worker", + "--dry-run", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--github-repo", + "--help", + "--host", + "--install", + "--json", + "--keep-commented", + "--key", + "--key-id", + "--lenient", + "--list", + "--minimal", + "--mode", + "--no-clean", + "--no-recommended", + "--no-setup", + "--no-triage", + "--non-interactive", + "--path", + "--pin", + "--plan", + "--project", + "--purge", + "--railway-service", + "--read-only", + "--readonly", + "--recommended", + "--retry-count", + "--secrets-only", + "--service", + "--services", + "--stdin", + "--store", + "--strict", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--verify", + "--version", + "--via", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1075,7 +4032,65 @@ }, "kind": "group", "summary": "Full pipeline: install → login → secrets → agent config → verify", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--auto", + "--aws-region", + "--category", + "--cf-worker", + "--dry-run", + "--enforce-tests", + "--env", + "--fail-on-warning", + "--fail-on-worse", + "--fix", + "--fly-app", + "--force", + "--force-history", + "--from", + "--github-repo", + "--help", + "--host", + "--install", + "--json", + "--keep-commented", + "--key", + "--key-id", + "--lenient", + "--list", + "--minimal", + "--mode", + "--no-clean", + "--no-recommended", + "--no-setup", + "--no-triage", + "--non-interactive", + "--path", + "--pin", + "--plan", + "--project", + "--purge", + "--railway-service", + "--read-only", + "--readonly", + "--recommended", + "--retry-count", + "--secrets-only", + "--service", + "--services", + "--stdin", + "--store", + "--strict", + "--target-env", + "--to", + "--value", + "--vercel-scope", + "--verify", + "--version", + "--via", + "--with-migrate", + "--yes" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1083,7 +4098,20 @@ "skill": { "kind": "command", "summary": "Module-discipline linter for a SKILL.md: contract shape, trigger + sibling-collision, declared least-privilege scope, and CI regression drift — plus --runtime to audit recorded runs for scope-adherence + negative controls (from the transcript index, zero-LLM) (test [--runtime] [--json] [--gate] [--update-snapshot]) — proves a skill is engineered like a module, NOT that its output is good (rubric grading is delegated) (experimental)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--gate", + "--help", + "--json", + "--non-interactive", + "--path", + "--read-only", + "--readonly", + "--runtime", + "--update-snapshot", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -1091,7 +4119,19 @@ "skills": { "kind": "command", "summary": "Check status of agent skills", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1099,7 +4139,16 @@ "slopsquat": { "kind": "command", "summary": "Score npm/PyPI packages for hallucination/slopsquat risk (registry metadata)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--fail-on", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -1107,7 +4156,18 @@ "standards": { "kind": "command", "summary": "Dev-standards gate: general metrics + per-language linters + user plugins vs the baseline (--category general|specific|plugins|, --enforce fails CI)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--category", + "--enforce", + "--env", + "--help", + "--json", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1115,7 +4175,19 @@ "status": { "kind": "command", "summary": "Adoption checklist — what's set up across kit + the next step for each gap", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1123,7 +4195,19 @@ "statusline": { "kind": "command", "summary": "Compact one-line status (mode score · update · open PAL) for Claude Code statusLine / a shell PS1; Codex gets it via SessionStart context", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "harness", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1131,7 +4215,19 @@ "supply-chain": { "kind": "command", "summary": "Install-time supply-chain triage: install-scripts, lockfile-drift, dep-confusion, slopsquat", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1139,7 +4235,20 @@ "team": { "kind": "command", "summary": "Manage team members, roles, and permissions (RBAC, invitations, audit logs)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--broker-gate", + "--env", + "--help", + "--limit", + "--mode", + "--no-install-gate", + "--non-interactive", + "--read-only", + "--readonly", + "--role", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -1147,7 +4256,17 @@ "tools": { "kind": "command", "summary": "Inventory the CLIs on this machine — path, installer, installed version, and (with --latest) how far behind each one is", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--json", + "--latest", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "experimental" @@ -1155,7 +4274,23 @@ "triage": { "kind": "command", "summary": "Security evaluation before installing packages, images, or skills", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--cached", + "--deep", + "--env", + "--help", + "--json", + "--name-only", + "--non-interactive", + "--pin", + "--read-only", + "--readonly", + "--sandbox", + "--scan-bytes", + "--tools", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": true, "x-kit-stability": "stable" @@ -1163,7 +4298,16 @@ "upgrade": { "kind": "command", "summary": "Update lock files from .kit.toml", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--env", + "--help", + "--non-interactive", + "--read-only", + "--readonly", + "--self", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1171,7 +4315,19 @@ "usage": { "kind": "command", "summary": "Tabbed report of what kit has recorded — coverage with its denominator, refusals, memory, triage — plus --prove, which runs the floor against inputs it must refuse", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--deep", + "--env", + "--help", + "--json", + "--non-interactive", + "--prove", + "--read-only", + "--readonly", + "--tab", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1179,7 +4335,20 @@ "verify-provenance": { "kind": "command", "summary": "Verify a release's SLSA provenance bundle offline (Ed25519 + SHA256 / cosign)", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--bundle", + "--env", + "--help", + "--identity", + "--issuer", + "--non-interactive", + "--offline", + "--read-only", + "--readonly", + "--trusted-root", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "all", "x-kit-mcp": false, "x-kit-stability": "stable" @@ -1187,7 +4356,19 @@ "whoami": { "kind": "command", "summary": "Show current agent / user identity", - "x-kit-args-modeled": false, + "x-kit-accepted-flags": [ + "--attest", + "--env", + "--help", + "--json", + "--mode", + "--no-auto-install", + "--non-interactive", + "--read-only", + "--readonly", + "--version" + ], + "x-kit-args-modeled": true, "x-kit-audience": "human", "x-kit-mcp": false, "x-kit-stability": "stable" diff --git a/docs/CLI_STABILITY.md b/docs/CLI_STABILITY.md index d644997..d53dd94 100644 --- a/docs/CLI_STABILITY.md +++ b/docs/CLI_STABILITY.md @@ -38,9 +38,12 @@ document — [`contracts/kit.opencli.json`](../contracts/kit.opencli.json) — g from the same `COMMAND_REGISTRY` source of truth (`scripts/gen-opencli.mjs`, drift-tested by `opencli.test.ts`). It's an "OpenAPI for CLIs" description so an external agent or tool can learn kit's commands without scraping `--help`; each command carries `x-kit-stability` -and `x-kit-mcp`, and `x-kit-args-modeled: false` marks that per-flag argument metadata is -not modeled yet (absent args/flags must not be read as "takes none"). Treated as an output -format, not a dependency — the spec is pre-1.0. +and `x-kit-mcp`. Each command also carries `x-kit-args-modeled: true` plus +`x-kit-accepted-flags`, the dispatch-level long-flag allowlist generated from +`src/flag-surface.ts` and kit's global flags. Standard OpenCLI `args` / `flags` +objects are still omitted because kit does not yet model positional args or flag +type/arity/choice metadata; absent standard args/flags must not be read as "takes +none". Treated as an output format, not a dependency — the spec is pre-1.0. ## Stable command promise diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 9ad622c..1c8f55c 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,9 @@ > (`x-kit-mcp`), and primary audience (`x-kit-audience`: `human` for > interactive/setup commands, `harness` for hook stdin protocols like > `gate-*`/`statusline` that neither humans nor agents invoke directly, or -> `all`). Agents should prefer that contract over parsing this document. +> `all`). It also carries `x-kit-accepted-flags`, the generated per-command +> allowlist for long flags kit accepts. Agents should prefer that contract over +> parsing this document. ## Global flags diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md index 667958d..971984a 100644 --- a/docs/MIGRATION_GUIDE.md +++ b/docs/MIGRATION_GUIDE.md @@ -312,17 +312,17 @@ RUN kit setup ### Debugging Setup Issues ```bash -# See detailed setup logs -kit setup --verbose +# Re-run the full setup profile explicitly +kit setup --mode full -# Check specific service -kit add database/supabase --check +# Preview a service provision +kit add database/supabase --dry-run # Verify environment kit check -# See what would be installed -kit install --dry-run +# See missing tools before installing them +kit check --category tools ``` ### Rolling Back Migration diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 9e63b26..673c9a7 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -86,10 +86,14 @@ config knob says otherwise. missing triage script, or an unmappable ref all block (fail-closed). The only bypass is `--no-triage`, which requires a one-shot elevation and is audit-logged; `kit heal` never takes it. See "The triage gate" below. -- **No interception of arbitrary installs run OUTSIDE kit.** The gate covers - installs kit itself drives. kit does not hook `npm`/`pip`/`docker` to block an - un-triaged `npm install` an agent runs directly in the shell — that enforcement - would live in the agent host. For installs kit performs, the gate is the kernel. +- **No universal interception of arbitrary installs run OUTSIDE kit.** The gate + covers installs kit itself drives. Supported agent hosts can install a + PreToolUse/command hook that blocks untriaged package installs before the + shell runs them, and `kit guard install` can add observe-only PATH shims for + package-manager calls and `git clone`. Those shims never block. There is no + OS-level hook that catches every `npm`/`pip`/`docker` invocation in an + uninstrumented shell or unsupported agent. For installs kit performs, the gate + is the kernel. ## Trust controls diff --git a/scripts/verify-suite.sh b/scripts/verify-suite.sh index c3db165..4a3cb14 100755 --- a/scripts/verify-suite.sh +++ b/scripts/verify-suite.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash # kit verify-suite — comprehensive end-to-end sweep +# kit-self-audit: operator-run # # Runs every documented kit subcommand against three contexts: # 1. BROWNFIELD: an existing real project (default: ${1:-.}) diff --git a/src/commands/guard.ts b/src/commands/guard.ts index 7b0fe26..2c118bb 100644 --- a/src/commands/guard.ts +++ b/src/commands/guard.ts @@ -4,6 +4,7 @@ * See src/guard.ts for the design contract: v1 observes, never blocks. */ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { execFileSync } from "node:child_process"; import { c } from "../utils/colors.js"; import { GUARD_TOOLS, @@ -20,6 +21,8 @@ import { staleShims, refreshShims, SHIM_MARKER, + guardPathWinners, + gitCloneObservation, } from "../guard.js"; /** Shim protocol: reconstruct the command, ask the SAME gate the agents use what @@ -36,8 +39,11 @@ export async function cmdGuardObserve(): Promise { const command = [tool, ...rest].join(" "); const { parseInstallCommand, decideBashGate } = await import("../install-gate.js"); const probe = parseInstallCommand(command); - if (!probe.isInstall) return true; // non-install invocation — no signal, no noise - const verdict = await decideBashGate(command, undefined, process.cwd()); + const clone = gitCloneObservation(tool, rest); + if (!probe.isInstall && !clone) return true; // non-install invocation — no signal, no noise + const verdict = probe.isInstall + ? await decideBashGate(command, undefined, process.cwd()) + : { block: false, reason: clone!.reason }; appendObservation({ ts: new Date().toISOString(), cwd: process.cwd(), @@ -45,7 +51,7 @@ export async function cmdGuardObserve(): Promise { command: command.slice(0, 200), wouldBlock: verdict.block, reason: verdict.reason.slice(0, 300), - refs: probe.refs, + refs: clone ? [clone.ref] : probe.refs, }); } catch { // observe is best-effort by contract — the shim runs the real tool regardless @@ -107,6 +113,23 @@ function uninstallGuard(): boolean { return true; } +function loginShellPath(): string | null { + try { + const shell = process.env.SHELL; + if (!shell || !existsSync(shell)) return null; + const marker = "__KIT_GUARD_PATH__"; + const out = execFileSync(shell, ["-lic", `printf '\\n${marker}%s\\n' "$PATH"`], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 2500, + }); + const line = out.split("\n").find((l) => l.startsWith(marker)); + return line ? line.slice(marker.length) : null; + } catch { + return null; + } +} + function guardStatus(): boolean { const dir = guardShimsDir(); const shims = GUARD_TOOLS.filter((t) => { @@ -119,14 +142,26 @@ function guardStatus(): boolean { } }); const onPath = (process.env.PATH ?? "").split(":").includes(dir); + const loginPath = loginShellPath(); + const winners = guardPathWinners(dir, loginPath ?? process.env.PATH ?? ""); + const covered = winners.filter((w) => w.kitWins).length; + const displaced = winners.filter((w) => w.installed && !w.kitWins); console.log(`${c.bold}kit guard${c.reset} — observe mode (never blocks)`); console.log( ` shims: ${shims.length}/${GUARD_TOOLS.length} installed · PATH active in this shell: ${onPath ? `${c.green}yes${c.reset}` : `${c.yellow}no${c.reset}`}`, ); + console.log( + ` login-shell PATH: ${loginPath ? "checked" : "unavailable (current PATH fallback)"} · ${covered}/${GUARD_TOOLS.length} shim(s) win command resolution`, + ); + for (const w of displaced.slice(0, 5)) { + console.log( + ` ${c.yellow}!${c.reset} ${w.tool} resolves to ${w.winner ?? "not found"} before kit's shim — move kit guard's PATH block after later PATH prepends`, + ); + } const obs = readObservations(); const wouldBlock = obs.filter((o) => o.wouldBlock); console.log( - ` observations: ${obs.length} install-shaped command(s) · ${wouldBlock.length > 0 ? c.yellow : c.green}${wouldBlock.length} would have been blocked${c.reset}`, + ` observations: ${obs.length} guarded command(s) · ${wouldBlock.length > 0 ? c.yellow : c.green}${wouldBlock.length} would have been blocked${c.reset}`, ); for (const o of obs.slice(-5)) { const icon = o.wouldBlock ? `${c.yellow}!${c.reset}` : `${c.green}✓${c.reset}`; diff --git a/src/commands/memory.ts b/src/commands/memory.ts index c31ff5f..d9837cd 100644 --- a/src/commands/memory.ts +++ b/src/commands/memory.ts @@ -20,7 +20,7 @@ import { import { effectiveMemoryClass, formatClassResolution } from "../memory/effective-class.js"; import { sparkline, fmtTokens } from "../memory/stats.js"; import { indexAllHarnesses } from "../memory/parser.js"; -import { mergeDb } from "../memory/merge.js"; +import { mergeDb, type MergeResult } from "../memory/merge.js"; import { buildSuggestPrompt } from "../memory/suggest.js"; import { learnRecurring } from "../memory/learn.js"; import { scaffoldFromCandidate } from "../insight/scaffold.js"; @@ -486,6 +486,19 @@ async function memIndex(): Promise { return true; } +function mergePayloadChanges(r: MergeResult): number { + return ( + r.messages + r.toolUses + r.pending + r.threads + r.tombstones + r.tombstoneDeletedMessages + ); +} + +function printTombstoneMergeStats(r: MergeResult): void { + if (r.tombstones + r.tombstoneDeletedMessages + r.tombstoneBlockedMessages === 0) return; + console.log( + ` ${c.dim}tombstones: ${r.tombstones} merged · ${r.tombstoneDeletedMessages} local rows deleted · ${r.tombstoneBlockedMessages} incoming rows blocked${c.reset}`, + ); +} + async function memMerge(): Promise { const sourcePath = process.argv[4]; if (!sourcePath || sourcePath.startsWith("--")) { @@ -498,7 +511,7 @@ async function memMerge(): Promise { const db = openMemoryDb(); try { const r = mergeDb(db, sourcePath, remapProject ? { remapProject } : {}); - if (r.messages + r.toolUses + r.pending + r.threads === 0) { + if (mergePayloadChanges(r) === 0) { // `sessions` is inflated by merge even for a fully-redundant source — don't // let it dress up a no-op merge as success. console.log( @@ -509,6 +522,7 @@ async function memMerge(): Promise { `${c.green}✓${c.reset} merged ${c.bold}${r.messages}${c.reset} messages + ${r.toolUses} tool-uses · ${r.sessions} sessions · ${r.pending} pending · ${r.threads} copilots ${c.dim}from ${sourcePath}${c.reset}`, ); } + printTombstoneMergeStats(r); // Scope visibility (#247): "merged" must not read as "reachable". Sessions // keyed to a foreign project (a container's -home-user, another machine's // tree) are invisible to project-scoped search — say where they landed. @@ -553,7 +567,7 @@ async function memSync(): Promise { const db = openMemoryDb(); try { const r = syncFromExport(db, src, { passphrase: pass }); - if (r.messages + r.toolUses + r.pending + r.threads === 0) { + if (mergePayloadChanges(r) === 0) { console.log( `${c.yellow}!${c.reset} nothing new — already in sync with ${c.dim}${src}${c.reset} (${r.sessions} sessions seen)`, ); @@ -565,6 +579,7 @@ async function memSync(): Promise { `${c.dim}last-write-wins on sessions; file_index (this machine's index state) left untouched${c.reset}`, ); } + printTombstoneMergeStats(r); } catch (err) { console.error(`${c.red}${(err as Error).message}${c.reset}`); return false; @@ -1195,7 +1210,7 @@ async function memShare(): Promise { const supersedes = flagValue(process.argv, "--supersedes"); const reverses = flagValue(process.argv, "--reverses"); const status = flagValue(process.argv, "--status") as SharedStatus | undefined; - const provenance = flagValue(process.argv, "--provenance") as SharedProvenance | undefined; + const provenance = (flagValue(process.argv, "--provenance") ?? "operator") as SharedProvenance; const confidence = flagValue(process.argv, "--confidence") as | "low" | "medium" diff --git a/src/flag-surface.ts b/src/flag-surface.ts index 102de51..4a74e3e 100644 --- a/src/flag-surface.ts +++ b/src/flag-surface.ts @@ -279,14 +279,74 @@ export const COMMAND_FLAGS: Record = { env: ["--compare", "--json", "--missing", "--non-interactive", "--show-values"], escalate: ["--broker-gate", "--help", "--limit", "--mode", "--no-install-gate"], fix: [], - "gate-bash": ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--format", "--with"], - "gate-egress": ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--format", "--with"], - "gate-env": ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--format", "--with"], - "gate-fs": ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--format", "--with"], + "gate-bash": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--format", + "--output", + "--output-document", + "--with", + ], + "gate-egress": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--format", + "--output", + "--output-document", + "--with", + ], + "gate-env": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--format", + "--output", + "--output-document", + "--with", + ], + "gate-fs": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--format", + "--output", + "--output-document", + "--with", + ], "gha-audit": ["--json"], governance: ["--broker-gate", "--help", "--limit", "--mode", "--no-install-gate"], - guard: ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--help", "--with"], - "guard-observe": ["--all", "--allow-scripts-pending", "--call", "--dry-run", "--help", "--with"], + guard: [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--help", + "--output", + "--output-document", + "--with", + ], + "guard-observe": [ + "--all", + "--allow-scripts-pending", + "--call", + "--command", + "--dry-run", + "--help", + "--output", + "--output-document", + "--with", + ], heal: ["--agent", "--dry-run", "--help"], health: ["--attest", "--json", "--mode", "--no-auto-install", "--non-interactive"], hooks: ["--force", "--non-interactive"], diff --git a/src/guard.test.ts b/src/guard.test.ts index 8fa697b..14c595d 100644 --- a/src/guard.test.ts +++ b/src/guard.test.ts @@ -30,6 +30,8 @@ import { SHIM_MARKER, RC_BEGIN, RC_END, + guardPathWinners, + gitCloneObservation, } from "./guard.js"; // kit guard v1 (observe): the shim's contract is FAIL-OPEN by construction — @@ -205,6 +207,53 @@ describe("observation log", () => { }); }); +describe("guard PATH effectiveness (#538)", () => { + it("reports when another PATH entry wins before kit's shim", () => { + const root = mkdtempSync(join(tmpdir(), "kit-guard-path-")); + try { + const kitShims = join(root, "kit", "shims"); + const otherBin = join(root, "other", "bin"); + mkdirSync(kitShims, { recursive: true }); + mkdirSync(otherBin, { recursive: true }); + writeShim("npm", kitShims); + writeFileSync(join(otherBin, "npm"), "#!/bin/sh\n", { mode: 0o755 }); + + const displaced = guardPathWinners(kitShims, `${otherBin}:${kitShims}`, ["npm"])[0]; + assert.equal(displaced.installed, true); + assert.equal(displaced.kitWins, false); + assert.equal(displaced.winner, join(otherBin, "npm")); + + const covered = guardPathWinners(kitShims, `${kitShims}:${otherBin}`, ["npm"])[0]; + assert.equal(covered.kitWins, true); + assert.equal(covered.winner, join(kitShims, "npm")); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + +describe("guard git clone observation (#537)", () => { + it("observes clone as repo intake but does not mark it a hard block", () => { + const obs = gitCloneObservation("git", [ + "-c", + "protocol.version=2", + "clone", + "--depth", + "1", + "https://github.com/acme/tool.git", + ]); + assert.ok(obs); + assert.equal(obs.ref, "github:acme/tool"); + assert.match(obs.reason, /observe-only/); + assert.match(obs.reason, /kit triage repo https:\/\/github\.com\/acme\/tool\.git/); + }); + + it("ignores non-clone git commands", () => { + assert.equal(gitCloneObservation("git", ["status"]), null); + assert.equal(gitCloneObservation("npm", ["clone", "https://github.com/acme/tool"]), null); + }); +}); + // #461: the shim hands off to the next `npm` on PATH. When that next entry belongs // to ANOTHER shim manager (mise/asdf/pyenv/rbenv), that manager re-resolves `npm` // through PATH — with kit's shims still first — and the two shims ping-pong @@ -239,7 +288,7 @@ function runShim( // on the hand-off's side effects (marker leakage, PATH surgery), not merely that // something ran. const reporter = (tag: string) => - `#!/bin/sh\necho "${tag} npm $*"\necho "MARKER=\${KIT_GUARD_ACTIVE_NPM:-unset}"\necho "PATH=\$PATH"\n`; + `#!/bin/sh\necho "${tag} npm $*"\necho "MARKER=\${KIT_GUARD_ACTIVE_NPM:-unset}"\necho "PATH=$PATH"\n`; if (!opts.noRealNpm) { const realDir = competing === "owns-tool" ? installBin : sysBin; writeFileSync(join(realDir, "npm"), reporter("REAL"), { mode: 0o755 }); @@ -256,7 +305,7 @@ function runShim( `#!/bin/sh _oi="\${IFS}" IFS=: -for _d in \$PATH; do +for _d in $PATH; do IFS="\${_oi}" [ -n "\${_d}" ] || continue [ "\${_d}" = "${otherShims}" ] && continue @@ -360,7 +409,7 @@ describe("hand-off when another shim manager owns the tool (#461)", () => { describe("coverage roster", () => { it("the fetch-and-run family is on the roster — npx-shaped tools above all", () => { - for (const t of ["npx", "bunx", "pipx", "uvx", "npm", "bun", "brew", "pip"]) { + for (const t of ["npx", "bunx", "pipx", "uvx", "npm", "bun", "brew", "pip", "git"]) { assert.ok(GUARD_TOOLS.includes(t), `${t} missing from GUARD_TOOLS`); } assert.ok(!existsSync("/nonexistent"), "sanity"); diff --git a/src/guard.ts b/src/guard.ts index b01610c..9d3a9a4 100644 --- a/src/guard.ts +++ b/src/guard.ts @@ -25,6 +25,8 @@ import { appendFileSync, renameSync, rmSync, + accessSync, + constants, } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; @@ -45,6 +47,7 @@ export const GUARD_TOOLS: readonly string[] = [ "brew", "gem", "cargo", + "git", ]; export const SHIM_MARKER = "# kit-managed guard shim (do not edit)"; @@ -59,6 +62,125 @@ export function guardLogPath(): string { return process.env.KIT_GUARD_LOG ?? join(homedir(), ".kit", "guard-observe.jsonl"); } +function executable(path: string): boolean { + try { + accessSync(path, constants.X_OK); + return true; + } catch { + return false; + } +} + +function samePath(a: string, b: string): boolean { + return a.replace(/\/+$/, "") === b.replace(/\/+$/, ""); +} + +function isKitShim(path: string): boolean { + try { + return readFileSync(path, "utf-8").includes(SHIM_MARKER); + } catch { + return false; + } +} + +export interface GuardPathWinner { + tool: string; + installed: boolean; + winner: string | null; + kitWins: boolean; +} + +/** Which executable a shell PATH would actually hit for each guarded tool. */ +export function guardPathWinners( + shimsDir: string, + pathEnv: string, + tools: readonly string[] = GUARD_TOOLS, +): GuardPathWinner[] { + const dirs = pathEnv.split(":").filter(Boolean); + return tools.map((tool) => { + const shim = join(shimsDir, tool); + const installed = isKitShim(shim); + let winner: string | null = null; + for (const dir of dirs) { + const candidate = join(dir, tool); + if (executable(candidate)) { + winner = candidate; + break; + } + } + return { tool, installed, winner, kitWins: installed && !!winner && samePath(winner, shim) }; + }); +} + +export interface GitCloneObservation { + target: string; + ref: string; + reason: string; +} + +const GIT_CLONE_VALUE_FLAGS = new Set([ + "-b", + "--branch", + "-o", + "--origin", + "--depth", + "--template", + "--reference", + "--reference-if-able", + "--separate-git-dir", + "--jobs", + "-j", + "--config", + "-c", + "--server-option", +]); + +function githubRefForCloneTarget(target: string): string | null { + let t = target.trim(); + const scp = /^[\w.-]+@github\.com:(.+)$/.exec(t); + if (scp) t = scp[1]; + else { + t = t.replace(/^https?:\/\/(?:www\.)?github\.com\//i, ""); + t = t.replace(/^ssh:\/\/(?:[\w.-]+@)?github\.com\//i, ""); + if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) return null; + t = t.replace(/^github\.com\//i, ""); + } + t = t + .replace(/\.git$/, "") + .replace(/^\/+/, "") + .replace(/\/+$/, ""); + const [owner, repo] = t.split("/").filter(Boolean); + if (!owner || !repo) return null; + return /^[\w.-]+$/.test(owner) && /^[\w.-]+$/.test(repo) ? `github:${owner}/${repo}` : null; +} + +/** Observe `git clone` as a repo intake signal. It is NOT a hard gate in v1. */ +export function gitCloneObservation( + tool: string, + args: readonly string[], +): GitCloneObservation | null { + if (tool !== "git") return null; + const cloneAt = args.indexOf("clone"); + if (cloneAt < 0) return null; + for (let i = cloneAt + 1; i < args.length; i++) { + const arg = args[i]; + if (arg === "--") continue; + if (arg.startsWith("-")) { + if (GIT_CLONE_VALUE_FLAGS.has(arg) || GIT_CLONE_VALUE_FLAGS.has(arg.split("=")[0] ?? "")) { + if (!arg.includes("=")) i++; + } + continue; + } + const ref = githubRefForCloneTarget(arg) ?? `repo:${arg}`; + return { + target: arg, + ref, + reason: `git clone observed (observe-only; kit does not hard-gate clone). Triage third-party repo intake with: kit triage repo ${arg}`, + }; + } + return null; +} + /** Shell rc files the PATH block goes into (existing ones + ~/.zshrc as the macOS default). */ export function guardRcFiles(): string[] { const override = process.env.KIT_GUARD_RC; @@ -116,7 +238,7 @@ _kit_path_minus_self() { _kit_out="" _kit_oifs="\${IFS}" IFS=: - for _p in \$PATH; do + for _p in $PATH; do IFS="\${_kit_oifs}" [ -n "\${_p}" ] || continue [ "\${_p%/}" = "\${_kit_shims%/}" ] && continue @@ -128,7 +250,7 @@ _kit_path_minus_self() { _kit_handoff="" _old_ifs="\${IFS}" IFS=: -for _d in \$PATH; do +for _d in $PATH; do IFS="\${_old_ifs}" [ -n "\${_d}" ] || continue [ "\${_d%/}" = "\${_kit_shims%/}" ] && continue @@ -141,7 +263,7 @@ for _d in \$PATH; do [ -n "\${_kit_handoff}" ] || _kit_handoff="\${_d}" continue fi - PATH="\$(_kit_path_minus_self)" + PATH="$(_kit_path_minus_self)" export PATH export ${active}=1 exec "\${_d}/${tool}" "$@" @@ -153,7 +275,7 @@ done IFS="\${_old_ifs}" if [ -n "\${_kit_handoff}" ]; then # Re-entered, and only shim dirs have ${tool}: run it with our dir off PATH. - PATH="\$(_kit_path_minus_self)" + PATH="$(_kit_path_minus_self)" export PATH exec "\${_kit_handoff}/${tool}" "$@" fi diff --git a/src/install-gate.test.ts b/src/install-gate.test.ts index f2cde82..13c2dc2 100644 --- a/src/install-gate.test.ts +++ b/src/install-gate.test.ts @@ -770,6 +770,42 @@ describe("parseInstallCommand — round-5 bypass closes", () => { }); }); +describe("parseInstallCommand — fetch-to-shell repo installers (#531)", () => { + it("triages a GitHub repo whose installer is piped directly to a shell", () => { + assert.deepEqual( + parseInstallCommand("curl -fsSL https://github.com/acme/tool/install.sh | sh").refs, + ["github:acme/tool"], + ); + assert.deepEqual( + parseInstallCommand("wget -qO- https://github.com/acme/tool/install.sh | bash").refs, + ["github:acme/tool"], + ); + }); + + it("triages a GitHub repo whose downloaded script is executed later in the same command", () => { + assert.deepEqual( + parseInstallCommand( + "curl -fsSL -o /tmp/i.sh https://github.com/acme/tool/install.sh && sh /tmp/i.sh", + ).refs, + ["github:acme/tool"], + ); + assert.deepEqual( + parseInstallCommand( + "wget -O /tmp/i.sh https://github.com/acme/tool/install.sh ; bash /tmp/i.sh", + ).refs, + ["github:acme/tool"], + ); + }); + + it("does not blind-block non-repo vanity installers", () => { + assert.equal(parseInstallCommand("curl -fsSL https://mise.run | sh").isInstall, false); + assert.equal( + parseInstallCommand("curl -fsSL https://example.com/install.sh | sh").isInstall, + false, + ); + }); +}); + // Fake triage: pass everything except names in `blocklist`. function fakeDeps(blocklist: string[] = []): GateDeps { return { @@ -819,6 +855,15 @@ describe("decideBashGate — decision", () => { const v = await decideBashGate("pip install -e .", fakeDeps()); assert.equal(v.block, false); }); + + it("blocks repo fetch-to-shell when repo triage does not pass (#531)", async () => { + const v = await decideBashGate( + "curl -fsSL https://github.com/acme/tool/install.sh | sh", + fakeDeps(["https://github.com/acme/tool"]), + ); + assert.equal(v.block, true); + assert.match(v.reason, /repo https:\/\/github\.com\/acme\/tool/); + }); }); describe("decideBashGate — local node_modules/.bin shadowing (npx tsc case)", () => { diff --git a/src/install-gate.ts b/src/install-gate.ts index 1bd11c4..205610b 100644 --- a/src/install-gate.ts +++ b/src/install-gate.ts @@ -721,6 +721,10 @@ export function parseInstallCommand(command: string): InstallProbe { runnerBinCandidates: [], }; if (!command || typeof command !== "string") return probe; + for (const ref of repoFetchToShellRefs(command)) { + probe.isInstall = true; + probe.refs.push(ref); + } // Scan the command AND any commands hidden in $(…)/backticks/`-c '…'`, bounded // so a wrapper (`sh -c '…'`, `$(…)`) can't smuggle an install past the splitter. @@ -911,6 +915,116 @@ export function ownerRepoArg(tok: string): string | null { return /^[\w.-]+$/.test(owner) && /^[\w.-]+$/.test(repo) ? `${owner}/${repo}` : null; } +const HTTP_URL_RE = /https?:\/\/[^\s'"`<>|;&)]+/gi; +const FETCH_TO_SHELL_PIPE_RE = + /\b(?:curl|wget)\b[^\n|;&]{0,2000}\bhttps?:\/\/[^\s'"`<>|;&)]+[^\n|;&]{0,2000}\|\s*(?:(?:sudo|doas|env|command)\s+(?:\S+=\S+\s+)*)?(?:\S*\/)?(?:sh|bash|zsh|dash|ksh|ash|fish)\b/g; +const SHELL_BINS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "fish"]); + +function githubRepoRefFromUrl(rawUrl: string): string | null { + try { + const u = new URL(rawUrl.replace(/[),.]+$/, "")); + const host = u.hostname.toLowerCase().replace(/^www\./, ""); + const parts = u.pathname.split("/").filter(Boolean); + const owner = + host === "github.com" || host === "raw.githubusercontent.com" ? parts[0] : undefined; + const repo = + host === "github.com" || host === "raw.githubusercontent.com" ? parts[1] : undefined; + if (!owner || !repo) return null; + const cleanRepo = repo.replace(/\.git$/, ""); + if (!/^[\w.-]+$/.test(owner) || !/^[\w.-]+$/.test(cleanRepo)) return null; + return `github:${owner}/${cleanRepo}`; + } catch { + return null; + } +} + +function repoRefsFromUrls(text: string): string[] { + const refs: string[] = []; + for (const m of text.matchAll(HTTP_URL_RE)) { + const ref = githubRepoRefFromUrl(m[0]); + if (ref) refs.push(ref); + } + return refs; +} + +function segmentShellTokens(segment: string): string[] { + const raw = stripRedirections(stripInlineComment(segment).trim().split(/\s+/).filter(Boolean)) + .map(dequote) + .filter(Boolean); + const tokens = stripCommandPrefix(raw); + if (tokens.length === 0) return tokens; + tokens[0] = binBase(tokens[0]); + return tokens; +} + +function fetchDownload(tokens: string[]): { output: string; refs: string[] } | null { + const bin = tokens[0]; + if (bin !== "curl" && bin !== "wget") return null; + let output: string | undefined; + const urls: string[] = []; + for (let i = 1; i < tokens.length; i++) { + const tok = tokens[i]; + if (/^https?:\/\//i.test(tok)) urls.push(tok); + if (bin === "curl") { + if (tok === "-o" || tok === "--output") { + output = tokens[i + 1]; + i++; + continue; + } + const eq = tok.match(/^--output=(.+)$/); + if (eq) output = eq[1]; + const glued = tok.match(/^-o(.+)$/); + if (glued) output = glued[1]; + } else { + if (tok === "-O" || tok === "--output-document") { + output = tokens[i + 1]; + i++; + continue; + } + const eq = tok.match(/^--output-document=(.+)$/); + if (eq) output = eq[1]; + const glued = tok.match(/^-.*O(.+)$/); + if (glued) output = glued[1]; + } + } + if (!output || output === "-") return null; + const refs = repoRefsFromUrls(urls.join(" ")); + return refs.length ? { output, refs } : null; +} + +function shellScriptTarget(tokens: string[]): string | null { + if (!SHELL_BINS.has(tokens[0] ?? "")) return null; + for (let i = 1; i < tokens.length; i++) { + const tok = tokens[i]; + if (tok === "-c" || tok === "--command") return null; + if (tok === "--") continue; + if (tok.startsWith("-")) continue; + return tok; + } + return null; +} + +function directScriptTarget(tokens: string[]): string | null { + const first = tokens[0]; + return first && /^[./~]/.test(first) ? first : null; +} + +function repoFetchToShellRefs(command: string): string[] { + const refs: string[] = []; + for (const m of command.matchAll(FETCH_TO_SHELL_PIPE_RE)) refs.push(...repoRefsFromUrls(m[0])); + + const downloads = new Map(); + for (const segment of command.split(SEGMENT_SPLIT)) { + const tokens = segmentShellTokens(segment); + if (tokens.length === 0) continue; + const fetched = fetchDownload(tokens); + if (fetched) downloads.set(fetched.output, fetched.refs); + const target = shellScriptTarget(tokens) ?? directScriptTarget(tokens); + if (target && downloads.has(target)) refs.push(...downloads.get(target)!); + } + return [...new Set(refs)]; +} + /** * When a RUNNER's fetched package is a known repo-fetcher, find the repo * argument among the args passed THROUGH to it (`skills add mattpocock/skills`) diff --git a/src/memory-share-cli.test.ts b/src/memory-share-cli.test.ts new file mode 100644 index 0000000..c8225fa --- /dev/null +++ b/src/memory-share-cli.test.ts @@ -0,0 +1,87 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const CLI_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "cli.js"); + +describe("kit memory share CLI", () => { + it("writes explicit operator provenance by default (#550)", () => { + const root = mkdtempSync(join(tmpdir(), "kit-share-cli-")); + try { + execFileSync( + process.execPath, + [ + CLI_PATH, + "memory", + "share", + "--area", + "memory", + "--kind", + "decision", + "--title", + "default provenance", + "--body", + "operator promoted this", + ], + { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + KIT_IDENTITY_DIR: join(root, "identity"), + KIT_MEMORY_DB: join(root, "memory.db"), + }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + const raw = readFileSync(join(root, ".kit", "shared", "memory.jsonl"), "utf8").trim(); + const entry = JSON.parse(raw) as { provenance?: string }; + assert.equal(entry.provenance, "operator"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("honors explicit derived provenance (#550)", () => { + const root = mkdtempSync(join(tmpdir(), "kit-share-cli-")); + try { + execFileSync( + process.execPath, + [ + CLI_PATH, + "memory", + "share", + "--area", + "memory", + "--kind", + "decision", + "--title", + "derived provenance", + "--body", + "kit derived this", + "--provenance", + "derived", + ], + { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + KIT_IDENTITY_DIR: join(root, "identity"), + KIT_MEMORY_DB: join(root, "memory.db"), + }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + const raw = readFileSync(join(root, ".kit", "shared", "memory.jsonl"), "utf8").trim(); + const entry = JSON.parse(raw) as { provenance?: string }; + assert.equal(entry.provenance, "derived"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/src/memory/hook.test.ts b/src/memory/hook.test.ts index f71e89c..960041c 100644 --- a/src/memory/hook.test.ts +++ b/src/memory/hook.test.ts @@ -14,6 +14,7 @@ import { runSessionEndIndex, consumeSessionEndLog, logSessionEndEvent, + agingNoticeForPaths, } from "./hook.js"; import { getCurrentProjectRoot } from "./project.js"; import { shareEntry } from "./shared.js"; @@ -239,6 +240,90 @@ describe("memory hook — recentDecisions (shared curated tier)", () => { }); }); +describe("memory hook — shared aging notice", () => { + it("surfaces aged derived/inferred entries only for touched areas (#550)", () => { + const root = mkdtempSync(join(tmpdir(), "kit-aging-hook-")); + try { + mkdirSync(join(root, ".kit", "shared"), { recursive: true }); + writeFileSync( + join(root, ".kit", "shared", "clusters.json"), + JSON.stringify({ memory: ["src/memory/**"], cli: ["src/cli.ts"] }), + ); + shareEntry( + root, + { + area: "memory", + kind: "decision", + title: "derived old rule", + body: "", + provenance: "derived", + }, + "2025-01-01T00:00:00Z", + ); + shareEntry( + root, + { + area: "cli", + kind: "decision", + title: "other old rule", + body: "", + provenance: "inferred", + }, + "2025-01-01T00:00:00Z", + ); + shareEntry( + root, + { + area: "memory", + kind: "decision", + title: "operator-owned old rule", + body: "", + provenance: "operator", + }, + "2025-01-01T00:00:00Z", + ); + + const text = agingNoticeForPaths( + root, + ["src/memory/hook.ts"], + new Date("2026-08-01T00:00:00Z"), + ); + assert.match(text, /1 stale, 0 aging/); + assert.match(text, /memory/); + assert.doesNotMatch(text, /cli/); + assert.match(text, /kit memory area memory --stale/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("stays quiet for legacy/operator entries and untouched areas (#550)", () => { + const root = mkdtempSync(join(tmpdir(), "kit-aging-hook-")); + try { + mkdirSync(join(root, ".kit", "shared"), { recursive: true }); + writeFileSync( + join(root, ".kit", "shared", "clusters.json"), + JSON.stringify({ memory: ["src/memory/**"] }), + ); + shareEntry( + root, + { area: "memory", kind: "decision", title: "legacy old rule", body: "" }, + "2025-01-01T00:00:00Z", + ); + assert.equal( + agingNoticeForPaths(root, ["src/memory/hook.ts"], new Date("2026-08-01T00:00:00Z")), + "", + ); + assert.equal( + agingNoticeForPaths(root, ["src/commands/memory.ts"], new Date("2026-08-01T00:00:00Z")), + "", + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + describe("memory hook — harness sweep debounce", () => { let tmp: string; const prevDir = process.env.KIT_MEMORY_DIR; diff --git a/src/memory/hook.ts b/src/memory/hook.ts index 252be5a..e1f3a04 100644 --- a/src/memory/hook.ts +++ b/src/memory/hook.ts @@ -17,12 +17,14 @@ import { indexCodexSessions } from "./codex.js"; import { palList } from "./pal.js"; import { activeShared, + agingReport, recallSafeShared, + readShared, provenanceRank, formatAge, type SharedEntry, } from "./shared.js"; -import { decisionsForPaths, changedPaths } from "./clusters.js"; +import { clustersForPaths, decisionsForPaths, changedPaths, readClusters } from "./clusters.js"; import { getCurrentProjectRoot } from "./project.js"; import { readCachedUpdateSync, getKitVersionSync } from "../update-check.js"; import { sanitizeForPrompt } from "./injection.js"; @@ -76,12 +78,14 @@ export function userPromptSubmitReminder(): string { // that has active decisions, surface them — touch area X ⇒ see X's decisions, // not a query lottery. Bounded + fail-open (no clusters.json ⇒ nothing). const push = touchedDecisionsNotice(); + const aging = touchedAgingNotice(); return ( (stale ? `${stale}\n` : "") + `You have local conversation memory: ${s.messages} messages indexed. ` + "Before answering anything project-specific, run `kit memory search ` " + `to retrieve what was actually said instead of reconstructing it.${pending}` + - (push ? `\n${push}` : "") + (push ? `\n${push}` : "") + + (aging ? `\n${aging}` : "") ); } catch { return ""; // fail-open: never block a prompt @@ -111,6 +115,45 @@ function touchedDecisionsNotice(root: string = getCurrentProjectRoot()): string } } +/** + * One-line review notice for aged machine-origin shared entries in the area(s) + * touched by the supplied paths. Operator/legacy entries never age; stale/aging + * classification is a review prompt only, never an auto-delete. Bounded and + * deterministic so it is safe for every-prompt hooks. + */ +export function agingNoticeForPaths(root: string, paths: string[], now: Date = new Date()): string { + try { + const areas = new Set(clustersForPaths(readClusters(root), paths)); + if (areas.size === 0) return ""; + const report = agingReport(readShared(root), now); + const stale = report.stale.filter((e) => areas.has(e.area)); + const aging = report.aging.filter((e) => areas.has(e.area)); + if (stale.length + aging.length === 0) return ""; + const affected = [...new Set([...stale, ...aging].map((e) => e.area))].sort(); + const shown = affected.slice(0, 2).join(", "); + const more = affected.length > 2 ? " …" : ""; + const firstArea = affected[0] ?? "area"; + return ( + `Shared memory aging for touched area(s): ${stale.length} stale, ${aging.length} aging ` + + `derived/inferred entr${stale.length + aging.length === 1 ? "y" : "ies"} in ${shown}${more}. ` + + `Review with \`kit memory area ${firstArea} --stale\` before relying on them.` + ); + } catch { + return ""; + } +} + +function touchedAgingNotice( + root: string = getCurrentProjectRoot(), + now: Date = new Date(), +): string { + try { + return agingNoticeForPaths(root, changedPaths(root), now); + } catch { + return ""; + } +} + /** * The most recent ACTIVE durable shared decisions for a project, newest first. * "Durable" = the curated kinds worth re-surfacing on resume (decision / diff --git a/src/memory/merge.test.ts b/src/memory/merge.test.ts index b9fbec8..f9f7ff5 100644 --- a/src/memory/merge.test.ts +++ b/src/memory/merge.test.ts @@ -3,10 +3,22 @@ import assert from "node:assert/strict"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { openMemoryDb, upsertSession, insertMessage, getStats } from "./db.js"; +import { + openMemoryDb, + upsertSession, + insertMessage, + getStats, + forgetMemory, + countTombstones, +} from "./db.js"; import { palAdd } from "./pal.js"; import { mergeDb } from "./merge.js"; +function messageCount(db: ReturnType, uuid: string): number { + return (db.prepare("SELECT COUNT(*) c FROM messages WHERE uuid = ?").get(uuid) as { c: number }) + .c; +} + describe("memory merge", () => { it("merges another store deduped by uuid; re-merge is a no-op", () => { const tmp = mkdtempSync(join(tmpdir(), "kit-merge-")); @@ -50,6 +62,70 @@ describe("memory merge", () => { target.close(); }); + it("does not resurrect incoming messages that the target has tombstoned (#549)", () => { + const tmp = mkdtempSync(join(tmpdir(), "kit-merge-")); + const srcPath = join(tmp, "source.db"); + + const src = openMemoryDb(srcPath); + upsertSession(src, { sessionId: "s1", harness: "codex" }); + insertMessage(src, { uuid: "gone", sessionId: "s1", type: "user", content: "forgotten" }); + insertMessage(src, { uuid: "keep", sessionId: "s1", type: "user", content: "keep me" }); + src.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + src.close(); + + const target = openMemoryDb(":memory:"); + upsertSession(target, { sessionId: "s1", harness: "codex" }); + insertMessage(target, { uuid: "gone", sessionId: "s1", type: "user", content: "forgotten" }); + assert.equal(forgetMemory(target, "gone", "test").ok, true); + + const r = mergeDb(target, srcPath); + assert.equal(r.messages, 1); + assert.equal(r.tombstoneBlockedMessages, 1); + assert.equal(messageCount(target, "gone"), 0); + assert.equal(messageCount(target, "keep"), 1); + assert.equal(countTombstones(target), 1); + + target.close(); + rmSync(tmp, { recursive: true, force: true }); + }); + + it("propagates source tombstones and deletes stale target messages (#549)", () => { + const tmp = mkdtempSync(join(tmpdir(), "kit-merge-")); + const forgottenPath = join(tmp, "forgotten.db"); + const stalePath = join(tmp, "stale.db"); + + const forgotten = openMemoryDb(forgottenPath); + upsertSession(forgotten, { sessionId: "s1", harness: "codex" }); + insertMessage(forgotten, { uuid: "gone", sessionId: "s1", type: "user", content: "erase" }); + assert.equal(forgetMemory(forgotten, "gone", "sync delete").ok, true); + forgotten.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + forgotten.close(); + + const stale = openMemoryDb(stalePath); + upsertSession(stale, { sessionId: "s1", harness: "codex" }); + insertMessage(stale, { uuid: "gone", sessionId: "s1", type: "user", content: "erase" }); + stale.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + stale.close(); + + const target = openMemoryDb(":memory:"); + upsertSession(target, { sessionId: "s1", harness: "codex" }); + insertMessage(target, { uuid: "gone", sessionId: "s1", type: "user", content: "erase" }); + + const r1 = mergeDb(target, forgottenPath); + assert.equal(r1.tombstones, 1); + assert.equal(r1.tombstoneDeletedMessages, 1); + assert.equal(messageCount(target, "gone"), 0); + assert.equal(countTombstones(target), 1); + + const r2 = mergeDb(target, stalePath); + assert.equal(r2.messages, 0); + assert.equal(r2.tombstoneBlockedMessages, 1); + assert.equal(messageCount(target, "gone"), 0); + + target.close(); + rmSync(tmp, { recursive: true, force: true }); + }); + it("reports imported project keys so a foreign scope is loud (#247)", () => { const tmp = mkdtempSync(join(tmpdir(), "kit-merge-")); const srcPath = join(tmp, "container.db"); diff --git a/src/memory/merge.ts b/src/memory/merge.ts index fc872a9..92d94a5 100644 --- a/src/memory/merge.ts +++ b/src/memory/merge.ts @@ -15,6 +15,9 @@ export interface MergeResult { sessions: number; messages: number; toolUses: number; + tombstones: number; + tombstoneDeletedMessages: number; + tombstoneBlockedMessages: number; pending: number; threads: number; /** @@ -44,6 +47,58 @@ export function projectKeyFor(projectRoot: string): string { return projectRoot.replace(/\//g, "-"); } +function tableExists(db: DatabaseSync, name: string): boolean { + return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name); +} + +function mergeTombstones(target: DatabaseSync, src: DatabaseSync, out: MergeResult): void { + if (!tableExists(src, "memory_tombstones")) return; + const upsert = target.prepare( + `INSERT INTO memory_tombstones (uuid, content_sha256, session_id, reason, deleted_at) + VALUES (?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP)) + ON CONFLICT(uuid) DO UPDATE SET + content_sha256 = excluded.content_sha256, + session_id = COALESCE(excluded.session_id, memory_tombstones.session_id), + reason = COALESCE(excluded.reason, memory_tombstones.reason), + deleted_at = CASE + WHEN excluded.deleted_at > memory_tombstones.deleted_at THEN excluded.deleted_at + ELSE memory_tombstones.deleted_at + END + WHERE + memory_tombstones.content_sha256 IS NOT excluded.content_sha256 OR + memory_tombstones.session_id IS NOT excluded.session_id OR + memory_tombstones.reason IS NOT excluded.reason OR + memory_tombstones.deleted_at IS NOT excluded.deleted_at`, + ); + const existingMessage = target.prepare("SELECT session_id FROM messages WHERE uuid = ?"); + const deleteMessage = target.prepare("DELETE FROM messages WHERE uuid = ?"); + const deleteToolUses = target.prepare("DELETE FROM tool_uses WHERE message_uuid = ?"); + const decSession = target.prepare( + "UPDATE sessions SET message_count = MAX(message_count - 1, 0) WHERE session_id = ?", + ); + + for (const t of src.prepare("SELECT * FROM memory_tombstones").all() as Row[]) { + const uuid = str(t.uuid); + const contentSha = str(t.content_sha256); + if (!uuid || !contentSha) continue; + const existing = existingMessage.get(uuid) as { session_id: string } | undefined; + if (existing) { + deleteMessage.run(uuid); + deleteToolUses.run(uuid); + decSession.run(existing.session_id); + out.tombstoneDeletedMessages++; + } + const r = upsert.run( + uuid, + contentSha, + (str(t.session_id) ?? null) as string | null, + (str(t.reason) ?? null) as string | null, + (str(t.deleted_at) ?? null) as string | null, + ); + if (Number(r.changes) > 0) out.tombstones++; + } +} + export function mergeDb( target: DatabaseSync, sourcePath: string, @@ -55,6 +110,9 @@ export function mergeDb( sessions: 0, messages: 0, toolUses: 0, + tombstones: 0, + tombstoneDeletedMessages: 0, + tombstoneBlockedMessages: 0, pending: 0, threads: 0, projects: {}, @@ -62,6 +120,10 @@ export function mergeDb( const remapKey = opts.remapProject ? projectKeyFor(opts.remapProject) : undefined; try { + // Tombstones must land before messages: a deletion record wins over stale + // rows from another machine and blocks future resurrection by uuid (#549). + mergeTombstones(target, src, out); + // Sessions for (const s of src.prepare("SELECT * FROM sessions").all() as Row[]) { const sessionId = str(s.session_id); @@ -94,6 +156,10 @@ export function mergeDb( const sessionId = str(m.session_id); const type = str(m.type); if (!uuid || !sessionId || !type) continue; // need a stable id to dedupe + if (target.prepare("SELECT 1 FROM memory_tombstones WHERE uuid = ?").get(uuid)) { + out.tombstoneBlockedMessages++; + continue; + } const added = insertMessage(target, { uuid, sessionId, diff --git a/src/opencli.test.ts b/src/opencli.test.ts index e47e70b..df64d78 100644 --- a/src/opencli.test.ts +++ b/src/opencli.test.ts @@ -4,6 +4,8 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { buildOpenCliDoc, serializeOpenCli, OPENCLI_VERSION, type OpenCliDoc } from "./opencli.js"; +import { COMMAND_FLAGS } from "./flag-surface.js"; +import { GLOBAL_FLAGS } from "./utils/flags.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); // dist/opencli.test.js -> repo root is one level up. @@ -55,17 +57,39 @@ describe("OpenCLI document shape", () => { assert.equal(airgap.commands.verify.kind, "command"); }); - it("never fabricates args/flags — every node marks args unmodeled", () => { - const nodes: { "x-kit-args-modeled": boolean }[] = []; + it("publishes accepted flag names from the generated flag surface", () => { + const check = doc.commands.check; + assert.equal(check?.["x-kit-args-modeled"], true); + assert.deepEqual( + check?.["x-kit-accepted-flags"], + [...new Set([...COMMAND_FLAGS.check, ...GLOBAL_FLAGS])].sort(), + ); + assert.ok(check?.["x-kit-accepted-flags"]?.includes("--category")); + assert.ok(check?.["x-kit-accepted-flags"]?.includes("--read-only")); + }); + + it("marks every tabled command and subcommand as modeled", () => { + for (const [name, c] of Object.entries(doc.commands)) { + assert.equal(c["x-kit-args-modeled"], true, `${name} must expose accepted flag names`); + assert.ok(Array.isArray(c["x-kit-accepted-flags"]), `${name} must carry a flag list`); + for (const [sub, s] of Object.entries(c.commands ?? {})) { + assert.equal(s["x-kit-args-modeled"], true, `${name} ${sub} must inherit parent flags`); + assert.deepEqual(s["x-kit-accepted-flags"], c["x-kit-accepted-flags"]); + } + } + }); + + it("does not fabricate OpenCLI arg/flag type metadata", () => { + const nodes: object[] = []; for (const c of Object.values(doc.commands)) { nodes.push(c); for (const s of Object.values(c.commands ?? {})) nodes.push(s); } assert.ok(nodes.length > 0); - assert.ok( - nodes.every((n) => n["x-kit-args-modeled"] === false), - "until the registry models args/flags, all nodes must declare them unmodeled", - ); + for (const n of nodes) { + assert.equal("args" in n, false); + assert.equal("flags" in n, false); + } }); it("only x-kit-* extension keys are used alongside spec fields (honest namespacing)", () => { @@ -77,6 +101,7 @@ describe("OpenCLI document shape", () => { "x-kit-mcp", "x-kit-audience", "x-kit-args-modeled", + "x-kit-accepted-flags", ]); const walk = (c: OpenCliDoc["commands"][string]) => { for (const k of Object.keys(c)) assert.ok(allowed.has(k), `unexpected command key: ${k}`); diff --git a/src/opencli.ts b/src/opencli.ts index 912ceed..87a6de1 100644 --- a/src/opencli.ts +++ b/src/opencli.ts @@ -10,11 +10,13 @@ * test failure), exactly as public-surface does. * * HONEST-BY-CONSTRUCTION: kit's registry models command NAMES, summaries, stability - * tiers, and MCP exposure — not yet per-flag arg/type metadata. Rather than invent - * `args`/`flags` we don't have (that would be a false-green artifact), each command - * carries `x-kit-args-modeled: false` and omits args/flags. When the registry grows - * structured arg metadata, populate them and flip the flag. We emit JSON (OpenCLI - * accepts YAML or JSON) to stay dependency-free and byte-deterministic. + * tiers, MCP exposure, and accepted long-flag names. It still does not model positional + * args, flag value types, arity, choices, or aliases, so the standard OpenCLI `args` / + * `flags` arrays remain omitted rather than fabricated. The kit-specific + * `x-kit-accepted-flags` extension is the dispatch allowlist from flag-surface.ts plus + * global flags, which is enough for drift/audit tools to answer "does kit accept this + * documented flag on this command?". We emit JSON (OpenCLI accepts YAML or JSON) to stay + * dependency-free and byte-deterministic. */ import { readFileSync } from "node:fs"; @@ -28,7 +30,9 @@ import { type CommandTier, type CommandAudience, } from "./cli.js"; +import { COMMAND_FLAGS } from "./flag-surface.js"; import { KIT_MCP_TOOLS } from "./mcp-server.js"; +import { GLOBAL_FLAGS } from "./utils/flags.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -50,11 +54,17 @@ export interface OpenCliCommand { */ "x-kit-audience": CommandAudience; /** - * False = kit's registry does not yet model this command's positional args / - * flags, so they are intentionally omitted rather than fabricated. Consumers - * must NOT read "no args/flags" from their absence. + * True when accepted long-flag names are modeled for this command via + * `x-kit-accepted-flags`. Positional args and flag type/arity metadata are still + * intentionally omitted from the standard OpenCLI fields. */ - "x-kit-args-modeled": false; + "x-kit-args-modeled": boolean; + /** + * Dispatch-level long flags accepted by this command, including kit globals. Kept + * namespaced because OpenCLI's standard `flags` objects require type/arity metadata + * kit does not yet model. + */ + "x-kit-accepted-flags"?: string[]; /** Nested subcommands, present only on `kind: "group"`. */ commands?: Record; } @@ -82,14 +92,24 @@ function readKitVersion(): string { export function buildOpenCliDoc(): OpenCliDoc { const mcp = new Set(KIT_MCP_TOOLS.map((t) => t.replace(/^kit_/, ""))); - const node = (name: string, kind: OpenCliCommand["kind"]): OpenCliCommand => ({ - kind, - summary: COMMAND_HELP[name] ?? "", - "x-kit-stability": COMMAND_TIERS[name] ?? "experimental", - "x-kit-mcp": mcp.has(name), - "x-kit-audience": COMMAND_AUDIENCE[name] ?? "all", - "x-kit-args-modeled": false, - }); + const acceptedFlags = (verb: string): string[] | null => { + const own = COMMAND_FLAGS[verb]; + if (!own) return null; + return [...new Set([...own, ...GLOBAL_FLAGS])].sort(); + }; + + const node = (name: string, kind: OpenCliCommand["kind"], flagVerb = name): OpenCliCommand => { + const flags = acceptedFlags(flagVerb); + return { + kind, + summary: COMMAND_HELP[name] ?? "", + "x-kit-stability": COMMAND_TIERS[name] ?? "experimental", + "x-kit-mcp": mcp.has(name), + "x-kit-audience": COMMAND_AUDIENCE[name] ?? "all", + "x-kit-args-modeled": flags !== null, + ...(flags !== null ? { "x-kit-accepted-flags": flags } : {}), + }; + }; const commands: Record = {}; for (const name of Object.keys(COMMANDS)) commands[name] = node(name, "command"); @@ -103,14 +123,11 @@ export function buildOpenCliDoc(): OpenCliDoc { const parentNode = commands[parent]; if (!parentNode) continue; // subcommand of an unknown/aliased verb — skip, never guess parentNode.kind = "group"; - (parentNode.commands ??= {})[sub] = { - kind: "command", - summary: COMMAND_HELP[key] ?? "", - "x-kit-stability": parentNode["x-kit-stability"], - "x-kit-mcp": false, - "x-kit-audience": parentNode["x-kit-audience"], - "x-kit-args-modeled": false, - }; + const child = node(key, "command", parent); + child["x-kit-stability"] = parentNode["x-kit-stability"]; + child["x-kit-audience"] = parentNode["x-kit-audience"]; + child["x-kit-mcp"] = false; + (parentNode.commands ??= {})[sub] = child; } return { diff --git a/src/self-audit-ci.test.ts b/src/self-audit-ci.test.ts index 6919194..913f40c 100644 --- a/src/self-audit-ci.test.ts +++ b/src/self-audit-ci.test.ts @@ -4,7 +4,13 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import { extractScriptRefs, resolveNpmScript, runCiScriptAudit } from "./self-audit-ci.js"; +import { + OPERATOR_RUN_MARKER, + extractScriptRefs, + resolveNpmScript, + runCiScriptAudit, + unreferencedScripts, +} from "./self-audit-ci.js"; // Repo root: this compiled test lives at dist/self-audit-ci.test.js, so the repo // root is one directory up from dist/. @@ -87,6 +93,7 @@ describe("runCiScriptAudit", () => { assert.equal(results.length, 1); assert.equal(results[0].status, "pass"); assert.match(results[0].detail, /refs across \d+ workflows all resolve/); + assert.match(results[0].detail, /scripts referenced or declared operator-run/); }); it("reports exactly one fail for a missing referenced script", () => { @@ -133,6 +140,87 @@ describe("runCiScriptAudit", () => { } }); + it("reports an unreferenced script as an advisory, not a failure", () => { + const dir = mkdtempSync(join(tmpdir(), "kit-ci-audit-inverse-")); + try { + mkdirSync(join(dir, ".github", "workflows"), { recursive: true }); + mkdirSync(join(dir, "scripts"), { recursive: true }); + writeFileSync( + join(dir, ".github", "workflows", "ci.yml"), + "jobs:\n x:\n steps:\n - run: npm run check\n", + ); + writeFileSync( + join(dir, "package.json"), + JSON.stringify({ name: "tmp", scripts: { check: "node scripts/live.mjs" } }), + ); + writeFileSync(join(dir, "scripts", "live.mjs"), "console.log('live');\n"); + writeFileSync(join(dir, "scripts", "dead.mjs"), "console.log('dead');\n"); + + const results = runCiScriptAudit(dir); + const inverse = results.find((r) => r.name === "unreferenced script"); + assert.equal(inverse?.status, "warn"); + assert.equal(inverse?.severity, "low"); + assert.match(inverse!.detail, /scripts\/dead\.mjs/); + assert.deepEqual(inverse?.files, ["scripts/dead.mjs"]); + assert.equal( + results.some((r) => r.status === "fail"), + false, + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("does not warn for an unreferenced operator-run script with the marker", () => { + const dir = mkdtempSync(join(tmpdir(), "kit-ci-audit-operator-")); + try { + mkdirSync(join(dir, ".github", "workflows"), { recursive: true }); + mkdirSync(join(dir, "scripts"), { recursive: true }); + writeFileSync( + join(dir, ".github", "workflows", "ci.yml"), + "jobs:\n x:\n steps:\n - run: npm run check\n", + ); + writeFileSync(join(dir, "package.json"), JSON.stringify({ scripts: { check: "echo ok" } })); + writeFileSync( + join(dir, "scripts", "manual.sh"), + `#!/usr/bin/env bash\n# ${OPERATOR_RUN_MARKER}\n`, + ); + + const results = runCiScriptAudit(dir); + assert.deepEqual( + results.filter((r) => r.name === "unreferenced script"), + [], + ); + assert.equal(results.length, 1); + assert.match(results[0].detail, /1 scripts referenced or declared operator-run/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("unreferencedScripts treats docs/source refs as live and skips self-refs", () => { + const dir = mkdtempSync(join(tmpdir(), "kit-ci-audit-scan-")); + try { + mkdirSync(join(dir, "scripts"), { recursive: true }); + mkdirSync(join(dir, "docs"), { recursive: true }); + mkdirSync(join(dir, "src"), { recursive: true }); + writeFileSync(join(dir, "scripts", "doc-live.mjs"), "// Usage: scripts/doc-live.mjs\n"); + writeFileSync(join(dir, "scripts", "src-live.mjs"), "console.log('src-live');\n"); + writeFileSync(join(dir, "scripts", "lonely.mjs"), "console.log('lonely');\n"); + writeFileSync(join(dir, "docs", "RUN.md"), "Run `node scripts/doc-live.mjs`.\n"); + writeFileSync(join(dir, "src", "caller.ts"), 'const script = "src-live.mjs";\n'); + + const report = unreferencedScripts(dir); + assert.deepEqual( + report.unreferenced.map((s) => s.path), + ["scripts/lonely.mjs"], + ); + assert.equal(report.total, 3); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("skips when there are no workflows", () => { const dir = mkdtempSync(join(tmpdir(), "kit-ci-audit-empty-")); try { diff --git a/src/self-audit-ci.ts b/src/self-audit-ci.ts index df2a5bb..eebfd56 100644 --- a/src/self-audit-ci.ts +++ b/src/self-audit-ci.ts @@ -7,8 +7,9 @@ // until that workflow runs. This analyzer resolves every such reference statically: // file refs must exist on disk; npm-run refs must be defined in package.json. // -// Pure + deterministic: no network, no LLM. extractScriptRefs/resolveNpmScript are -// the testable units; runCiScriptAudit is the filesystem-bound orchestrator. +// Pure + deterministic: no network, no LLM. extractScriptRefs/resolveNpmScript and +// unreferencedScripts are the testable units; runCiScriptAudit is the filesystem-bound +// orchestrator. import { readFileSync, readdirSync, existsSync } from "node:fs"; import { join } from "node:path"; @@ -16,6 +17,7 @@ import type { SecurityCheckResult } from "./check-security.js"; /** Category used for every result this analyzer emits. */ const CI_CATEGORY: SecurityCheckResult["category"] = "self-audit/ci-script-paths"; +export const OPERATOR_RUN_MARKER = "kit-self-audit: operator-run"; export interface ScriptRef { kind: "node" | "python" | "npm"; @@ -24,6 +26,18 @@ export interface ScriptRef { file: string; } +export interface ScriptInventoryItem { + path: string; + operatorRun: boolean; +} + +export interface ScriptInventoryReport { + total: number; + referenced: number; + operatorRun: ScriptInventoryItem[]; + unreferenced: ScriptInventoryItem[]; +} + // Matchers applied to each line of a workflow's `run:` bodies. We don't parse YAML — // `run:` blocks are free-form shell, so a line scan is both simpler and more robust // than trying to model the step structure. `${{ ... }}` interpolation (matrix/env) @@ -98,6 +112,104 @@ function listWorkflows(repoRoot: string): string[] { .map((e) => join(dir, e.name)); } +function listScriptFiles(repoRoot: string): string[] { + const dir = join(repoRoot, "scripts"); + if (!existsSync(dir)) return []; + return readdirSync(dir, { withFileTypes: true }) + .filter((e) => e.isFile()) + .map((e) => `scripts/${e.name}`) + .sort(); +} + +const REFERENCE_TEXT_EXTENSIONS = [".yml", ".yaml", ".json", ".ts", ".mjs", ".js", ".md", ".sh"]; +const REFERENCE_SKIP_DIRS = new Set([".git", "dist", "node_modules"]); + +function listReferenceFiles(repoRoot: string): string[] { + const out: string[] = []; + function visit(absDir: string, relDir: string): void { + let entries; + try { + entries = readdirSync(absDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (entry.isDirectory()) { + if (REFERENCE_SKIP_DIRS.has(entry.name)) continue; + const childRel = relDir ? `${relDir}/${entry.name}` : entry.name; + visit(join(absDir, entry.name), childRel); + continue; + } + if (!entry.isFile()) continue; + const rel = relDir ? `${relDir}/${entry.name}` : entry.name; + if (REFERENCE_TEXT_EXTENSIONS.some((ext) => rel.endsWith(ext))) out.push(rel); + } + } + visit(repoRoot, ""); + return out.sort(); +} + +function scriptHasOperatorRunMarker(repoRoot: string, relPath: string): boolean { + try { + return readFileSync(join(repoRoot, relPath), "utf8").includes(OPERATOR_RUN_MARKER); + } catch { + return false; + } +} + +function textReferencesScript(text: string, relPath: string): boolean { + const base = relPath.slice(relPath.lastIndexOf("/") + 1); + return text.includes(relPath) || text.includes(`./${relPath}`) || text.includes(base); +} + +/** + * Inverse script-path report: workflow->script proves referenced files exist; this + * proves scripts/ has no orphaned helpers hiding outside workflows/package scripts/docs/source. + * + * Operator-run scripts are deliberate manual tools. They stay unreferenced by design and + * opt out with `# kit-self-audit: operator-run` in their header. + */ +export function unreferencedScripts(repoRoot: string): ScriptInventoryReport { + const scripts = listScriptFiles(repoRoot); + const referenceFiles = listReferenceFiles(repoRoot); + const operatorRun: ScriptInventoryItem[] = []; + const unreferenced: ScriptInventoryItem[] = []; + let referenced = 0; + + for (const script of scripts) { + let live = false; + for (const file of referenceFiles) { + if (file === script) continue; + let text; + try { + text = readFileSync(join(repoRoot, file), "utf8"); + } catch { + continue; + } + if (textReferencesScript(text, script)) { + live = true; + break; + } + } + + const item = { path: script, operatorRun: scriptHasOperatorRunMarker(repoRoot, script) }; + if (live) { + referenced++; + } else if (item.operatorRun) { + operatorRun.push(item); + } else { + unreferenced.push(item); + } + } + + return { + total: scripts.length, + referenced, + operatorRun, + unreferenced, + }; +} + /** * Audit every script reference across the repo's GitHub Actions workflows. * @@ -169,13 +281,28 @@ export function runCiScriptAudit(repoRoot: string): SecurityCheckResult[] { } } + const inverse = unreferencedScripts(repoRoot); + for (const script of inverse.unreferenced) { + results.push({ + category: CI_CATEGORY, + name: "unreferenced script", + status: "warn", + severity: "low", + detail: `${script.path} is not referenced by workflows, package.json scripts, docs, or source`, + files: [script.path], + suggestion: `Reference it, delete it, or add '# ${OPERATOR_RUN_MARKER}' if it is a deliberate operator-run tool.`, + }); + } + if (results.length === 0) { return [ { category: CI_CATEGORY, name: "CI script paths", status: "pass", - detail: `${totalRefs} refs across ${workflows.length} workflows all resolve`, + detail: + `${totalRefs} refs across ${workflows.length} workflows all resolve; ` + + `${inverse.referenced + inverse.operatorRun.length}/${inverse.total} scripts referenced or declared operator-run`, }, ]; } diff --git a/src/self-audit-docs.test.ts b/src/self-audit-docs.test.ts index e174b1e..c33d3f4 100644 --- a/src/self-audit-docs.test.ts +++ b/src/self-audit-docs.test.ts @@ -7,6 +7,7 @@ import { join } from "node:path"; import { extractDocCommandRefs, extractDocFlagRefs, + loadContractFlagSurface, extractDocTomlSections, loadSourceFlagTokens, flagValidationCoverage, @@ -153,7 +154,14 @@ function makeRepo(files: Record): string { const CONTRACT = JSON.stringify({ opencliVersion: "0.1", - commands: { check: { kind: "command" }, fix: { kind: "command" } }, + commands: { + check: { + kind: "command", + "x-kit-args-modeled": true, + "x-kit-accepted-flags": ["--category", "--json", "--read-only", "--strict"], + }, + fix: { kind: "command", "x-kit-args-modeled": true, "x-kit-accepted-flags": [] }, + }, }); describe("self-audit-docs — runDocsClaimsAudit", () => { @@ -299,8 +307,8 @@ describe("self-audit-docs — extractDocFlagRefs (pure)", () => { it("finds flags on a kit invocation", () => { const md = ["```bash", "kit check --json --strict", "```"].join("\n"); assert.deepEqual( - extractDocFlagRefs(md, "d.md").map((r) => r.verb), - ["--json", "--strict"], + extractDocFlagRefs(md, "d.md").map((r) => `${r.command} ${r.flag}`), + ["check --json", "check --strict"], ); }); @@ -312,17 +320,60 @@ describe("self-audit-docs — extractDocFlagRefs (pure)", () => { it("normalises --flag=value to --flag", () => { assert.deepEqual( - extractDocFlagRefs("`kit check --category=security`", "d.md").map((r) => r.verb), - ["--category"], + extractDocFlagRefs("`kit check --category=security`", "d.md").map( + (r) => `${r.command} ${r.flag}`, + ), + ["check --category"], ); }); it("reports the line the flag appears on", () => { const md = ["# T", "", "```", "kit doctor --save-baseline", "```"].join("\n"); assert.deepEqual(extractDocFlagRefs(md, "p.md"), [ - { verb: "--save-baseline", line: 4, file: "p.md" }, + { command: "doctor", flag: "--save-baseline", line: 4, file: "p.md" }, ]); }); + + it("attributes leading global flags to the command they modify", () => { + assert.deepEqual(extractDocFlagRefs("`kit --read-only check`", "d.md"), [ + { command: "check", flag: "--read-only", line: 1, file: "d.md" }, + ]); + }); + + it("ignores pass-through flags after --", () => { + assert.deepEqual(extractDocFlagRefs("`kit run -- pnpm test --watch`", "d.md"), []); + }); +}); + +describe("self-audit-docs — loadContractFlagSurface", () => { + it("loads modeled accepted flag names from the OpenCLI contract", () => { + const root = makeRepo({ "contracts/kit.opencli.json": CONTRACT }); + try { + const surface = loadContractFlagSurface(root); + assert.deepEqual([...(surface?.get("check") ?? [])].sort(), [ + "--category", + "--json", + "--read-only", + "--strict", + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("returns null when every command still declares args unmodeled", () => { + const root = makeRepo({ + "contracts/kit.opencli.json": JSON.stringify({ + opencliVersion: "0.1", + commands: { check: { kind: "command", "x-kit-args-modeled": false } }, + }), + }); + try { + assert.equal(loadContractFlagSurface(root), null); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); }); describe("self-audit-docs — loadSourceFlagTokens", () => { @@ -400,13 +451,54 @@ describe("self-audit-docs — runDocsClaimsAudit over the other two classes", () it("fails on a fabricated flag and names it", () => { const root = makeRepo({ "contracts/kit.opencli.json": CONTRACT, - "src/a.ts": 'flagValue(args, "--json");', "docs/P.md": ["```bash", "kit check --max-parallel=4", "```"].join("\n"), }); try { const flags = runDocsClaimsAudit(root).find((r) => r.name === "documented flags"); assert.equal(flags?.status, "fail"); - assert.match(flags!.detail, /--max-parallel \(docs\/P\.md:2\)/); + assert.match(flags!.detail, /kit check --max-parallel \(docs\/P\.md:2\)/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("fails when a real flag is documented on the wrong command", () => { + const root = makeRepo({ + "contracts/kit.opencli.json": JSON.stringify({ + opencliVersion: "0.1", + commands: { + bootstrap: { + kind: "command", + "x-kit-args-modeled": true, + "x-kit-accepted-flags": ["--profile"], + }, + check: { kind: "command", "x-kit-args-modeled": true, "x-kit-accepted-flags": [] }, + }, + }), + "docs/P.md": ["```bash", "kit check --profile", "```"].join("\n"), + }); + try { + const flags = runDocsClaimsAudit(root).find((r) => r.name === "documented flags"); + assert.equal(flags?.status, "fail"); + assert.match(flags!.detail, /kit check --profile \(docs\/P\.md:2\)/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("reports didNotRun when accepted flag names are not modeled", () => { + const root = makeRepo({ + "contracts/kit.opencli.json": JSON.stringify({ + opencliVersion: "0.1", + commands: { check: { kind: "command", "x-kit-args-modeled": false } }, + }), + "docs/P.md": "`kit check --json`", + }); + try { + const flags = runDocsClaimsAudit(root).find((r) => r.name === "documented flags"); + assert.equal(flags?.status, "warn"); + assert.equal(flags?.didNotRun, true); + assert.match(flags!.detail, /accepted flag names NOT verified/); } finally { rmSync(root, { recursive: true, force: true }); } diff --git a/src/self-audit-docs.ts b/src/self-audit-docs.ts index d46a3e1..d43dc10 100644 --- a/src/self-audit-docs.ts +++ b/src/self-audit-docs.ts @@ -25,9 +25,9 @@ // Three claim classes are checked, each against a sound oracle: // // commands → contracts/kit.opencli.json (the committed command contract) -// flags → every `--flag` literal appearing in src/**.ts. kit parses argv by -// naming each flag literally (`flagValue(args, "--x")`), so a flag the -// implementation never names cannot possibly be read. +// flags → per-command `x-kit-accepted-flags` in contracts/kit.opencli.json. +// That extension is generated from src/flag-surface.ts, the same table +// dispatch uses to reject unknown flags. // sections → config.ts KNOWN_SECTIONS, the explicit set kit validates .kit.toml // against. // @@ -38,13 +38,9 @@ // which is correct usage). A gate that cries wolf is worse than no gate, so key // checking is left out rather than shipped unsound. // -// Known scope limit that remains: the flag check is repo-global, not per-command. A -// real flag documented on the wrong command still passes — `kit check --profile` used -// to run and silently do nothing (`--profile` belongs to `bootstrap`). Per-command -// ownership needs each command to declare its own flags, which is exactly what -// `flagValidationCoverage` below measures the progress of: as of 6.2.0 one command -// module validates its flags and the rest accept anything, so that number is reported -// as a tracked warning rather than left invisible. +// The OpenCLI standard `flags` array remains intentionally omitted until kit models +// flag types/arity. The namespaced accepted-flag list is narrower: it answers the +// enforcement question without fabricating metadata a code generator could misuse. import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; @@ -140,6 +136,17 @@ export interface DocCommandRef { file: string; } +export interface DocFlagRef { + /** The top-level command whose allowlist must contain this flag. */ + command: string; + /** Normalised long flag name, e.g. `--json` from `--json=true`. */ + flag: string; + /** 1-based line in the source markdown. */ + line: number; + /** Repo-relative posix path of the markdown file. */ + file: string; +} + // A `kit ` occurrence in command position: at the start of a code span/line, // after a shell prompt, or after a `&&` / `|` chain operator. Anchoring this way is // what separates a real invocation from English prose — an unanchored scan matches @@ -218,13 +225,68 @@ export function extractDocCommandRefs(markdownText: string, file: string): DocCo return refs; } +const DOC_GLOBAL_PREFIX_FLAGS = new Set([ + "--read-only", + "--readonly", + "--non-interactive", + "--env", +]); + +const KIT_INVOCATION_RE = /(?:^|\$\s|&&\s|\|\s)\s*(?:npx\s+)?kit\s+([^;&|`\n]+)/g; + +function normaliseDocFlagToken(token: string): string | null { + let clean = token.trim().replace(/^[[{(<]+/, ""); + if (clean === "--") return "--"; + const eq = clean.indexOf("="); + if (eq >= 0) clean = clean.slice(0, eq); + clean = clean.replace(/[\]),}>.:;]+$/g, ""); + return /^--[a-z][a-z0-9-]{1,30}$/.test(clean) ? clean : null; +} + +function extractInvocationFlagRefs(invocation: string, line: number, file: string): DocFlagRef[] { + const tokens = invocation.trim().split(/\s+/).filter(Boolean); + const leadingFlags: string[] = []; + let command: string | null = null; + let commandIndex = -1; + + for (let i = 0; i < tokens.length; i++) { + const flag = normaliseDocFlagToken(tokens[i]); + if (flag === "--") return []; + if (flag) { + if (DOC_GLOBAL_PREFIX_FLAGS.has(flag)) { + leadingFlags.push(flag); + continue; + } + return []; + } + + const word = tokens[i].replace(/^[[{(<]+/, "").replace(/[\]),}>.:;]+$/g, ""); + if (/^[a-z][a-z0-9-]{1,30}$/.test(word) && !PLACEHOLDER.has(word)) { + command = word; + commandIndex = i; + } + break; + } + + if (!command) return []; + + const refs = leadingFlags.map((flag) => ({ command, flag, line, file })); + for (const token of tokens.slice(commandIndex + 1)) { + const flag = normaliseDocFlagToken(token); + if (flag === "--") break; + if (flag) refs.push({ command, flag, line, file }); + } + return refs; +} + /** - * Extract flags documented on a `kit …` invocation. Only spans that actually invoke - * kit are scanned, so a bare `--verbose` in prose about some other tool is ignored. - * `--flag=value` is normalised to `--flag`. + * Extract flags documented on a `kit …` invocation, attributed to the command that + * owns the allowlist. Only spans that actually invoke kit are scanned, so a bare + * `--verbose` in prose about some other tool is ignored. `--flag=value` is + * normalised to `--flag`, and pass-through flags after `--` are left alone. */ -export function extractDocFlagRefs(markdownText: string, file: string): DocCommandRef[] { - const refs: DocCommandRef[] = []; +export function extractDocFlagRefs(markdownText: string, file: string): DocFlagRef[] { + const refs: DocFlagRef[] = []; const lines = markdownText.split("\n"); let inFence = false; @@ -236,9 +298,8 @@ export function extractDocFlagRefs(markdownText: string, file: string): DocComma } const spans = inFence ? [line] : [...line.matchAll(/`([^`\n]+)`/g)].map((m) => m[1]); for (const span of spans) { - if (!/(?:^|\$\s|&&\s)\s*(?:npx\s+)?kit\s/.test(span)) continue; - for (const m of span.matchAll(/\s(--[a-z][a-z0-9-]{1,30})/g)) { - refs.push({ verb: m[1], line: i + 1, file }); + for (const m of span.matchAll(KIT_INVOCATION_RE)) { + refs.push(...extractInvocationFlagRefs(m[1], i + 1, file)); } } } @@ -394,6 +455,36 @@ export function loadContractVerbs(repoRoot: string): Set | null { } } +interface ContractFlagCommand { + "x-kit-args-modeled"?: unknown; + "x-kit-accepted-flags"?: unknown; +} + +/** + * Read per-command accepted flag names from the committed OpenCLI contract. + * Returns null when no command has modeled flag names — the caller must report + * "could not verify", never quietly fall back to a repo-global grep. + */ +export function loadContractFlagSurface(repoRoot: string): Map> | null { + try { + const raw = readFileSync(join(repoRoot, CONTRACT_PATH), "utf-8"); + const parsed: unknown = JSON.parse(raw); + const commands = (parsed as { commands?: unknown })?.commands; + if (!commands || typeof commands !== "object") return null; + + const out = new Map>(); + for (const [verb, value] of Object.entries(commands as Record)) { + if (value["x-kit-args-modeled"] !== true) continue; + const flags = value["x-kit-accepted-flags"]; + if (!Array.isArray(flags) || !flags.every((f) => typeof f === "string")) continue; + out.set(verb, new Set(flags)); + } + return out.size > 0 ? out : null; + } catch { + return null; + } +} + // Subtrees a markdown walk never needs to enter. const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "coverage", ".next", "tmp"]); @@ -441,12 +532,6 @@ const CLAIM_CLASSES: ClaimClass[] = [ extract: extractDocCommandRefs, render: (v) => `kit ${v}`, }, - { - name: "documented flags", - unit: "`kit … --flag`", - extract: extractDocFlagRefs, - render: (v) => v, - }, { name: "documented config sections", unit: "`[section]`", @@ -520,6 +605,87 @@ export function loadCommandSurface(repoRoot: string): { verb: string; harness: b return [...out].map(([verb, harness]) => ({ verb, harness })); } +function runDocumentedFlagsAudit( + repoRoot: string, + inScope: { file: string; text: string }[], + exemptFiles: number, +): SecurityCheckResult { + const surface = loadContractFlagSurface(repoRoot); + if (!surface) { + return { + category: DOCS_CATEGORY, + name: "documented flags", + status: "warn", + severity: "medium", + detail: `${CONTRACT_PATH} has no modeled accepted flag names — accepted flag names NOT verified`, + didNotRun: true, + suggestion: "regenerate the OpenCLI contract after flag-surface is wired into it", + }; + } + + const unknown: DocFlagRef[] = []; + const unverified: DocFlagRef[] = []; + let checked = 0; + + for (const { file, text } of inScope) { + for (const ref of extractDocFlagRefs(text, file)) { + checked++; + const accepted = surface.get(ref.command); + if (!accepted) { + unverified.push(ref); + continue; + } + if (!accepted.has(ref.flag)) unknown.push(ref); + } + } + + if (unknown.length > 0) { + return { + category: DOCS_CATEGORY, + name: "documented flags", + status: "fail", + severity: "medium", + detail: + `${unknown.length} of ${checked} \`kit --flag\` ref(s) do not exist: ` + + unknown + .slice(0, 6) + .map((u) => `kit ${u.command} ${u.flag} (${u.file}:${u.line})`) + .join(", "), + files: [...new Set(unknown.map((u) => u.file))], + suggestion: + "fix the doc, or add the flag to that command's declared flag surface — a real flag on the wrong command still misleads readers", + }; + } + + if (unverified.length > 0) { + return { + category: DOCS_CATEGORY, + name: "documented flags", + status: "warn", + severity: "medium", + detail: + `${unverified.length} of ${checked} \`kit --flag\` ref(s) could not be verified ` + + `because their command has no modeled accepted flag names: ` + + unverified + .slice(0, 6) + .map((u) => `kit ${u.command} ${u.flag} (${u.file}:${u.line})`) + .join(", "), + files: [...new Set(unverified.map((u) => u.file))], + didNotRun: true, + suggestion: "regenerate the OpenCLI contract so every command has x-kit-accepted-flags", + }; + } + + return { + category: DOCS_CATEGORY, + name: "documented flags", + status: "pass", + detail: + `${checked} \`kit --flag\` ref(s) across ${inScope.length} doc(s) all resolve ` + + `against ${surface.size} modeled command(s) (${exemptFiles} doc(s) exempt)`, + }; +} + /** * Does any doc invoke `verb`? Brace form counts: `kit hooks {install,add,sync}` documents * `hooks install`. Without that, the repo's own house style would read as 40+ false gaps. @@ -620,7 +786,6 @@ export function runDocsClaimsAudit(repoRoot: string): SecurityCheckResult[] { const oracles: Record> = { "documented commands": verbs, - "documented flags": loadSourceFlagTokens(repoRoot), "documented config sections": new Set(KNOWN_SECTIONS), "documented env vars": loadKnownEnvVars(repoRoot), }; @@ -674,45 +839,49 @@ export function runDocsClaimsAudit(repoRoot: string): SecurityCheckResult[] { suggestion: `run 'node scripts/derive-command-flags.mjs --emit' — a flag that silently does nothing is how 'kit check --category' stayed broken across six majors`, })); - return flagValidation.concat( - CLAIM_CLASSES.map((cls) => { - const oracle = oracles[cls.name]; - const unknown: DocCommandRef[] = []; - let checked = 0; - - for (const { file, text } of inScope) { - for (const ref of cls.extract(text, file)) { - checked++; - if (!oracle.has(ref.verb)) unknown.push(ref); - } - } + const claimResults: SecurityCheckResult[] = CLAIM_CLASSES.map((cls): SecurityCheckResult => { + const oracle = oracles[cls.name]; + const unknown: DocCommandRef[] = []; + let checked = 0; - if (unknown.length === 0) { - return { - category: DOCS_CATEGORY, - name: cls.name, - status: "pass", - detail: - `${checked} ${cls.unit} ref(s) across ${inScope.length} doc(s) all resolve ` + - `against ${oracle.size} known (${exemptFiles} doc(s) exempt)`, - }; + for (const { file, text } of inScope) { + for (const ref of cls.extract(text, file)) { + checked++; + if (!oracle.has(ref.verb)) unknown.push(ref); } + } + if (unknown.length === 0) { return { category: DOCS_CATEGORY, name: cls.name, - status: "fail", - severity: "medium", + status: "pass", detail: - `${unknown.length} of ${checked} ${cls.unit} ref(s) do not exist: ` + - unknown - .slice(0, 6) - .map((u) => `${cls.render(u.verb)} (${u.file}:${u.line})`) - .join(", "), - files: [...new Set(unknown.map((u) => u.file))], - suggestion: `fix the doc, or implement it — a doc naming something kit does not have misleads humans and agents alike`, + `${checked} ${cls.unit} ref(s) across ${inScope.length} doc(s) all resolve ` + + `against ${oracle.size} known (${exemptFiles} doc(s) exempt)`, }; - }), + } + + return { + category: DOCS_CATEGORY, + name: cls.name, + status: "fail", + severity: "medium", + detail: + `${unknown.length} of ${checked} ${cls.unit} ref(s) do not exist: ` + + unknown + .slice(0, 6) + .map((u) => `${cls.render(u.verb)} (${u.file}:${u.line})`) + .join(", "), + files: [...new Set(unknown.map((u) => u.file))], + suggestion: `fix the doc, or implement it — a doc naming something kit does not have misleads humans and agents alike`, + }; + }); + + return flagValidation.concat( + claimResults.slice(0, 1), + [runDocumentedFlagsAudit(repoRoot, inScope, exemptFiles)], + claimResults.slice(1), // Appended last on purpose: `flagValidation` occupies res[0] and a test pins that // position. Prepending this row displaced it and failed a test about node_modules // skipping — an unrelated rule broken by an ordering assumption.