@aihq/harness 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,16 +8,17 @@
8
8
  [![Node ≥20](https://img.shields.io/badge/node-%E2%89%A520-339933.svg)](package.json)
9
9
 
10
10
  <p align="center">
11
- <img src="docs/assets/aih-overview.svg" alt="aih — extract corporate trust, self-heal the runtime, tune the workstation, bootstrap a governed repo, and run AI-assisted coding behind a proxy" width="100%">
11
+ <img src="docs/assets/aih-overview.svg" alt="aih — extract corporate trust, self-heal the runtime, tune the workstation, bootstrap a governed repo, govern agent skills, and run AI-assisted coding in enterprise environments" width="100%">
12
12
  </p>
13
13
 
14
14
  A cross-platform CLI that helps prepare developer workstations and repositories for
15
- **reviewable, governed AI-assisted coding behind a corporate proxy**. It extracts
16
- corporate trust, tunes local inference, adds repo guardrails, wires up
17
- MCP / observability / sandboxing, and lays down a tool-agnostic context
18
- architecture all from one command surface. On top of that setup it runs a
19
- governance loop for external agent skills vet approve pack marketplace →
20
- evidence — anchored in a committed approval lock (`aih-skills.lock.json`).
15
+ **reviewable, governed AI-assisted coding in enterprise environments** from
16
+ locked-down, TLS-intercepted networks to open ones. It extracts corporate trust,
17
+ tunes local inference, adds repo guardrails, wires up MCP / observability /
18
+ sandboxing, and lays down a tool-agnostic context architecture all from one
19
+ command surface. On top of that setup it runs a governance loop for external
20
+ agent skills — vet → approve → pack → marketplace → evidence — anchored in a
21
+ committed approval lock (`aih-skills.lock.json`).
21
22
 
22
23
  > Implements the architectural blueprint *"Enterprise DevSecOps AI Bootstrapping:
23
24
  > Cryptographic Trust, Local Performance Optimization, and Unified Observability"*
@@ -27,6 +28,16 @@ evidence — anchored in a committed approval lock (`aih-skills.lock.json`).
27
28
  > support obligation, SLA, indemnity, consulting, or professional advice is provided. `aih`
28
29
  > is dry-run by default — review the plan before running `--apply`. See [DISCLAIMER.md](DISCLAIMER.md).
29
30
 
31
+ ## The 1.0 contract
32
+
33
+ Pin `@aihq/harness@^1` and automate against it. Every command, flag, and deprecated
34
+ alias is snapshot-tested in CI against a committed fixture, the `--json` envelope is
35
+ schema-pinned, and exit-code semantics are pinned — a surface change fails the build
36
+ until it ships as a reviewed contract decision. Renames ship as deprecated aliases
37
+ (the old name keeps working, with a one-line warning) before a major removes them,
38
+ and security fixes land on the latest and the previous minor. The full policy:
39
+ [STABILITY.md](STABILITY.md).
40
+
30
41
  ## Design posture
31
42
 
32
43
  - **Dry-run by default.** `aih <cmd>` computes and prints a plan; nothing is
@@ -81,43 +92,68 @@ aih init . --apply # apply it
81
92
 
82
93
  ## Command surface
83
94
 
95
+ One honest line per command — the long-form behavior detail for every command lives in
96
+ [docs/commands.md](docs/commands.md), and `aih <command> --help` is authoritative for flags.
97
+
98
+ ### Workstation & runtime
99
+
100
+ | Command | What it does |
101
+ | --- | --- |
102
+ | [`aih certs`](docs/commands.md#aih-certs) | Extract the corporate root CA from the OS trust store and propagate trust to npm/pip/cargo/conda. |
103
+ | [`aih heal`](docs/commands.md#aih-heal) | Diagnose and repair the broken runtime behind any TLS-intercepting proxy — corporate trust, npm, PATH, MCP pre-flight. |
104
+ | [`aih tools`](docs/commands.md#aih-tools) | Install the agent shell tools the harness leans on (`rg`/`fd`/`jq`, `ast-grep`, `gh`, …) through the platform package manager. |
105
+ | [`aih ready`](docs/commands.md#aih-ready) | Grade a blocker-aware readiness verdict: can a developer start work with an AI agent here, now? |
106
+ | [`aih hardware`](docs/commands.md#aih-hardware) | Profile CPU/RAM/GPU and emit tuned Ollama/llama.cpp settings. |
107
+ | [`aih vdi`](docs/commands.md#aih-vdi) | Detect VDI (Citrix/WorkSpaces/RES/RDP) and redirect caches + SQLite to local scratch. |
108
+ | [`aih bootstrap`](docs/commands.md#aih-bootstrap) | Orchestrate the workstation 4-phase rollout (certs → hardware/vdi → telemetry). |
109
+
110
+ ### Repo canon & bootstrap
111
+
112
+ | Command | What it does |
113
+ | --- | --- |
114
+ | [`aih init`](docs/commands.md#aih-init) | Initialize a repo in one pass: profile + superpowers + bootstrap-ai + scaffold + secrets + guardrails + mcp + sandbox. |
115
+ | [`aih profile`](docs/commands.md#aih-profile) | Detect the repo's stack recursively and synthesize Cursor stack rules (`.cursor/rules/*.mdc`). |
116
+ | [`aih scaffold`](docs/commands.md#aih-scaffold) | Scaffold repo hygiene — secret deny-list, pre-commit hook, `.gitignore` entries; `--canon legacy` adds the full context-doc family. |
117
+ | [`aih bootstrap-ai`](docs/commands.md#aih-bootstrap-ai) | Emit and verify the repo's Layer-2 canon — `RULE_ROUTER.md`, per-CLI adapters, root bootloaders; `--verify` is the drift gate. |
118
+ | [`aih contract`](docs/commands.md#aih-contract) | Synthesize the machine-readable repo contract (`project.json`) from the detected stack. |
119
+ | [`aih adopt`](docs/commands.md#aih-adopt) | Converge an existing AI canon onto aih's managed model without overwriting your work (brownfield migration). |
120
+ | [`aih prune`](docs/commands.md#aih-prune) | Remove the stale per-CLI artifacts left for CLIs the repo no longer targets (reversible by default). |
121
+ | [`aih ecc`](docs/commands.md#aih-ecc) | Install affaan-m/ECC (skills, instincts, memory) for the selected CLIs via ECC's own installer. |
122
+ | [`aih superpowers`](docs/commands.md#aih-superpowers) | Install obra/Superpowers (brainstorm → plan → TDD → subagent-review skills) for the selected CLIs. |
123
+ | [`aih crispy`](docs/commands.md#aih-crispy) | Run the CRISPY context-engineering stage machine (deterministic, gate-ordered). |
124
+ | [`aih workspace`](docs/commands.md#aih-workspace) | Scaffold a multi-repo workspace at the parent folder: cross-repo map, combined MCP, `.code-workspace`. |
125
+
126
+ ### Skill governance & supply chain
127
+
128
+ | Command | What it does |
129
+ | --- | --- |
130
+ | [`aih trust`](docs/commands.md#aih-trust) | Vet, pin, and gate external GitHub repos and skills before an agent acquires them. |
131
+ | [`aih skill`](docs/commands.md#aih-skill) | Govern the skill lifecycle — vet → approve → inventory → quarantine → remove — anchored in `aih-skills.lock.json`. |
132
+ | [`aih pack`](docs/commands.md#aih-pack) | Curate committed sets of approved skills (`aih-packs.json`); every ref is cross-checked against the lock, fail-closed. |
133
+ | [`aih marketplace`](docs/commands.md#aih-marketplace) | Build, validate, and publish a reproducible, verifiable distribution artifact from the approval lock — never a registry. |
134
+ | [`aih policy`](docs/commands.md#aih-policy) | Validate the committed org policy — or a policy-bundle envelope — as a read-only CI gate. |
135
+ | [`aih evidence`](docs/commands.md#aih-evidence) | Package the audit trail aih already emits (locks, cards, vet evidence, run logs) into one deterministic evidence bundle. |
136
+ | [`aih bundle`](docs/commands.md#aih-bundle) | Build a deterministic fleet bundle (contract + policy + config) with checksums; `aih verify-bundle` re-checks any copy. |
137
+ | [`aih secrets`](docs/commands.md#aih-secrets) | Scan for plaintext `.env*`/`secrets/` and write agent deny rules; `--verify` is the secret-scan CI gate. |
138
+ | [`aih guardrails`](docs/commands.md#aih-guardrails) | Generate `.gitleaks.toml`, `.pre-commit-config.yaml`, and a CI license gate that blocks AGPL/strong-copyleft. |
139
+
140
+ ### Analytics & operations
141
+
142
+ | Command | What it does |
143
+ | --- | --- |
144
+ | [`aih report`](docs/commands.md#aih-report) | Render the read-only analytics digest — context footprint, adoption, trends; `--v9`/`--open` build the offline HTML dashboard. |
145
+ | [`aih track`](docs/commands.md#aih-track) | Record one metrics sample (commits, LOC delta, adoption) to `.aih/history.jsonl` — the time-series behind `aih report` trends. |
146
+ | [`aih usage`](docs/commands.md#aih-usage) | Install the multi-tool usage-capture layer → `.aih/usage.jsonl` — local activity counts only, no cost, no prompts. |
147
+ | [`aih telemetry`](docs/commands.md#aih-telemetry) | Inject OpenTelemetry env, a redacting Bindplane collector, and an analytics fetcher. |
148
+ | [`aih mcp`](docs/commands.md#aih-mcp) | Generate the MCP server config for the targeted CLIs; per-tool guidance where a generated file would be wrong. |
149
+ | [`aih sandbox`](docs/commands.md#aih-sandbox) | Generate a devcontainer + managed sandbox settings (egress allowlist, `failIfUnavailable`). |
150
+
151
+ ### Verification
152
+
84
153
  | Command | What it does |
85
154
  | --- | --- |
86
- | `aih certs` | Extract the corporate root CA from the OS trust store, lock it down, and propagate trust to npm/pip/cargo/conda. |
87
- | `aih heal` | Diagnose **and repair** the broken runtime `certs` assumes works — corporate TLS trust, npm, PATH, and MCP pre-flight — generically for any TLS-intercepting proxy (`--ca-pattern`/`AIH_CA_PATTERN`, never hardcoded). Diagnoses by default (exits non-zero when broken) and repairs under `--apply`; the npm self-heal is emitted as an operator-run script (never executed) and the only mutation is a local Windows registry write to persist the CA for GUI-launched apps (Claude/Kiro), so the harness never contacts a remote. `--scope certs,npm,path,mcp,all`. |
88
- | `aih tools` | Install the agent shell tools the harness leans on — `rg`/`fd`/`jq` plus `ast-grep`/`comby`/`tree`/`gh`/`code-review-graph` — through the platform package manager. Dry-run previews; `--apply` installs. A blocked install on a locked-down box is escalated as an IT ticket rather than failing silently. |
89
- | `aih ready` | Readiness gate — one graded, blocker-aware verdict answering "can a developer start work with an AI agent here, now?", composed from aih's read-only probes (runtime/TLS/PATH/core tools, per-CLI loadability, contract, secret scan). Diagnoses by default (non-zero when blocked); the one auto-fixable blocker (missing `rg`/`fd`/`jq`) installs under confirmation. Surfaces a `sec-ready` panel in `aih report --v9`. |
90
- | `aih hardware` | Profile CPU/RAM/GPU; compute memory/thread/parallel limits + quantization; emit tuned Ollama/llama.cpp settings. |
91
- | `aih vdi` | Detect VDI (Citrix/WorkSpaces/RES/RDP) and redirect caches + SQLite to local scratch (junction on Windows). |
92
- | `aih profile` | Recursively detect the repo's stack and synthesize Cursor stack rules (`.cursor/rules/*.mdc`). Root bootloaders are owned by `bootstrap-ai`. |
93
- | `aih ecc` | Install [affaan-m/ECC](https://github.com/affaan-m/ECC) (skills, instincts, memory, security, research-first) for the selected CLIs, scoped to the detected stack: Claude plugin path, `ecc-install` for codex/cursor/zed/opencode, `consult` advisor otherwise. |
94
- | `aih superpowers` | Install [obra/Superpowers](https://github.com/obra/Superpowers) (brainstorm → plan → TDD → subagent-review skills) for the selected CLIs. |
95
- | `aih scaffold` | Create the canonical context dir (`--context-dir`, default `ai-coding`) — INDEX/SKILL skeleton, an agent **`SETUP-TASKS.md`** playbook (fill context + guardrails from the code), a write-once `project-guardrails.md`, a secret deny-list, and a pre-commit hook. (Bootloaders are `bootstrap-ai`'s job.) |
96
- | `aih guardrails` | Generate `.gitleaks.toml`, `.pre-commit-config.yaml`, and a CI license gate that blocks AGPL/strong-copyleft. |
97
- | `aih secrets` | Scan for plaintext `.env*`/`secrets/` and write agent deny rules + vault-injection guidance. `--verify` is the **secret-scan CI gate** (exit 1 when plaintext secrets exist); `--sarif <file>` emits one error-level result per path for GitHub code-scanning. |
98
- | `aih trust` | Vet, pin, and gate external GitHub repos and skills before an agent acquires them. `scan <target>` grades danger (auto-exec hooks, dependency-confusion, typosquat, incoming-MCP, secrets) and emits SARIF; `allow`/`pin` record reviewed sources + pinned SHAs in org policy; `list`/`verify` audit the committed policy and trust-lock evidence. |
99
- | `aih skill` | The **skill lifecycle** on top of `trust` — a complete governance loop for external agent skills. `vet <src>` runs the read-only gate pipeline (shape, license, trust scan) to a **GREEN/YELLOW/RED/UNKNOWN** verdict + a local evidence artifact (never installs). `card`/`approve --pin --owner` turn that evidence into committed governance: a skill card + a root **`aih-skills.lock.json`** entry, behind a fail-closed chain (pin → evidence → approvable verdict → license → owner; RED blocked, UNKNOWN refused, YELLOW = the manual review). The lockfile has **install-time teeth**: `workspace add` refuses promoting a skill with no committed approval *for that source's pinned commit* at `team`/`enterprise` posture (advisory at `vibe`) — a same-named skill from an unrelated source never inherits an approval, and stale approvals are refused. `inventory` joins on-disk skills against the approvals — approved / unapproved / stale-pin / quarantined, one row per physical install — and feeds a "Skill governance" panel in `report --v9`. `quarantine --name <skill>` **disables reversibly** (dir → `.aih/quarantine/`, approval kept; move it back to restore). `remove --name <skill>` retracts: archives the skill dir reversibly (`--delete` to hard-delete), drops the approval + card; refuses ambiguous duplicates, nested-skill collateral, machine-root installs, and stranding a parked copy's approval; cleans up orphaned approvals. |
100
- | `aih pack` | **Curation manifests** on top of the per-skill lifecycle — a committed root `aih-packs.json` names sets of approved skills so a team installs "the docs-quality pack", not N individual approvals. The `aih-skills.lock.json` stays the **pin authority**: every manifest ref is a fail-closed cross-check against the lock entry (`pack.pin-mismatch` blocks; a disagreeing manifest is never a second pin). `status`/`validate` grade each pack on the two orthogonal axes (approval × install) — `validate` is the **CI gate** (coded findings: `pack.missing-approval`, `pack.pin-mismatch`, `pack.duplicate-name`). `add`/`remove-entry`/`init` author the manifest with refs **derived from the lock** (authoring never invents a pin; `init` seeds a pack from `skill approve --pack` tags; an emptied pack is dropped whole). `plan`/`install` drive the gated two-phase acquisition once per source — **gate ALL sources before promoting ANY**, promote only the pack's refs (subset-exact), route drifted installs back through the gate, resume idempotently — fail-closed at every posture (clean approvals required even at `vibe`; `--acknowledge` refused, acknowledgements stay per-source). `uninstall` retracts every installed member with `skill remove`'s exact per-member semantics — reversible archive (or `--delete`), approval + card dropped, loader-ref advisories, the same refusal guards, and **one blocked member refuses the whole plan**; the manifest curation stays. Installed skills' pack tags roll up in the report's Skill-governance panel. |
101
- | `aih marketplace` | Package the approved skill set into a **reproducible, verifiable distribution artifact** — a directory a team can host anywhere (git repo or static host), never a registry/server. `build` reads `aih-skills.lock.json` (the **approval authority**) and emits the exact vetted skill bytes (trust-lock hash cross-checked), the committed skill cards, the content-addressed vet evidence, a `marketplace.json` manifest, and `SHA256SUMS` — byte-identical across builds from identical inputs (no wall-clock; `--stamp` is operator-supplied), and **fail-closed whole**: an approved skill that is uninstalled, drifted, ambiguous, or missing its card/evidence refuses the entire build. `validate` is the **read-only CI gate** over a built or fetched artifact (coded findings: `marketplace.manifest-parse`, `marketplace.path-traversal`, `marketplace.missing-file`, `marketplace.checksum-mismatch`, `marketplace.sums-coverage`, `marketplace.unapproved-verdict`, `marketplace.signature`), containment-checking every manifest/sums path **before** touching the filesystem with it. `publish` signs the artifact's `SHA256SUMS` (cosign or a GitHub attestation — a publish without a signer is refused; that's just a build); `validate --require-signature` then **fails rather than skips** when that signature can't be verified. Consumers stay on `aih workspace add` — the vet gate still runs at consume time. |
102
- | `aih mcp` | Generate the MCP server config **for the targeted CLIs** (`--cli`/`--all-tools`, default claude): Claude/Cursor/Kiro/Kimi get their correct project file written (`.mcp.json`, `.cursor/mcp.json`, …); Codex (TOML), Copilot, OpenCode, Zed, and global-config tools get exact per-tool guidance instead of a file aih would get wrong. Scopes: local/project/remote. For locked-down orgs, `--mode offline` (vendored local-command servers) or `--mode none` (no MCP + a CLI-tool fallback) plus a `managed-mcp.json` admin template. |
103
- | `aih sandbox` | Generate a devcontainer + managed sandbox settings (egress allowlist, `failIfUnavailable`). |
104
- | `aih telemetry` | Inject OpenTelemetry env, a redacting Bindplane collector, and an analytics fetcher (usage + skills endpoints → `{ usage_report, skills }`). |
105
- | `aih report` | Read-only analytics digest. Local: a dev console — agent **context footprint** (token bloat) plus a **per-turn load-group** panel (the heaviest single tool's always-loaded bootloaders — what one tool actually pays per turn, not the union sum; `--gate --token-budget <n>` exits non-zero in CI when it's exceeded). The footprint is **gitignore-honoring** (counts only tracked/untracked-not-ignored source, never generated per-CLI copies — `--all-files` to override; `--since <ref>` narrows to files changed in a PR), **repo & branch status** (current branch, ahead/behind vs main, dirty; `--team` adds in-progress team branches via a `gh` → `git ls-remote` → last-fetched ladder that degrades gracefully when gh/network is blocked), repo config presence, local AI-CLI tooling saturation, and **trends** (unicode sparklines of commits/LOC/adoption/branches over recorded history — see `aih track`). Org (`--org <export.json>`): top skills, tokens by type, **cache savings** (net-of-write estimate), and accept/reject from a saved Admin-API export. Body prints verbatim; `--json` carries structured data; `--format md\|html` writes a static artifact under `--apply`. **`--v9`** opts into the developer-console HTML dashboard with LIVE / PREVIEW / EMPTY panel honesty, machine-relative ECC inventory, usage-by-CLI, heavy lifters, dormant ECC skills, MCP parity, remediation wins, and no-cost local usage analytics; legacy and `--v4` remain opt-in/unchanged. **`--open`** builds the self-contained HTML dashboard and launches it in your browser (implies html + apply); **`--refresh <sec>`** keeps it live — opens once, then regenerates every `<sec>`s while the page auto-reloads (Ctrl+C to stop). Dark by default with a light toggle; fonts are embedded so it works fully offline. Network-free by default; `--team` is the lone opt-in network call. |
106
- | `aih track` | Record one metrics sample (commits 7d, LOC delta, adoption score, branch count, tracked files) to `.aih/history.jsonl` — the time-series behind `aih report` trends. Read-only git/filesystem; dry-run previews, `--apply` appends (idempotent per commit). Wire into a commit / agent-stop hook so history accumulates — e.g. Kiro's `metrics-on-stop` hook (`aih bootstrap-ai --cli kiro`) runs `aih track --apply` automatically. |
107
- | `aih usage` | Install the **multi-tool usage-capture** layer → `.aih/usage.jsonl` (rendered by `aih report` and `aih report --v9`). The **universal floor** is a git `post-commit` hook that records commit activity for **any** tool (it keys off the commit, not the agent). The per-tool **skill/MCP** layer wires in via each CLI's verified local hook (Claude/Codex/Cursor/Gemini/Kiro/…); skills aggregate by source (ECC/canon/user), and `--rollup <repo,repo>` aggregates local logs across repos on demand. Usage is local activity counts only — **no cost, no prompts, no arguments**, machine-local and gitignored. |
108
- | `aih crispy` | Run the CRISPY context-engineering stage machine (deterministic, gate-ordered). |
109
- | `aih bootstrap` | Orchestrate the workstation 4-phase rollout (certs → hardware/vdi → telemetry). |
110
- | `aih bootstrap-ai` | Emit + verify the repo's Layer-2 `ai-coding/` canon: `RULE_ROUTER.md`, per-CLI adapters, and root bootloaders (tool preamble + a regenerated shared block). `--verify` is the drift gate **and a weak-model-safety lint of the generated canon** — every `#[[file:…]]`/backtick reference must resolve and no leftover `<insert>`/`TODO` scaffolding ships (a dangling reference fails the gate; soft-imperative/taste-word prose is advisory). |
111
- | `aih contract` | Synthesize the machine-readable repo contract (`project.json`) from the detected stack — the structured seam agents and tooling read for build/test/lint commands and conventions, alongside the `ai-coding/` prose canon. Merges over any user-added keys (write-once-safe); dry-run previews, `--apply` writes. |
112
- | `aih prune` | Remove the stale per-CLI artifacts a repo still carries for a CLI it no longer targets (the inverse of `bootstrap-ai`). Dry-run preview by default; `--apply` moves aih-owned files to gitignored `.aih/legacy/` (reversible), subtracts aih's managed block **in place** from co-owned bootloaders (never deletes them), and leaves unmarked MCP/settings as manual advisories. Diffed against **committed intent only** (`.aih-config.json`), so a bare run is safe anywhere; a dirty/untracked target refuses without `--force`. `--delete` hard-deletes to a gitignored `*.aih.bak` sibling (never overwriting a prior backup) instead of archiving; `--unrunnable` also prunes a still-targeted CLI whose binary is absent from `PATH` (loud warning; never the default). |
113
- | `aih init` | Initialize a repo: profile + superpowers + bootstrap-ai + scaffold + secrets + guardrails + mcp + sandbox in one pass (one writer per file). ECC is a separate gated network step — run `aih ecc` when ready (it points at ECC's own installer). |
114
- | `aih adopt` | Converge an **existing** AI canon onto aih's managed model **without overwriting your work** (brownfield migration) — for a repo that already has an `AGENTS.md`/`.cursor`/`ai-*` setup. `--migrate-cli` folds committed CLI-native content into the canon (copy + pointer-convert, content-verified, backed up); `--ack <paths>` marks paths as intentionally tool-native so adopt stops flagging them. |
115
- | `aih workspace` | Scaffold a **multi-repo** workspace (parent-only): cross-repo architecture map (write-once) + per-repo discipline, a VS Code `.code-workspace`, combined graph/filesystem MCP spanning every child repo, and a `.aih-workspace.json` marker. |
116
- | `aih bundle` | Build a deterministic **fleet bundle** — the repo contract, org policy, and managed config packaged with a checksum manifest (and an optional `gh`-attested signature) for distribution to a team or CI. `aih verify-bundle` re-checks a bundle against its checksums + signature. |
117
- | `aih policy` | Schema gates for the org policy. `validate` is the **read-only CI gate** over the committed `aih-org-policy.json` — a missing file is a friendly skip (vibe repos carry no org policy), a parse/schema failure is a coded finding (`org-policy.invalid`) — or, under `--bundle <path>`, over a distributable **policy-bundle envelope** (`org-policy.bundle-invalid`, naming which layer failed: the envelope or the embedded policy). |
118
- | `aih evidence` | Package the **audit trail aih already emits** — approval lock, packs manifest, trust lock, skill cards, vet evidence, run logs, report/SARIF outputs — into one deterministic **evidence bundle** (`build`): the exact fleet-bundle layout (`files/<rel>` copies, `manifest.json`, `SHA256SUMS`, optional best-effort `--sign cosign\|gh`) plus `evidence.json`, a typed kind index. Byte-identical across builds from identical inputs (no wall-clock); absent artifact kinds are skipped silently; re-check any copy with `aih verify-bundle --bundle <out>`. |
119
- | `aih doctor` | Fail-closed verification of the workstation/repo configuration (+ workspace mode: validates each child repo). Includes a **canon markdown lint** (read-only) over the scaffolded `ai-coding/` tree. |
120
- | `aih status` | Read-only inventory of what the harness has configured. |
155
+ | [`aih doctor`](docs/commands.md#aih-doctor) | Verify the workstation/repo configuration fail-closed; workspace mode validates each child repo. |
156
+ | [`aih status`](docs/commands.md#aih-status) | Show a read-only inventory of what the harness has configured. |
121
157
 
122
158
  Shared flags: `--apply`, `--force`, `--verify`, `--json`, `--posture <vibe|team|enterprise>`, `--support-out <dir>`, `--no-log`, `--context-dir <dir>`, `--root <dir>`, `--cli <list>`, `--all-tools`, `--detect`, `--yes` (the read-only `doctor`/`status`/`verify-bundle` take the relevant subset).
123
159
  Settings also read from `AIH_*` env vars (`AIH_APPLY`, `AIH_CONTEXT_DIR`, `AIH_LOG`, …).
@@ -272,49 +308,11 @@ It writes, at the parent (it does **not** touch the child repos — run `aih ini
272
308
 
273
309
  ### Support tickets
274
310
 
275
- Any verifying command (`aih doctor`, `aih heal`, `aih bootstrap-ai --verify`, `aih secrets --verify`, …)
276
- turns a failed or skipped check that carries a `Check.code` into a **ticket-ready, tool-neutral support
277
- template** so a developer blocked by corporate environment config (untrusted CA, broken npm, blocked
278
- registry) can escalate without hand-writing the ask. `aih report` also derives its own **advisory**
279
- findings from the analytics panels (per-turn context **over budget**, incomplete **adoption** in an
280
- initialised repo) as developer self-fix notes — they never fail the run (a bare `aih report` still exits
281
- 0; only `--gate` makes the budget a CI gate). Templates render in three registers, keyed off who fixes
282
- the issue:
283
-
284
- - **External escalation** — an external-audience check that **failed**; the fix is a system change owned
285
- by IT, security, or the dev-platform team (untrusted corporate CA, broken package manager, unreachable
286
- registry). Blocking failures lead with `[<project>] Blocking setup issue — …`.
287
- - **External improvement request** — an external-audience check that **skipped**: a non-blocking
288
- configuration gap that degrades the setup without blocking it.
289
- - **Developer self-fix note** — a developer-audience finding the developer resolves directly (install
290
- git, `aih mcp --apply`); terse, runnable, and the only register that may name `aih`.
291
-
292
- By default the terminal prints one `[copy] …` label per template under a **Support templates:** heading.
293
- Add **`--support-out <dir>`** to write each full ticket to a repo-contained `<dir>/<code>.md` file (you
294
- named the path — that's the consent, same as `--sarif <file>`). **`--json`** carries the data under a
295
- top-level `support: { findings, templates }` key. Support output is **suppressed when streaming SARIF**
296
- (`--sarif -`) so stdout stays a clean code-scanning artifact.
297
-
298
- **External tickets are tool-neutral by contract** — they never name aih or its commands; they describe
299
- the failed *internal configuration* the recipient must fix at the system level. Each follows the
300
- structure **Summary → Impact → Issue → Observed evidence → Environment → Requested fix → Acceptance
301
- criteria**, and every escalation ends with a security work-around guard (keep TLS verification and secret
302
- controls enabled; don't change project code). Evidence, affected area, and acceptance criteria are canned
303
- per code — never guessed — with the live check detail riding along as evidence (redacted: home-dir
304
- scrubbed, secret-aware argv masking).
305
-
306
- **Project context (`SETUP.md`).** A project can shape the tickets with opt-in HTML-comment markers in
307
- `SETUP.md`, `docs/SETUP.md`, or `.aih/SETUP.md` (first found wins):
308
-
309
- - `<!-- support:why -->…<!-- /support:why -->` — *why a correct environment matters for this project*,
310
- woven into the ticket's Impact / "Why this helps" section. Falls back to the first paragraph under a
311
- `## Why` / `## Overview` / `## Purpose` / `## Background` / `## About` heading, so existing setup files
312
- contribute without edits.
313
- - `<!-- support:routing -->…<!-- /support:routing -->` — real routing metadata (assignment group, ticket
314
- prefix) rendered verbatim in the Environment block. **Never invented** — shown only when you provide it.
315
- - `<!-- support:language -->…<!-- /support:language -->` — an instruction to adapt the message to the
316
- org's corporate language, surfaced as a **terminal note** to the author, never embedded in the ticket
317
- body (which stays clean to paste).
311
+ Any verifying command (`aih doctor`, `aih heal`, `aih secrets --verify`, …) turns a failed or skipped
312
+ check that carries a `Check.code` into a ticket-ready support template — three registers keyed off who
313
+ fixes the issue, and external tickets are tool-neutral by contract (they never name aih). Labels print
314
+ by default; `--support-out <dir>` writes full tickets, `--json` carries them. Registers, redaction, and
315
+ the `SETUP.md` context markers: [docs/commands.md](docs/commands.md#support-tickets).
318
316
 
319
317
  ### Run ledger
320
318
 
@@ -346,11 +344,12 @@ aih usage --rollup ../repo-a,../repo-b
346
344
  [GitHub Milestones](https://github.com/samartomar/ai-harness/milestones).
347
345
  - **Changelog** — [CHANGELOG.md](CHANGELOG.md); tagged builds on
348
346
  [Releases](https://github.com/samartomar/ai-harness/releases).
349
- - **Versioning & support** — [VERSIONING.md](VERSIONING.md). SemVer; while pre-1.0 only the
350
- latest minor receives fixes.
347
+ - **Versioning & support** — [VERSIONING.md](VERSIONING.md). SemVer; from 1.0, security
348
+ fixes land on the latest **and the previous minor** (N-1) of the current major.
351
349
  - **Supply chain** — every release publishes via npm **Trusted Publishing** with build
352
- **provenance** and ships an **SPDX SBOM** + **SHA256 checksum** on the GitHub Release.
353
- Verify an install with `npm audit signatures`.
350
+ **provenance** and ships an **SPDX SBOM**, a **SHA256 checksum**, its keyless **cosign
351
+ signature bundle** (`SHA256SUMS.txt.sigstore.json`), and the Sigstore **build-provenance
352
+ bundle** on the GitHub Release. Verify an install with `npm audit signatures`.
354
353
  - **Support** — [SUPPORT.md](SUPPORT.md) · **Security** — [SECURITY.md](SECURITY.md)
355
354
  (private reporting) · **Contributing** — [CONTRIBUTING.md](CONTRIBUTING.md).
356
355
 
@@ -370,14 +369,12 @@ levels so coverage only ratchets up; CI and releases fail on regression. See
370
369
 
371
370
  ### Stability
372
371
 
373
- The CLI surface and machine-readable outputs are contract-tested in
372
+ The tests behind [The 1.0 contract](#the-10-contract) live in
374
373
  [tests/contract/](tests/contract/): every command and option is snapshotted against a
375
374
  committed fixture ([command-surface.json](tests/contract/command-surface.json)), the
376
- `--json` envelope is schema-pinned, and exit-code semantics are pinned. Any drift fails
377
- CI and forces a reviewed decision additive changes regenerate the fixture in the same
378
- PR (label it `contract:additive`); removals or renames of anything pinned are breaking
379
- and ship in majors only, per the stability policy in [STABILITY.md](STABILITY.md) —
380
- renames ship with a deprecated alias of the old name until the removing major.
375
+ `--json` envelope is schema-pinned, and exit-code semantics are pinned. Additive changes
376
+ regenerate the fixture in the same PR (label it `contract:additive`); removals or renames
377
+ of anything pinned are breaking and ship in majors only, per [STABILITY.md](STABILITY.md).
381
378
 
382
379
  ## License
383
380
 
@@ -3045,7 +3045,7 @@ ${f}
3045
3045
  fix the manifest (aih pack validate) or remove members individually with \`aih skill remove\``)}let l=new Map((a?.skills??[]).map(f=>[f.name,f.approval])),c=Me(e),d=re(e.root),p=[],u=[];for(let f of[...new Set(n.skills.map(v=>v.name))]){if(l.get(f)==="missing-approval"){u.push({name:f,outcome:"not-installed"});continue}let v=c.skills.some(A=>A.name===f),y=d.skills.some(A=>A.name===f);if(!v&&!y){u.push({name:f,outcome:"not-installed"});continue}try{let A=Vl(e,f,r,d);d=A.lock,p.push(...A.actions),u.push({name:f,outcome:A.summary.kind==="orphaned-approval"?"orphaned-approval":"removed",summary:A.summary})}catch(A){throw A instanceof E?Ko(`pack ${t}: member ${f} blocks the uninstall \u2014 ${A.message}
3046
3046
  no member was removed (a pack uninstall is all-or-nothing); resolve that member first or remove it individually with \`aih skill remove --name ${f}\``):A}}let m=u.flatMap(f=>f.outcome==="not-installed"?[]:f.summary.advisories);return p.push(C("pack uninstall",i6(t,u,m),{pack:t,hardDelete:r,counts:{members:u.length,removed:u.filter(f=>f.outcome==="removed").length,orphanedApprovals:u.filter(f=>f.outcome==="orphaned-approval").length,notInstalled:u.filter(f=>f.outcome==="not-installed").length},members:u.map(a6),manifest:"unchanged",lockfile:Z})),S("pack uninstall",...p)}s(c6,"packUninstallPlan");var Id={name:"uninstall",summary:"Uninstall a pack's installed members \u2014 archive each (reversible) and drop its approval; the manifest curation stays",options:[{flags:"--pack <name>",description:"the pack whose installed members to remove (required)"},{flags:"--delete",description:"hard-delete each member to a gitignored *.aih.bak sibling instead of the reversible .aih/legacy/ archive"}],plan:c6};import{join as l6}from"path";var ck={marker:".aih-config.json",ctx:"init orchestrator",none:"none"},d6={adapter:"adapter note",bootloader:"bootloader canon block",mcp:"MCP config",settings:"settings","kiro-steering":"Kiro steering","kiro-hook":"Kiro hook"};function p6(e,t){let n=k(l6(e.root,t));if(n===void 0)return;let r=ge(n,ne);if(r===void 0||r!==me(e.contextDir).trim())return;let o=Tp(n,ne);return o===n?void 0:o}s(p6,"bootloaderMinusBlock");function u6(e){let t=e.artifacts.filter(n=>n.disposition==="advisory");return t.length===0?[]:["","Manual review \u2014 aih can't safely edit these (its entries carry no on-disk marker,","and names may collide with yours), so it will NOT touch them:",...t.map(n=>` [manual] ${n.path} \u2014 remove ${n.clis.join(", ")}'s entries by hand if unused`)]}s(u6,"advisoryLines");function m6(e){return e.unrunnable.length===0?[]:["",`!! --unrunnable: treating ${e.unrunnable.join(", ")} as dropped because no binary is`,"on PATH. A PATH problem (fresh shell, VDI, not-yet-installed tool) looks IDENTICAL","to a truly dropped CLI \u2014 prune these only if you actually stopped using them.","Targets in .aih-config.json are unchanged; re-target via `aih bootstrap-ai`."]}s(m6,"unrunnableLines");function f6(e,t,n,r){if(e.source==="none")return g("No committed target set (.aih-config.json) to diff against \u2014 nothing is treated","as stale. Run `aih bootstrap-ai` to record which CLIs this repo targets, then","`aih prune` will remove artifacts for any CLI you later drop.");if(e.unknownTargets.length>0)return g(`!! ${vn} lists target(s) aih does not recognize: ${e.unknownTargets.join(", ")}.`,"A typo here could make prune treat a CLI you MEANT TO KEEP as dropped, so nothing","is treated as stale until the marker is fixed. Valid targets: see `aih prune --help`;","re-write the marker via `aih bootstrap-ai --apply --cli <list>`.");if(e.dropped.length===0)return g("No stale per-CLI artifacts \u2014 every CLI wired into this repo is still targeted.",`Kept (${ck[e.source]}): ${e.targeted.join(", ")||"none"}.`);let o=r?`${t} file(s) hard-delete (single-slot <path>.aih.bak backup), ${n} bootloader block(s)`:`${t} file(s) move to .aih/legacy/ (reversible), ${n} bootloader block(s)`;return g(`Kept (${ck[e.source]}): ${e.targeted.join(", ")||"none"}`,`Dropped: ${e.dropped.join(", ")}`,...m6(e),"",o,"subtracted in place; the actions above list each. Pass --apply to execute.",...u6(e))}s(f6,"contextBody");function g6(e,t,n){let r=`${t.clis.join(", ")} dropped`;if(t.disposition==="file")return Ut(t.path,`stale ${d6[t.kind]} (${r})`,{hardDelete:n});if(t.disposition==="block"){let o=p6(e,t.path);return o===void 0?void 0:P(t.path,o,`subtract aih canon block from ${t.path} (${r})`)}}s(g6,"actionFor");async function h6(e){let t=e.options.unrunnable===!0?await Hm(e):void 0,n=e.options.delete===!0,r=Xs(e,{treatAsDropped:t}),o=[];r.artifacts.some(c=>c.disposition==="file")&&o.push(le(e.root));let a=0,i=0;for(let c of r.artifacts){let d=g6(e,c,n);d&&(o.push(d),d.kind==="remove"?a+=1:d.kind==="write"&&(i+=1))}let l=r.dropped.length>0?`Stale artifacts \u2014 ${r.artifacts.length} for ${r.dropped.length} dropped CLI(s)`:"Stale artifacts \u2014 none";return o.push(C(l,f6(r,a,i,n),r)),S("prune",...o)}s(h6,"prunePlan");var lk={name:"prune",summary:"Remove stale per-CLI artifacts left by CLIs this repo no longer targets",options:[{flags:"--delete",description:"hard-delete stale files (single-slot <path>.aih.bak backup) instead of the reversible .aih/legacy/ move"},{flags:"--unrunnable",description:"ALSO treat targeted CLIs with no binary on PATH as prunable (loud opt-in \u2014 a PATH problem looks identical to a dropped CLI)"}],plan:h6};function v6(e){return e?`Your first command: ${e} (declared in the repo \u2014 this runs the real work; aih stops here)`:"No runnable command declared \u2014 see setup.md before starting."}s(v6,"handoffLine");function y6(e){let t="readiness \u2014 no blockers";return e.blockers.length>0?{name:t,verdict:"fail",detail:`${e.blockers.length} blocker(s): ${e.blockers.map(n=>n.id).join(", ")}`,code:"ready.blocked"}:{name:t,verdict:"pass",detail:"no blockers \u2014 an agent can start here"}}s(y6,"gateCheck");function b6(e){let t=new Set(e);return kl.filter(n=>t.has(n.bin))}s(b6,"specsForMissing");async function k6(e,t){if(e.apply)return!0;if(!e.prompter)return!1;let r=(await e.prompter.ask(`Install ${t.join(", ")} now? [y/N]: `)).trim().toLowerCase();return r==="y"||r==="yes"}s(k6,"shouldInstall");async function w6(e,t){let n=await ja(e),r=Na(e,t,n);for(let o of t)r.push(R(`${o.tool} installed`,a=>$a(a,o,n)));return r.push(I("core tools \u2014 re-run to confirm","Installed the missing core shell tools. Re-run `aih ready` to confirm the readiness gate now passes.")),r}s(w6,"coreToolInstallActions");async function x6(e){let t=await td(e),n=`${nd(t)}
3047
3047
  ${v6(t.firstCommand)}
3048
- `,r={banner:t.banner,blockers:t.blockers,warns:t.warns,score:t.score,rawScore:t.rawScore,grade:t.grade,firstCommand:t.firstCommand},o=[C("Developer readiness",n,r),R("readiness \u2014 no blockers",()=>y6(t))],a=await ed(e);return a.length>0&&await k6(e,a)&&(e.apply=!0,o.push(...await w6(e,b6(a)))),S("ready",...o)}s(x6,"readyPlan");var dk={name:"ready",summary:"Readiness gate \u2014 can a developer start work with an AI agent here? (graded, blocker-aware)",alwaysVerify:!0,wantsInstallPrompt:!0,plan:x6};function C6(e,t){let n=t.size>0?[...t].sort().join(", "):"none detected";return g(`Missing agent shell tools: ${e.map(r=>r.bin).join(", ")}.`,`Package managers available: ${n}.`,"","Dry-run shows the exact install command per tool; `aih tools --apply` runs them","(local execs). A blocked install (no package manager, no admin, locked registry)","becomes a support ticket \u2014 see `--support-out <dir>` to save it.")}s(C6,"summaryText");async function S6(e){let t=await ja(e),n=await iv(e);if(n.length===0)return S("tools",I("tools \u2014 all present","All agent shell tools are on PATH. Nothing to install."));let r=Na(e,n,t);for(let o of n)r.push(R(`${o.tool} installed`,a=>$a(a,o,t)));return r.push(I("tools summary",C6(n,t))),S("tools",...r)}s(S6,"toolsPlan");var pk={name:"tools",summary:"Install the agent shell tools the harness leans on (rg/fd/jq + ast-grep/comby/tree/gh/code-review-graph); escalates a blocked install as a ticket",alwaysVerify:!0,options:[],plan:S6};async function A6(e){let t=await Tg(e);if(!t)return S("track",C("track \u2014 not a git repository",g("`aih track` samples git + repo state, but the target root is not a git repo.","Run it inside a repository (or from a commit hook)."),{recorded:!1}));let n=g(`commit ${t.sha} (${t.ts})`,` branch ${t.branch} \xB7 ${t.branches} local branch(es) \xB7 ${t.sourceFiles} tracked files`,` commits(7d) ${t.commits7d} \xB7 LOC +${t.loc.added}/-${t.loc.removed} (net ${t.loc.net})`,` adoption ${t.adoptionScore}/100 \xB7 context ~${t.contextTokens} tokens`);return S("track",C(`track \u2014 sample for ${t.sha} (appended under --apply)`,n,t),Lg(e,t),le(e.root))}s(A6,"trackPlan");var uk={name:"track",summary:"Record a metrics sample (git + repo state) to .aih/history.jsonl \u2014 powers `aih report` trends",plan:A6};import{existsSync as H6}from"fs";import{basename as U6,join as G6,posix as Yo}from"path";import{existsSync as Ld,readdirSync as P6,statSync as mk}from"fs";import{join as Jo}from"path";function R6(e){let t=e.trim().replace(/\\/g,"/");if(t.length===0)return"";if(t.startsWith("/")||/^[A-Za-z]:/.test(t)||t.startsWith("//"))throw new E(`workspace repo path must be relative to the parent: ${e}`,"AIH_WORKSPACE");let n=t.split("/").filter(r=>r.length>0);if(n.some(r=>r==="."||r===".."))throw new E(`workspace repo path must not traverse parents: ${e}`,"AIH_WORKSPACE");return n.join("/")}s(R6,"normalizeRepoPath");function E6(e,t){let n=Jo(e,t);try{return mk(n).isDirectory()&&Ld(Jo(n,".git"))}catch{return!1}}s(E6,"isGitRepo");function fk(e,t=[]){if(t.length>0){let r=[...new Set(t.map(R6).filter(i=>i.length>0))],o=r.filter(i=>!Ld(Jo(e,i)));if(o.length>0)throw new E(`workspace --repos entries do not exist under the parent: ${o.join(", ")}`,"AIH_WORKSPACE");let a=r.filter(i=>!E6(e,i));if(a.length>0)throw new E(`workspace --repos entries are not git repos (missing .git): ${a.join(", ")}`,"AIH_WORKSPACE");return r}let n;try{n=P6(e)}catch{return[]}return n.filter(r=>!r.startsWith(".")).filter(r=>{let o=Jo(e,r);try{return mk(o).isDirectory()&&Ld(Jo(o,".git"))}catch{return!1}}).sort()}s(fk,"detectChildRepos");function gk(e){return typeof e!="string"||e.trim().length===0?[]:e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}s(gk,"reposOption");import{posix as hk}from"path";function T6(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t.length>0?t:"workspace"}s(T6,"slugify");function I6(e=new Date){return e.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}Z$/,"Z")}s(I6,"timestampSlug");async function L6(e){let t=gt(e.root,e.contextDir);if(!t)throw new E("workspace snapshot requires .aih-workspace.json","AIH_WORKSPACE");let n=typeof e.options.label=="string"?e.options.label.trim():"",r=await Dy(e,t,{...n.length>0?{label:n}:{}}),o=n.length>0?`-${T6(n)}`:"",a=hk.join(".aih","workspace-snapshots",`${I6()}${o}.json`),i=[L(a,r,`workspace child repo snapshot \u2192 ${a.replace(/\\/g,"/")}`),le(e.root)];return e.options.lock===!0&&i.push(L(hk.join(t.contextDir,"workspace-lock.json"),r,"shared known-good workspace lock")),S("workspace snapshot",...i)}s(L6,"workspaceSnapshotPlan");var Or={name:"snapshot",summary:"Record the current child repo branch/SHA set for a federated workspace",options:[{flags:"--label <label>",description:"human label for the snapshot filename/body"},{flags:"--lock",description:"also write the shared known-good lock under the workspace context dir"}],plan:L6};import{posix as D6}from"path";var vk={api:10,backend:10,service:10,worker:20,shared:30,frontend:40,ui:40,infra:50,docs:60};function M6(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t.length>0?t:"workspace-task"}s(M6,"slugify");function O6(e=new Date){return e.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}Z$/,"Z")}s(O6,"timestampSlug");function yk(e){let t=e.kind?.toLowerCase();if(t&&vk[t]!==void 0)return vk[t];let n=e.id.toLowerCase();return n.includes("backend")||n.includes("api")?10:n.includes("worker")?20:n.includes("shared")?30:n.includes("ui")||n.includes("web")||n.includes("front")?40:n.includes("infra")?50:100}s(yk,"repoOrder");function bk(e){return[...e].sort((t,n)=>yk(t)-yk(n)||t.id.localeCompare(n.id))}s(bk,"orderedRepos");function j6(e){return e.length===0?["- _No explicit workspace contract edges declared yet._"]:e.map(t=>{let n=[t.contractPath?`contract: ${t.contractPath}`:void 0,t.consumerPath?`consumer: ${t.consumerPath}`:void 0].filter(r=>r!==void 0);return`- ${t.id} (${t.from} -> ${t.to}, ${t.kind})${n.length>0?` - ${n.join("; ")}`:""}`})}s(j6,"affectedContracts");function N6(e,t,n){let r=bk(t);return g("# Workspace Plan","",`Task: ${e}`,"","## Repos Touched","",...t.length>0?t.map(o=>`- ${o.id}`):["- _none declared_"],"","## Read Order","",...r.length>0?r.map((o,a)=>`${a+1}. ${o.path}/${o.router}`):["1. _Add repos to `.aih-workspace.json` first._"],"","## Contracts Affected","",j6(n),"","## Implementation Order","","1. Read the affected child routers.","2. Update explicit contract files first.","3. Implement producer-side changes.","4. Implement consumer-side changes.","5. Update deployment or infrastructure wiring if needed.","","## Test Order","","1. Run producer repo unit and contract tests.","2. Run consumer repo unit and integration tests.","3. Run cross-repo end-to-end verification.","4. Run `aih report --workspace` from the parent workspace.","","## Rollback","",...bk(t).map(o=>`- ${o.id} commit:`))}s(N6,"taskPlanDoc");async function $6(e){let t=gt(e.root,e.contextDir);if(!t)throw new E("workspace plan requires .aih-workspace.json","AIH_WORKSPACE");let n=typeof e.options.task=="string"&&e.options.task.trim().length>0?e.options.task.trim():void 0;if(!n)throw new E("workspace plan requires a task description","AIH_WORKSPACE");let r=D6.join(".aih","workspace-plans",`${O6()}-${M6(n)}.md`);return S("workspace plan",P(r,N6(n,t.repos,t.edges),`workspace task plan \u2192 ${r}`),le(e.root))}s($6,"workspaceTaskPlan");var jr={name:"plan",summary:"Create a federated multi-repo task plan",plan:$6};function kk(e,t,n=!1){return{workspaceType:"multi-repo",graphScope:"combined-child-repos",contextDir:t,repos:e,...n?{git:!0}:{},generatedBy:"aih workspace"}}s(kk,"workspaceMarker");function wk(e){return{folders:e.map(t=>({path:t})),settings:{}}}s(wk,"codeWorkspace");function xk(e,t){let n=t&&t.length>0?`@modelcontextprotocol/server-filesystem@${t}`:"@modelcontextprotocol/server-filesystem";return{mcpServers:{"code-review-graph":{command:"uvx",args:["code-review-graph@2.3.6","serve"]},filesystem:{command:"npx",args:["-y",n,...e]}}}}s(xk,"spanningMcp");function F6(e){return e==="."?"./":`${e}/`}s(F6,"repoDisplayPath");function B6(e){return`${e.path}/${e.router}`}s(B6,"childRouterPath");function Ck(e){let t=e.length>0?e.map(n=>`| ${n.id} | ${F6(n.path)} | ${n.kind??""} | ${B6(n)} |`):["| _none_ | _none_ | | _run `aih workspace --repos ... --apply`_ |"];return g("# Workspace Router","","This is a federated workspace, not a monorepo.","","## Repos","","| Repo | Path | Role | Router |","|---|---|---|---|",t,"","## Rule","","Before editing a child repo, read that child repo's router first.")}s(Ck,"workspaceRouterDoc");function Sk(e){let t=e.length>0?e.map(n=>`| ${n.id} | ${n.from} | ${n.to} | ${n.kind} | ${n.contractPath??""} | ${n.consumerPath??""} |`):["| _none declared_ | | | | | |","","Declare cross-repo dependencies in `.aih-workspace.json` under `edges[]`."];return g("# Workspace Contracts","","These are explicit cross-repo dependencies for the parent coordination plane.","No child files are modified by this workspace contract document.","","| Contract | From | To | Kind | Contract file | Consumer path |","|---|---|---|---|---|---|",t)}s(Sk,"workspaceContractsDoc");function Ak(e,t,n){let r=t.flatMap(c=>[`### ${c}`,"","- **Owns:** _what this repo is responsible for._",`- **Canon:** \`${c}/${n}/RULE_ROUTER.md\``,"- **Entry points:** _main handlers / routes / packages._",""]),o=t.length>0?t:["repo-a","repo-b"],a=`| Feature | ${o.join(" | ")} | Contract (API / event / schema) |`,i=`| --- | ${o.map(()=>"---").join(" | ")} | --- |`,l=`| _Login_ | ${o.map(()=>"_\u2026_").join(" | ")} | _\`LoginRequest\`/\`LoginResponse\`_ |`;return g("# Cross-repo architecture","",`> Workspace canon for \`${e}\`. **WRITE-ONCE** \u2014 aih seeded this with your repo`,"> list; you own it from here. Re-running `aih workspace` will NOT overwrite it.","","## System overview","",`_How ${t.length>0?t.join(" + "):"the repos"} fit together: request flow, data`,"flow, auth, async/events. Sketch the high-level shape in 3\u20135 sentences or a diagram._","","## Repositories","",r,"## Cross-repo feature map","","One row per feature that spans repos \u2014 the contract is the source of truth.","",a,i,l,"","## Blast-radius protocol","","When an agent is asked to change a product behavior:","","1. Start at this workspace map and identify every repo touched by the feature.","2. Use the workspace graph/filesystem MCP to inspect cross-repo call sites, imports,"," API contracts, infra bindings, runbooks, and docs before editing.","3. Before writing code in any repo, read that repo's own canon and validation flow.","4. Make source changes inside child repos, not in the parent workspace root.","5. Run each affected repo's local validation, then update this feature map when a"," cross-repo contract changes.","","## Change patterns","","- **Add a cross-repo feature:** define/version the contract first, implement the"," backend, then the UI; add or refresh the feature-map row.","- **Debug a data flow:** trace UI action \u2192 contract \u2192 backend handler \u2192 datastore;"," check each repo's canon for its own conventions.","- **Change a contract:** it is a breaking change across repos \u2014 update both sides"," in lockstep and bump the contract version.")}s(Ak,"crossRepoArchitectureDoc");function Pk(e,t){let n=e.length>0?e.map(r=>`- **${r}** \u2192 read \`${r}/${t}/RULE_ROUTER.md\` (+ \`${r}/${t}/conventions.md\`) before writing code in \`${r}/\`.`):["- _Add repos to the workspace, then re-run `aih workspace`._"];return g("# Repo discipline (workspace)","","This parent folder is the coordination plane, not a source repo. Use it to reason","about blast radius across UI/backend/infra/docs, then do implementation work inside","the affected child repos. Conventions differ per repo \u2014 before editing a repo, load","THAT repo's canon first.","",n,"",`For any change that crosses repos, consult \`${t}/cross-repo-architecture.md\` for`,"the contract, inspect the combined workspace graph, update every affected repo in","lockstep, and keep the feature map current. Never assume one repo's conventions","apply to another.")}s(Pk,"repoDisciplineDoc");function Dd(e,t,n,r){return g(`# ${t} \u2014 workspace (${e})`,"","This is a multi-repo workspace, not a single repo. Start here:","",`- \`${r}/workspace-router.md\` \u2014 top-level routing table into each child repo's canon.`,`- \`${r}/cross-repo-architecture.md\` \u2014 how the repos fit together + the cross-repo feature map.`,`- \`${r}/workspace-contracts.md\` \u2014 declared cross-repo contract edges.`,`- \`${r}/repo-discipline.md\` \u2014 read a repo's own canon before editing it.`,"- Workspace MCP graph/filesystem \u2014 use for blast-radius discovery across all child repos.","",`Repos: ${n.length>0?n.join(", "):"(none detected yet)"}. Each has its own canon`,`under \`<repo>/${r}/\` (run \`aih init\` in each). Edit workspace guidance in \`${r}/\`, not here.`)}s(Dd,"workspaceBootloader");function Rk(e,t,n,r=!1){let o=t.length>0?t.map(i=>` aih init ./${i} --apply`):[" # (no child repos detected \u2014 clone repos under this folder, then re-run)"],a=r?["- Remote setup is user/team-owned: `aih workspace --git` creates only the local bridge repo; add an origin later if and where you choose."]:[];return g(`Workspace scaffolded for \`${e}\` (parent-only). Detected repos: ${t.length>0?t.join(", "):"none"}.`,"","Lay down each repo's canon (run from the workspace root):","",o,"",`- Fill in \`${n}/cross-repo-architecture.md\` \u2014 it is write-once; aih won't overwrite it.`,`- Open \`${e}.code-workspace\` in VS Code (all repos in one window).`,"- Use the parent `.mcp.json` graph/filesystem servers for cross-repo blast-radius analysis.",...a,"- Validate: `aih doctor` at the workspace root checks each child is scaffolded.")}s(Rk,"nextStepsDoc");function z6(e){return typeof e=="object"&&e!==null&&Array.isArray(e.repos)&&e.repos.some(t=>typeof t=="object"&&t!==null)}s(z6,"hasObjectRepos");function W6(e,t){return R(`child ${e} scaffolded`,n=>{let r=`child ${e} scaffolded`;return H6(G6(n.root,e,t,"RULE_ROUTER.md"))?{name:r,verdict:"pass",detail:`${e}/${t}/ canon present`}:{name:r,verdict:"skip",detail:`not scaffolded \u2014 run \`aih init ./${e} --apply\``}})}s(W6,"childScaffoldedProbe");async function V6(e){let t=e.contextDir,n=U6(e.root)||"workspace",r=fk(e.root,gk(e.options.repos)),o=e.options.git===!0,a=gt(e.root,t),i=a&&a.repos.length>0?a.repos:Sf(r,Yo.join(t,"RULE_ROUTER.md")),l=a?.edges??[],c=[L(".aih-workspace.json",z6(a?.raw)?{...a.raw,...o?{git:!0}:{}}:kk(r,t,o),`workspace marker (multi-repo: ${r.length>0?r.join(", "):"no repos detected"})`,{merge:!0}),L(`${n}.code-workspace`,wk(r),"VS Code multi-root workspace",{merge:!0}),P(Yo.join(t,"workspace-router.md"),Ck(i),"workspace router (federated child repo table of contents)"),P(Yo.join(t,"workspace-contracts.md"),Sk(l),"workspace contracts (parent-owned cross-repo dependency index)"),P(Yo.join(t,"cross-repo-architecture.md"),Ak(n,r,t),"cross-repo architecture + feature map (write-once \u2014 you own it)",{once:!0}),P(Yo.join(t,"repo-discipline.md"),Pk(r,t),"per-repo discipline routing (read a repo's canon before editing it)"),P("CLAUDE.md",Dd("Claude workspace",n,r,t),"Claude workspace bootloader \u2192 cross-repo canon"),P("AGENTS.md",Dd("agent workspace",n,r,t),"AGENTS.md workspace bootloader (Codex/Kiro/\u2026 ) \u2192 cross-repo canon"),L(".mcp.json",xk(r,(e.env.AIH_MCP_FS_VERSION??"").trim()||void 0),`combined graph + filesystem MCP spanning ${r.length} child repo(s), merged into any existing .mcp.json`,{merge:!0})];o&&c.push(yf(e.root,r));let d=[...c,I("workspace next steps (run `aih init` per child)",Rk(n,r,t,o)),...o?await bf(e,c):[]];for(let p of r)d.push(W6(p,t));return S("workspace",...d)}s(V6,"workspacePlan");var Ek={name:"workspace",summary:"Scaffold a multi-repo workspace: cross-repo map, combined graph/filesystem MCP, .code-workspace (parent-only)",options:[{flags:"--repos <list>",description:"child repos (comma-separated); else auto-detect *//.git"},{flags:"--git",description:"initialize a local git repo for workspace coordination files; remote setup remains user-owned"}],plan:V6};import{randomUUID as t$}from"crypto";import{basename as n$,join as r$}from"path";import{createInterface as q6}from"readline";function Tk(e=process.env){return e.AIH_NO_PROMPT==="1"?!1:!!(process.stdin.isTTY&&process.stdout.isTTY)}s(Tk,"isInteractive");function Ik(){return{ask(e){let t=q6({input:process.stdin,output:process.stdout});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}}}s(Ik,"makeReadlinePrompter");import{isAbsolute as Y6}from"path";import{Command as K6}from"commander";var Md="1.0.0";function J6(e=[],t){let n=new K6;return n.name("aih").description("Enterprise AI Bootstrapping Harness \u2014 governed, proxy-safe AI coding setup").version(Md).showHelpAfterError("(add --help for usage)"),Dk(n,e,t),n}s(J6,"buildProgram");function Doe(e){let t=e[2];return t==="--version"||t==="-V"}s(Doe,"isVersionFastPath");async function Moe(){let{commands:e,warnings:t}=await fp(Lk());return{program:J6(e,t),warnings:t}}s(Moe,"buildProgramWithPlugins");var Z6="https://json.schemastore.org/sarif-2.1.0.json",X6="https://github.com/samartomar/ai-harness";function Q6(e){return e==="fail"?"error":"note"}s(Q6,"level");function _6(e){let t=e.replace(/\\/g,"/");if(!(t.length===0||Y6(t)||/^[A-Za-z]:/.test(t))&&!t.split("/").some(n=>n===".."))return t}s(_6,"safeArtifactUri");function e$(e){if(!e.location)return[];let t=_6(e.location.uri);if(t===void 0)return[];let n={artifactLocation:{uri:t}};return e.location.startLine!==void 0&&(n.region={startLine:e.location.startLine}),[{physicalLocation:n}]}s(e$,"locations");function Mk(e){return e.fingerprint?{"aih/v1":e.fingerprint}:void 0}s(Mk,"fingerprints");function Ok(e,t="aih"){let r=[...new Set(e.checks.map(i=>i.name))].map(i=>({id:i,name:i,shortDescription:{text:i},defaultConfiguration:{level:"warning"}})),o=e.checks.map(i=>({ruleId:i.name,level:Q6(i.verdict),message:{text:i.detail??i.name},locations:e$(i),...Mk(i)?{partialFingerprints:Mk(i)}:{}}));return JSON.stringify({$schema:Z6,version:"2.1.0",runs:[{tool:{driver:{name:t,informationUri:X6,version:Md,rules:r}},results:o}]},null,2)}s(Ok,"reportToSarif");var o$=s(e=>new Promise(t=>setTimeout(t,e)),"delay"),s$=["SETUP.md","docs/SETUP.md",".aih/SETUP.md"];function a$(e){for(let t of s$){let n=k(r$(e,t));if(n!==void 0)return n}}s(a$,"readSetupText");function i$(e){let t=Number(e);return Number.isFinite(t)&&t>0?Math.floor(t):void 0}s(i$,"positiveInt");function c$(e){let n=(e.split(/[,\s]+/).find(l=>l.startsWith("--"))??e).replace(/^--/,""),r=n.indexOf("<"),o=n.indexOf("["),a=Math.min(r===-1?n.length:r,o===-1?n.length:o);return n.slice(0,a).trim().replace(/-([a-z])/g,(l,c)=>c.toUpperCase())}s(c$,"flagKey");function l$(e,t){let n={};for(let r of e.options??[]){let o=c$(r.flags);t[o]!==void 0&&(n[o]=t[o])}return n}s(l$,"extractOptions");async function ke(e,t,n={}){let r=n.env??process.env,o=n.write??(x=>process.stdout.write(x)),a=n.run??Wt,i=t.optsWithGlobals(),l=Array.isArray(t.processedArgs)?t.processedArgs[0]:void 0,c=n.positionalRoot===!1?void 0:n.positionalRoot??l,d=n.now??(()=>new Date),p=d(),u=(n.newRunId??(()=>`run_${t$().slice(0,8)}`))(),m=al(n.argv??process.argv.slice(2)).map(x=>cn(x,r)),f=c??i.root??r.AIH_ROOT??process.cwd(),v=i.log===!1,y=s(x=>{Nf(f,r,{noLog:v})&&$f(f,jf(x),p)},"logRun"),A=!1;try{let x=c??i.root??r.AIH_ROOT??process.cwd(),h=G(x),w=jt(t,"contextDir")==="cli"?i.contextDir:void 0,T=w===void 0?h?.contextDir:void 0,F=jt(t,"posture")==="cli"?"cli":void 0,_=or({root:x,env:r,flag:i.posture,flagSource:F,marker:h}),M=Wn(r,{apply:i.apply,verify:i.verify,json:i.json,contextDir:w??T,root:x,caPattern:i.caPattern});A=M.json;let we=Yn({run:a,env:r}),Je=(i.detect===!0||e.wantsInstallPrompt===!0)&&i.yes!==!0&&!M.json,Se=n.prompter??(Je&&Tk(r)?Ik():void 0),Zo=i$(i.refresh),Od=i.open===!0||i.demo===!0||Zo!==void 0,Ye={root:M.root,contextDir:M.contextDir,posture:_.posture,postureSource:_.postureSource,apply:e.readOnly?!1:M.apply||Od,verify:e.readOnly||e.alwaysVerify||typeof i.sarif=="string"?!0:M.verify,json:M.json,run:a,host:we,env:r,prompter:Se,options:{...l$(e,i),...n.optionOverrides??{},caPattern:M.caPattern,cli:i.cli,allTools:i.allTools,detect:i.detect,force:i.force,open:Od?!0:i.open}},$k=await e.plan(Ye),Re=await Ze($k,Ye,{skipWorktreeGate:e.skipWorktreeGate===!0}),jd=Re.execs.some(xe=>xe.ran&&xe.ok===!1),Nd=Re.report?Re.report.exitCode():0,$d=Nd||(jd?1:0),it=Re.report&&Re.report.checks.length>0?Ar({capability:Re.capability,checks:Re.report.checks,projectName:n$(M.root)||"this project",root:M.root,command:al(["aih",e.name,...Ye.verify?["--verify"]:[],...Ye.apply?["--apply"]:[]]).join(" "),contextDir:M.contextDir,targets:(G(M.root)?.targets??[]).join(", ")||"none",platform:we.platform,runId:u,timestamp:p.toISOString(),setupText:a$(M.root),env:r}):void 0,vi,yi=i.supportOut;if(it&&typeof yi=="string"&&yi.length>0){vi={};for(let xe of it.templates){let Xo=`${yi}/${xe.code.replace(/[^a-z0-9.-]/gi,"_")}.md`;vt(Ye,Xo,`${xe.subject}
3048
+ `,r={banner:t.banner,blockers:t.blockers,warns:t.warns,score:t.score,rawScore:t.rawScore,grade:t.grade,firstCommand:t.firstCommand},o=[C("Developer readiness",n,r),R("readiness \u2014 no blockers",()=>y6(t))],a=await ed(e);return a.length>0&&await k6(e,a)&&(e.apply=!0,o.push(...await w6(e,b6(a)))),S("ready",...o)}s(x6,"readyPlan");var dk={name:"ready",summary:"Readiness gate \u2014 can a developer start work with an AI agent here? (graded, blocker-aware)",alwaysVerify:!0,wantsInstallPrompt:!0,plan:x6};function C6(e,t){let n=t.size>0?[...t].sort().join(", "):"none detected";return g(`Missing agent shell tools: ${e.map(r=>r.bin).join(", ")}.`,`Package managers available: ${n}.`,"","Dry-run shows the exact install command per tool; `aih tools --apply` runs them","(local execs). A blocked install (no package manager, no admin, locked registry)","becomes a support ticket \u2014 see `--support-out <dir>` to save it.")}s(C6,"summaryText");async function S6(e){let t=await ja(e),n=await iv(e);if(n.length===0)return S("tools",I("tools \u2014 all present","All agent shell tools are on PATH. Nothing to install."));let r=Na(e,n,t);for(let o of n)r.push(R(`${o.tool} installed`,a=>$a(a,o,t)));return r.push(I("tools summary",C6(n,t))),S("tools",...r)}s(S6,"toolsPlan");var pk={name:"tools",summary:"Install the agent shell tools the harness leans on (rg/fd/jq + ast-grep/comby/tree/gh/code-review-graph); escalates a blocked install as a ticket",alwaysVerify:!0,options:[],plan:S6};async function A6(e){let t=await Tg(e);if(!t)return S("track",C("track \u2014 not a git repository",g("`aih track` samples git + repo state, but the target root is not a git repo.","Run it inside a repository (or from a commit hook)."),{recorded:!1}));let n=g(`commit ${t.sha} (${t.ts})`,` branch ${t.branch} \xB7 ${t.branches} local branch(es) \xB7 ${t.sourceFiles} tracked files`,` commits(7d) ${t.commits7d} \xB7 LOC +${t.loc.added}/-${t.loc.removed} (net ${t.loc.net})`,` adoption ${t.adoptionScore}/100 \xB7 context ~${t.contextTokens} tokens`);return S("track",C(`track \u2014 sample for ${t.sha} (appended under --apply)`,n,t),Lg(e,t),le(e.root))}s(A6,"trackPlan");var uk={name:"track",summary:"Record a metrics sample (git + repo state) to .aih/history.jsonl \u2014 powers `aih report` trends",plan:A6};import{existsSync as H6}from"fs";import{basename as U6,join as G6,posix as Yo}from"path";import{existsSync as Ld,readdirSync as P6,statSync as mk}from"fs";import{join as Jo}from"path";function R6(e){let t=e.trim().replace(/\\/g,"/");if(t.length===0)return"";if(t.startsWith("/")||/^[A-Za-z]:/.test(t)||t.startsWith("//"))throw new E(`workspace repo path must be relative to the parent: ${e}`,"AIH_WORKSPACE");let n=t.split("/").filter(r=>r.length>0);if(n.some(r=>r==="."||r===".."))throw new E(`workspace repo path must not traverse parents: ${e}`,"AIH_WORKSPACE");return n.join("/")}s(R6,"normalizeRepoPath");function E6(e,t){let n=Jo(e,t);try{return mk(n).isDirectory()&&Ld(Jo(n,".git"))}catch{return!1}}s(E6,"isGitRepo");function fk(e,t=[]){if(t.length>0){let r=[...new Set(t.map(R6).filter(i=>i.length>0))],o=r.filter(i=>!Ld(Jo(e,i)));if(o.length>0)throw new E(`workspace --repos entries do not exist under the parent: ${o.join(", ")}`,"AIH_WORKSPACE");let a=r.filter(i=>!E6(e,i));if(a.length>0)throw new E(`workspace --repos entries are not git repos (missing .git): ${a.join(", ")}`,"AIH_WORKSPACE");return r}let n;try{n=P6(e)}catch{return[]}return n.filter(r=>!r.startsWith(".")).filter(r=>{let o=Jo(e,r);try{return mk(o).isDirectory()&&Ld(Jo(o,".git"))}catch{return!1}}).sort()}s(fk,"detectChildRepos");function gk(e){return typeof e!="string"||e.trim().length===0?[]:e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}s(gk,"reposOption");import{posix as hk}from"path";function T6(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t.length>0?t:"workspace"}s(T6,"slugify");function I6(e=new Date){return e.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}Z$/,"Z")}s(I6,"timestampSlug");async function L6(e){let t=gt(e.root,e.contextDir);if(!t)throw new E("workspace snapshot requires .aih-workspace.json","AIH_WORKSPACE");let n=typeof e.options.label=="string"?e.options.label.trim():"",r=await Dy(e,t,{...n.length>0?{label:n}:{}}),o=n.length>0?`-${T6(n)}`:"",a=hk.join(".aih","workspace-snapshots",`${I6()}${o}.json`),i=[L(a,r,`workspace child repo snapshot \u2192 ${a.replace(/\\/g,"/")}`),le(e.root)];return e.options.lock===!0&&i.push(L(hk.join(t.contextDir,"workspace-lock.json"),r,"shared known-good workspace lock")),S("workspace snapshot",...i)}s(L6,"workspaceSnapshotPlan");var Or={name:"snapshot",summary:"Record the current child repo branch/SHA set for a federated workspace",options:[{flags:"--label <label>",description:"human label for the snapshot filename/body"},{flags:"--lock",description:"also write the shared known-good lock under the workspace context dir"}],plan:L6};import{posix as D6}from"path";var vk={api:10,backend:10,service:10,worker:20,shared:30,frontend:40,ui:40,infra:50,docs:60};function M6(e){let t=e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"");return t.length>0?t:"workspace-task"}s(M6,"slugify");function O6(e=new Date){return e.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}Z$/,"Z")}s(O6,"timestampSlug");function yk(e){let t=e.kind?.toLowerCase();if(t&&vk[t]!==void 0)return vk[t];let n=e.id.toLowerCase();return n.includes("backend")||n.includes("api")?10:n.includes("worker")?20:n.includes("shared")?30:n.includes("ui")||n.includes("web")||n.includes("front")?40:n.includes("infra")?50:100}s(yk,"repoOrder");function bk(e){return[...e].sort((t,n)=>yk(t)-yk(n)||t.id.localeCompare(n.id))}s(bk,"orderedRepos");function j6(e){return e.length===0?["- _No explicit workspace contract edges declared yet._"]:e.map(t=>{let n=[t.contractPath?`contract: ${t.contractPath}`:void 0,t.consumerPath?`consumer: ${t.consumerPath}`:void 0].filter(r=>r!==void 0);return`- ${t.id} (${t.from} -> ${t.to}, ${t.kind})${n.length>0?` - ${n.join("; ")}`:""}`})}s(j6,"affectedContracts");function N6(e,t,n){let r=bk(t);return g("# Workspace Plan","",`Task: ${e}`,"","## Repos Touched","",...t.length>0?t.map(o=>`- ${o.id}`):["- _none declared_"],"","## Read Order","",...r.length>0?r.map((o,a)=>`${a+1}. ${o.path}/${o.router}`):["1. _Add repos to `.aih-workspace.json` first._"],"","## Contracts Affected","",j6(n),"","## Implementation Order","","1. Read the affected child routers.","2. Update explicit contract files first.","3. Implement producer-side changes.","4. Implement consumer-side changes.","5. Update deployment or infrastructure wiring if needed.","","## Test Order","","1. Run producer repo unit and contract tests.","2. Run consumer repo unit and integration tests.","3. Run cross-repo end-to-end verification.","4. Run `aih report --workspace` from the parent workspace.","","## Rollback","",...bk(t).map(o=>`- ${o.id} commit:`))}s(N6,"taskPlanDoc");async function $6(e){let t=gt(e.root,e.contextDir);if(!t)throw new E("workspace plan requires .aih-workspace.json","AIH_WORKSPACE");let n=typeof e.options.task=="string"&&e.options.task.trim().length>0?e.options.task.trim():void 0;if(!n)throw new E("workspace plan requires a task description","AIH_WORKSPACE");let r=D6.join(".aih","workspace-plans",`${O6()}-${M6(n)}.md`);return S("workspace plan",P(r,N6(n,t.repos,t.edges),`workspace task plan \u2192 ${r}`),le(e.root))}s($6,"workspaceTaskPlan");var jr={name:"plan",summary:"Create a federated multi-repo task plan",plan:$6};function kk(e,t,n=!1){return{workspaceType:"multi-repo",graphScope:"combined-child-repos",contextDir:t,repos:e,...n?{git:!0}:{},generatedBy:"aih workspace"}}s(kk,"workspaceMarker");function wk(e){return{folders:e.map(t=>({path:t})),settings:{}}}s(wk,"codeWorkspace");function xk(e,t){let n=t&&t.length>0?`@modelcontextprotocol/server-filesystem@${t}`:"@modelcontextprotocol/server-filesystem";return{mcpServers:{"code-review-graph":{command:"uvx",args:["code-review-graph@2.3.6","serve"]},filesystem:{command:"npx",args:["-y",n,...e]}}}}s(xk,"spanningMcp");function F6(e){return e==="."?"./":`${e}/`}s(F6,"repoDisplayPath");function B6(e){return`${e.path}/${e.router}`}s(B6,"childRouterPath");function Ck(e){let t=e.length>0?e.map(n=>`| ${n.id} | ${F6(n.path)} | ${n.kind??""} | ${B6(n)} |`):["| _none_ | _none_ | | _run `aih workspace --repos ... --apply`_ |"];return g("# Workspace Router","","This is a federated workspace, not a monorepo.","","## Repos","","| Repo | Path | Role | Router |","|---|---|---|---|",t,"","## Rule","","Before editing a child repo, read that child repo's router first.")}s(Ck,"workspaceRouterDoc");function Sk(e){let t=e.length>0?e.map(n=>`| ${n.id} | ${n.from} | ${n.to} | ${n.kind} | ${n.contractPath??""} | ${n.consumerPath??""} |`):["| _none declared_ | | | | | |","","Declare cross-repo dependencies in `.aih-workspace.json` under `edges[]`."];return g("# Workspace Contracts","","These are explicit cross-repo dependencies for the parent coordination plane.","No child files are modified by this workspace contract document.","","| Contract | From | To | Kind | Contract file | Consumer path |","|---|---|---|---|---|---|",t)}s(Sk,"workspaceContractsDoc");function Ak(e,t,n){let r=t.flatMap(c=>[`### ${c}`,"","- **Owns:** _what this repo is responsible for._",`- **Canon:** \`${c}/${n}/RULE_ROUTER.md\``,"- **Entry points:** _main handlers / routes / packages._",""]),o=t.length>0?t:["repo-a","repo-b"],a=`| Feature | ${o.join(" | ")} | Contract (API / event / schema) |`,i=`| --- | ${o.map(()=>"---").join(" | ")} | --- |`,l=`| _Login_ | ${o.map(()=>"_\u2026_").join(" | ")} | _\`LoginRequest\`/\`LoginResponse\`_ |`;return g("# Cross-repo architecture","",`> Workspace canon for \`${e}\`. **WRITE-ONCE** \u2014 aih seeded this with your repo`,"> list; you own it from here. Re-running `aih workspace` will NOT overwrite it.","","## System overview","",`_How ${t.length>0?t.join(" + "):"the repos"} fit together: request flow, data`,"flow, auth, async/events. Sketch the high-level shape in 3\u20135 sentences or a diagram._","","## Repositories","",r,"## Cross-repo feature map","","One row per feature that spans repos \u2014 the contract is the source of truth.","",a,i,l,"","## Blast-radius protocol","","When an agent is asked to change a product behavior:","","1. Start at this workspace map and identify every repo touched by the feature.","2. Use the workspace graph/filesystem MCP to inspect cross-repo call sites, imports,"," API contracts, infra bindings, runbooks, and docs before editing.","3. Before writing code in any repo, read that repo's own canon and validation flow.","4. Make source changes inside child repos, not in the parent workspace root.","5. Run each affected repo's local validation, then update this feature map when a"," cross-repo contract changes.","","## Change patterns","","- **Add a cross-repo feature:** define/version the contract first, implement the"," backend, then the UI; add or refresh the feature-map row.","- **Debug a data flow:** trace UI action \u2192 contract \u2192 backend handler \u2192 datastore;"," check each repo's canon for its own conventions.","- **Change a contract:** it is a breaking change across repos \u2014 update both sides"," in lockstep and bump the contract version.")}s(Ak,"crossRepoArchitectureDoc");function Pk(e,t){let n=e.length>0?e.map(r=>`- **${r}** \u2192 read \`${r}/${t}/RULE_ROUTER.md\` (+ \`${r}/${t}/conventions.md\`) before writing code in \`${r}/\`.`):["- _Add repos to the workspace, then re-run `aih workspace`._"];return g("# Repo discipline (workspace)","","This parent folder is the coordination plane, not a source repo. Use it to reason","about blast radius across UI/backend/infra/docs, then do implementation work inside","the affected child repos. Conventions differ per repo \u2014 before editing a repo, load","THAT repo's canon first.","",n,"",`For any change that crosses repos, consult \`${t}/cross-repo-architecture.md\` for`,"the contract, inspect the combined workspace graph, update every affected repo in","lockstep, and keep the feature map current. Never assume one repo's conventions","apply to another.")}s(Pk,"repoDisciplineDoc");function Dd(e,t,n,r){return g(`# ${t} \u2014 workspace (${e})`,"","This is a multi-repo workspace, not a single repo. Start here:","",`- \`${r}/workspace-router.md\` \u2014 top-level routing table into each child repo's canon.`,`- \`${r}/cross-repo-architecture.md\` \u2014 how the repos fit together + the cross-repo feature map.`,`- \`${r}/workspace-contracts.md\` \u2014 declared cross-repo contract edges.`,`- \`${r}/repo-discipline.md\` \u2014 read a repo's own canon before editing it.`,"- Workspace MCP graph/filesystem \u2014 use for blast-radius discovery across all child repos.","",`Repos: ${n.length>0?n.join(", "):"(none detected yet)"}. Each has its own canon`,`under \`<repo>/${r}/\` (run \`aih init\` in each). Edit workspace guidance in \`${r}/\`, not here.`)}s(Dd,"workspaceBootloader");function Rk(e,t,n,r=!1){let o=t.length>0?t.map(i=>` aih init ./${i} --apply`):[" # (no child repos detected \u2014 clone repos under this folder, then re-run)"],a=r?["- Remote setup is user/team-owned: `aih workspace --git` creates only the local bridge repo; add an origin later if and where you choose."]:[];return g(`Workspace scaffolded for \`${e}\` (parent-only). Detected repos: ${t.length>0?t.join(", "):"none"}.`,"","Lay down each repo's canon (run from the workspace root):","",o,"",`- Fill in \`${n}/cross-repo-architecture.md\` \u2014 it is write-once; aih won't overwrite it.`,`- Open \`${e}.code-workspace\` in VS Code (all repos in one window).`,"- Use the parent `.mcp.json` graph/filesystem servers for cross-repo blast-radius analysis.",...a,"- Validate: `aih doctor` at the workspace root checks each child is scaffolded.")}s(Rk,"nextStepsDoc");function z6(e){return typeof e=="object"&&e!==null&&Array.isArray(e.repos)&&e.repos.some(t=>typeof t=="object"&&t!==null)}s(z6,"hasObjectRepos");function W6(e,t){return R(`child ${e} scaffolded`,n=>{let r=`child ${e} scaffolded`;return H6(G6(n.root,e,t,"RULE_ROUTER.md"))?{name:r,verdict:"pass",detail:`${e}/${t}/ canon present`}:{name:r,verdict:"skip",detail:`not scaffolded \u2014 run \`aih init ./${e} --apply\``}})}s(W6,"childScaffoldedProbe");async function V6(e){let t=e.contextDir,n=U6(e.root)||"workspace",r=fk(e.root,gk(e.options.repos)),o=e.options.git===!0,a=gt(e.root,t),i=a&&a.repos.length>0?a.repos:Sf(r,Yo.join(t,"RULE_ROUTER.md")),l=a?.edges??[],c=[L(".aih-workspace.json",z6(a?.raw)?{...a.raw,...o?{git:!0}:{}}:kk(r,t,o),`workspace marker (multi-repo: ${r.length>0?r.join(", "):"no repos detected"})`,{merge:!0}),L(`${n}.code-workspace`,wk(r),"VS Code multi-root workspace",{merge:!0}),P(Yo.join(t,"workspace-router.md"),Ck(i),"workspace router (federated child repo table of contents)"),P(Yo.join(t,"workspace-contracts.md"),Sk(l),"workspace contracts (parent-owned cross-repo dependency index)"),P(Yo.join(t,"cross-repo-architecture.md"),Ak(n,r,t),"cross-repo architecture + feature map (write-once \u2014 you own it)",{once:!0}),P(Yo.join(t,"repo-discipline.md"),Pk(r,t),"per-repo discipline routing (read a repo's canon before editing it)"),P("CLAUDE.md",Dd("Claude workspace",n,r,t),"Claude workspace bootloader \u2192 cross-repo canon"),P("AGENTS.md",Dd("agent workspace",n,r,t),"AGENTS.md workspace bootloader (Codex/Kiro/\u2026 ) \u2192 cross-repo canon"),L(".mcp.json",xk(r,(e.env.AIH_MCP_FS_VERSION??"").trim()||void 0),`combined graph + filesystem MCP spanning ${r.length} child repo(s), merged into any existing .mcp.json`,{merge:!0})];o&&c.push(yf(e.root,r));let d=[...c,I("workspace next steps (run `aih init` per child)",Rk(n,r,t,o)),...o?await bf(e,c):[]];for(let p of r)d.push(W6(p,t));return S("workspace",...d)}s(V6,"workspacePlan");var Ek={name:"workspace",summary:"Scaffold a multi-repo workspace: cross-repo map, combined graph/filesystem MCP, .code-workspace (parent-only)",options:[{flags:"--repos <list>",description:"child repos (comma-separated); else auto-detect *//.git"},{flags:"--git",description:"initialize a local git repo for workspace coordination files; remote setup remains user-owned"}],plan:V6};import{randomUUID as t$}from"crypto";import{basename as n$,join as r$}from"path";import{createInterface as q6}from"readline";function Tk(e=process.env){return e.AIH_NO_PROMPT==="1"?!1:!!(process.stdin.isTTY&&process.stdout.isTTY)}s(Tk,"isInteractive");function Ik(){return{ask(e){let t=q6({input:process.stdin,output:process.stdout});return new Promise(n=>{t.question(e,r=>{t.close(),n(r.trim())})})}}}s(Ik,"makeReadlinePrompter");import{isAbsolute as Y6}from"path";import{Command as K6}from"commander";var Md="1.0.1";function J6(e=[],t){let n=new K6;return n.name("aih").description("Enterprise AI Bootstrapping Harness \u2014 governed AI coding setup for enterprise workstations and repos").version(Md).showHelpAfterError("(add --help for usage)"),Dk(n,e,t),n}s(J6,"buildProgram");function Doe(e){let t=e[2];return t==="--version"||t==="-V"}s(Doe,"isVersionFastPath");async function Moe(){let{commands:e,warnings:t}=await fp(Lk());return{program:J6(e,t),warnings:t}}s(Moe,"buildProgramWithPlugins");var Z6="https://json.schemastore.org/sarif-2.1.0.json",X6="https://github.com/samartomar/ai-harness";function Q6(e){return e==="fail"?"error":"note"}s(Q6,"level");function _6(e){let t=e.replace(/\\/g,"/");if(!(t.length===0||Y6(t)||/^[A-Za-z]:/.test(t))&&!t.split("/").some(n=>n===".."))return t}s(_6,"safeArtifactUri");function e$(e){if(!e.location)return[];let t=_6(e.location.uri);if(t===void 0)return[];let n={artifactLocation:{uri:t}};return e.location.startLine!==void 0&&(n.region={startLine:e.location.startLine}),[{physicalLocation:n}]}s(e$,"locations");function Mk(e){return e.fingerprint?{"aih/v1":e.fingerprint}:void 0}s(Mk,"fingerprints");function Ok(e,t="aih"){let r=[...new Set(e.checks.map(i=>i.name))].map(i=>({id:i,name:i,shortDescription:{text:i},defaultConfiguration:{level:"warning"}})),o=e.checks.map(i=>({ruleId:i.name,level:Q6(i.verdict),message:{text:i.detail??i.name},locations:e$(i),...Mk(i)?{partialFingerprints:Mk(i)}:{}}));return JSON.stringify({$schema:Z6,version:"2.1.0",runs:[{tool:{driver:{name:t,informationUri:X6,version:Md,rules:r}},results:o}]},null,2)}s(Ok,"reportToSarif");var o$=s(e=>new Promise(t=>setTimeout(t,e)),"delay"),s$=["SETUP.md","docs/SETUP.md",".aih/SETUP.md"];function a$(e){for(let t of s$){let n=k(r$(e,t));if(n!==void 0)return n}}s(a$,"readSetupText");function i$(e){let t=Number(e);return Number.isFinite(t)&&t>0?Math.floor(t):void 0}s(i$,"positiveInt");function c$(e){let n=(e.split(/[,\s]+/).find(l=>l.startsWith("--"))??e).replace(/^--/,""),r=n.indexOf("<"),o=n.indexOf("["),a=Math.min(r===-1?n.length:r,o===-1?n.length:o);return n.slice(0,a).trim().replace(/-([a-z])/g,(l,c)=>c.toUpperCase())}s(c$,"flagKey");function l$(e,t){let n={};for(let r of e.options??[]){let o=c$(r.flags);t[o]!==void 0&&(n[o]=t[o])}return n}s(l$,"extractOptions");async function ke(e,t,n={}){let r=n.env??process.env,o=n.write??(x=>process.stdout.write(x)),a=n.run??Wt,i=t.optsWithGlobals(),l=Array.isArray(t.processedArgs)?t.processedArgs[0]:void 0,c=n.positionalRoot===!1?void 0:n.positionalRoot??l,d=n.now??(()=>new Date),p=d(),u=(n.newRunId??(()=>`run_${t$().slice(0,8)}`))(),m=al(n.argv??process.argv.slice(2)).map(x=>cn(x,r)),f=c??i.root??r.AIH_ROOT??process.cwd(),v=i.log===!1,y=s(x=>{Nf(f,r,{noLog:v})&&$f(f,jf(x),p)},"logRun"),A=!1;try{let x=c??i.root??r.AIH_ROOT??process.cwd(),h=G(x),w=jt(t,"contextDir")==="cli"?i.contextDir:void 0,T=w===void 0?h?.contextDir:void 0,F=jt(t,"posture")==="cli"?"cli":void 0,_=or({root:x,env:r,flag:i.posture,flagSource:F,marker:h}),M=Wn(r,{apply:i.apply,verify:i.verify,json:i.json,contextDir:w??T,root:x,caPattern:i.caPattern});A=M.json;let we=Yn({run:a,env:r}),Je=(i.detect===!0||e.wantsInstallPrompt===!0)&&i.yes!==!0&&!M.json,Se=n.prompter??(Je&&Tk(r)?Ik():void 0),Zo=i$(i.refresh),Od=i.open===!0||i.demo===!0||Zo!==void 0,Ye={root:M.root,contextDir:M.contextDir,posture:_.posture,postureSource:_.postureSource,apply:e.readOnly?!1:M.apply||Od,verify:e.readOnly||e.alwaysVerify||typeof i.sarif=="string"?!0:M.verify,json:M.json,run:a,host:we,env:r,prompter:Se,options:{...l$(e,i),...n.optionOverrides??{},caPattern:M.caPattern,cli:i.cli,allTools:i.allTools,detect:i.detect,force:i.force,open:Od?!0:i.open}},$k=await e.plan(Ye),Re=await Ze($k,Ye,{skipWorktreeGate:e.skipWorktreeGate===!0}),jd=Re.execs.some(xe=>xe.ran&&xe.ok===!1),Nd=Re.report?Re.report.exitCode():0,$d=Nd||(jd?1:0),it=Re.report&&Re.report.checks.length>0?Ar({capability:Re.capability,checks:Re.report.checks,projectName:n$(M.root)||"this project",root:M.root,command:al(["aih",e.name,...Ye.verify?["--verify"]:[],...Ye.apply?["--apply"]:[]]).join(" "),contextDir:M.contextDir,targets:(G(M.root)?.targets??[]).join(", ")||"none",platform:we.platform,runId:u,timestamp:p.toISOString(),setupText:a$(M.root),env:r}):void 0,vi,yi=i.supportOut;if(it&&typeof yi=="string"&&yi.length>0){vi={};for(let xe of it.templates){let Xo=`${yi}/${xe.code.replace(/[^a-z0-9.-]/gi,"_")}.md`;vt(Ye,Xo,`${xe.subject}
3049
3049
 
3050
3050
  ${xe.body}`),vi[xe.code]=Xo}}if(!(i.sarif==="-"&&Re.report!==void 0))if(A){let xe=it?{...Re,support:{findings:it.findings,templates:it.templates}}:Re;o(`${JSON.stringify(xe,null,2)}
3051
3051
  `)}else o(`${Xe(Re)}
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{qa as s,ra as e,sa as n}from"./chunk-HPWF2YPV.js";e(process.argv)?s().parse(process.argv):n().then(({program:r,warnings:o})=>{for(let i of o)process.stderr.write(`aih: plugin: ${i}
2
+ import{qa as s,ra as e,sa as n}from"./chunk-WZTIDZFM.js";e(process.argv)?s().parse(process.argv):n().then(({program:r,warnings:o})=>{for(let i of o)process.stderr.write(`aih: plugin: ${i}
3
3
  `);return r.parseAsync(process.argv)}).catch(r=>{process.stderr.write(`fatal: ${r instanceof Error?r.message:String(r)}
4
4
  `),process.exitCode=1});
package/dist/index.d.ts CHANGED
@@ -968,7 +968,7 @@ declare function allowedPluginRoots(): string[];
968
968
  */
969
969
  declare function loadExternalCommands(builtinNames: ReadonlySet<string>, opts?: PluginLoadOptions): Promise<PluginLoadResult>;
970
970
 
971
- declare const VERSION = "1.0.0";
971
+ declare const VERSION = "1.0.1";
972
972
  /**
973
973
  * Build the configured commander program. Imported by both the CLI entry and
974
974
  * tests. Stays SYNC: `extra` lets callers merge pre-loaded plugin specs — the
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{$ as xr,A as Y,B as _,C as c,D as j,E as k,F as q,G as v,H as w,I as y,J as z,K as F,L as G,M as H,N as J,O as K,P as Q,Q as U,R as X,S as Z,T as $,U as rr,V as or,W as mr,X as er,Y as tr,Z as fr,_ as pr,a as i,aa as ir,b as a,ba as ar,c as A,ca as Ar,d,da as dr,e as g,ea as gr,f as l,fa as lr,g as n,ga as nr,h as s,ha as sr,i as u,ia as ur,j as C,ja as Cr,k as I,ka as r,l as L,la as o,m as N,ma as m,n as P,na as e,o as b,oa as t,p as E,pa as f,q as O,qa as p,r as S,s as D,sa as x,t as M,u as R,v as h,w as B,x as T,y as V,z as W}from"./chunk-HPWF2YPV.js";export{m as ALL_COMMANDS,i as AihError,r as CAPABILITIES,P as ContextDir,u as DirtyWorktreeError,I as FsTransaction,d as FsTxnError,n as MergeError,l as NotImplementedError,lr as PLUGIN_PACKAGE,s as PathContainmentError,A as PlatformError,o as READONLY,nr as SHARED_FLAG_TOKENS,a as SettingsError,f as VERSION,g as VerificationError,U as VerificationReport,ur as allowedPluginRoots,v as beginMarker,p as buildProgram,x as buildProgramWithPlugins,e as builtinCommandNames,Q as deepMerge,or as defaultRunner,tr as derBase64ToPem,D as digest,S as doc,M as dynamicDigest,w as endMarker,q as ensureTrailingNewline,T as envBlock,B as exec,$ as executePlan,mr as fakeRunner,z as formatExport,j as frontmatter,c as indent,K as isPlainObject,k as jsonFile,_ as lines,Cr as loadExternalCommands,b as loadSettings,gr as makeHostAdapter,y as managedBlock,er as missingToolRunner,ar as parseCertLines,xr as parseFirstInt,J as parseJsoncText,ir as parseNvidiaSmi,Ar as parsePemBlocks,W as plan,R as probe,h as probeMany,L as readIfExists,N as readRegularFile,t as registerCommands,V as remove,H as removeManagedBlock,Z as resolveContents,dr as resolvePlatform,C as retryTransient,fr as safeCaPattern,sr as sanitizeLabel,Y as stripTrailingNewlines,rr as summarizeResult,F as upsertManagedBlock,G as upsertTextBlock,pr as vdiFromEnv,X as writeArtifact,O as writeJson,E as writeText};
1
+ import{$ as xr,A as Y,B as _,C as c,D as j,E as k,F as q,G as v,H as w,I as y,J as z,K as F,L as G,M as H,N as J,O as K,P as Q,Q as U,R as X,S as Z,T as $,U as rr,V as or,W as mr,X as er,Y as tr,Z as fr,_ as pr,a as i,aa as ir,b as a,ba as ar,c as A,ca as Ar,d,da as dr,e as g,ea as gr,f as l,fa as lr,g as n,ga as nr,h as s,ha as sr,i as u,ia as ur,j as C,ja as Cr,k as I,ka as r,l as L,la as o,m as N,ma as m,n as P,na as e,o as b,oa as t,p as E,pa as f,q as O,qa as p,r as S,s as D,sa as x,t as M,u as R,v as h,w as B,x as T,y as V,z as W}from"./chunk-WZTIDZFM.js";export{m as ALL_COMMANDS,i as AihError,r as CAPABILITIES,P as ContextDir,u as DirtyWorktreeError,I as FsTransaction,d as FsTxnError,n as MergeError,l as NotImplementedError,lr as PLUGIN_PACKAGE,s as PathContainmentError,A as PlatformError,o as READONLY,nr as SHARED_FLAG_TOKENS,a as SettingsError,f as VERSION,g as VerificationError,U as VerificationReport,ur as allowedPluginRoots,v as beginMarker,p as buildProgram,x as buildProgramWithPlugins,e as builtinCommandNames,Q as deepMerge,or as defaultRunner,tr as derBase64ToPem,D as digest,S as doc,M as dynamicDigest,w as endMarker,q as ensureTrailingNewline,T as envBlock,B as exec,$ as executePlan,mr as fakeRunner,z as formatExport,j as frontmatter,c as indent,K as isPlainObject,k as jsonFile,_ as lines,Cr as loadExternalCommands,b as loadSettings,gr as makeHostAdapter,y as managedBlock,er as missingToolRunner,ar as parseCertLines,xr as parseFirstInt,J as parseJsoncText,ir as parseNvidiaSmi,Ar as parsePemBlocks,W as plan,R as probe,h as probeMany,L as readIfExists,N as readRegularFile,t as registerCommands,V as remove,H as removeManagedBlock,Z as resolveContents,dr as resolvePlatform,C as retryTransient,fr as safeCaPattern,sr as sanitizeLabel,Y as stripTrailingNewlines,rr as summarizeResult,F as upsertManagedBlock,G as upsertTextBlock,pr as vdiFromEnv,X as writeArtifact,O as writeJson,E as writeText};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aihq/harness",
3
- "version": "1.0.0",
4
- "description": "Enterprise AI Bootstrapping Harness — bootstraps governed, proxy-safe AI coding into workstations and repos",
3
+ "version": "1.0.1",
4
+ "description": "Enterprise AI Bootstrapping Harness — governed AI-assisted coding for enterprise workstations and repos: TLS trust, repo canon, skill supply chain, evidence",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/samartomar/ai-harness.git"