@sabaiway/agent-workflow-kit 1.45.1 → 1.46.0
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/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +2 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +2 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +16 -5
- package/references/modes/upgrade.md +2 -2
- package/references/modes/velocity.md +1 -1
- package/references/templates/agent_rules.md +1 -1
- package/tools/doc-parity.mjs +16 -4
- package/tools/manifest/schema.md +29 -2
- package/tools/manifest/validate.mjs +38 -0
- package/tools/procedures.mjs +2 -0
- package/tools/recommendations.mjs +279 -79
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 1.46.0 — Recommendations UX rework: verdict-first, shape-capped, user-language; sandbox-lane discoverability (REC-UX-REWORK, AD-053)
|
|
8
|
+
|
|
9
|
+
A **feature** release (ships with engine 1.16.0, memory 2.2.0, bridges 2.6.0/2.5.0 bundled)
|
|
10
|
+
reworking the upgrade Recommendations section after its first consumer-side report drew a
|
|
11
|
+
readability verdict — the section now LEADS with an answer instead of a wall of caveats:
|
|
12
|
+
|
|
13
|
+
- **Verdict-first render (D1).** Items carry a frozen two-class severity (`attention` — a
|
|
14
|
+
configured declaration broken/drifted/degrading/invalid; `optional` — an offer to enable an
|
|
15
|
+
unconfigured capability). The optimal state stays the byte-identical empty-state line; every
|
|
16
|
+
other state opens with ONE composed verdict line from frozen, doc-parity-bound templates
|
|
17
|
+
(`{K} item(s) need attention` / `nothing is broken` ONLY when nothing needs attention AND no
|
|
18
|
+
probe was skipped / `{N} optional recommendation(s), apply any you want` / `optimality NOT
|
|
19
|
+
attested — {M} probe check(s) skipped`). Items render attention-first with severity tags.
|
|
20
|
+
- **Shape is contract (D2).** Every benefit string and static WHAT template lives in frozen
|
|
21
|
+
exported registries (`BENEFITS` + the new `WHATS`, per-site variants included) pinned by a
|
|
22
|
+
static gate: one line, 140-char cap, banned tokens (RISK/CAVEAT/IF-hedges/dates); dynamic parts
|
|
23
|
+
cap by truncation-with-count; `add()` backstops composed items into the stated-skip lane; skip
|
|
24
|
+
reasons normalize to one capped line. The first RED run enumerated 9 violators — all rewritten.
|
|
25
|
+
- **Risk moved to the consent moment (D3).** Posture/risk prose lives in the mode doc's per-item
|
|
26
|
+
notes (closed, bidirectionally test-pinned via `RISK_NOTED_KEYS`); the apply lane is an explicit
|
|
27
|
+
informed-consent checkpoint: select → posture note inline → explicit confirm → run EXACTLY the
|
|
28
|
+
rendered one-liner.
|
|
29
|
+
- **`network-allowlist` → `sandbox-lane` (D4, merges REC-SANDBOX-LANE).** The unknowable-condition
|
|
30
|
+
hedge is gone: the item surfaces the manifest-declared observed session-sandbox recipe
|
|
31
|
+
(`networkHosts` ∪ resolved `writableDirs`) for wired review wrappers and converges on a NEUTRAL
|
|
32
|
+
fingerprint acknowledgement (`agentWorkflow.sandboxLaneAck`, either settings scope; home-symbolic
|
|
33
|
+
normalization keeps a committed ack machine-portable; a changed recipe re-fires). Security keys
|
|
34
|
+
are never read as an ack and never recommended as a fix; the mode doc's sandbox-lanes section
|
|
35
|
+
routes per host class by a narrowest-scope ladder.
|
|
36
|
+
- **User-language presentation (D5).** The paste-verbatim contract is retired: the agent PRESENTS
|
|
37
|
+
the section in the user's conversational language — every fact and count, nothing added or
|
|
38
|
+
dropped; commands/paths/hosts byte-exact; raw tool block on request (the AD-032 lane). A static
|
|
39
|
+
language-contract test pins the new tokens present and the retired phrases absent on every live
|
|
40
|
+
surface (both mode docs, README row, tool header, doc-parity comments).
|
|
41
|
+
- **Manifests: `writableDirs` (D6).** New optional validated field — `{env, default}` entries
|
|
42
|
+
(validate `--strict`, fixtures ×4, kit mirrors re-synced); the advisor resolves at run time
|
|
43
|
+
mirroring the wrapper byte-semantics (non-empty env wins; empty ≡ unset; only `~`, `~/…`,
|
|
44
|
+
absolute forms ride as-given — anything else anchors to the project root).
|
|
45
|
+
- **Prompt-economy render (D7).** The cost-lanes advisory (`procedures.mjs`) gains the
|
|
46
|
+
prompt-economy clause rendered from the engine canon (read-only fan-out on restricted-tool
|
|
47
|
+
vehicles only; one plain pipeline per call; capability-gated launcher guidance; the
|
|
48
|
+
quality/speed guard + honest limit), drift-guarded by one distinctive token per invariant on all
|
|
49
|
+
three surfaces.
|
|
50
|
+
|
|
51
|
+
Bundled bridges: **codex-cli-bridge 2.6.0**, **antigravity-cli-bridge 2.5.0** — each manifest
|
|
52
|
+
gains its `writableDirs` declaration (codex `{CODEX_HOME, ~/.codex}`; agy
|
|
53
|
+
`{null, ~/.gemini/antigravity-cli}`); wrapper behavior unchanged.
|
|
54
|
+
|
|
7
55
|
## 1.45.1 — codex frontier pin moves to gpt-5.6-sol (bridge 2.5.0)
|
|
8
56
|
|
|
9
57
|
A **patch** release (the kit surface is unchanged; the bundled `codex-cli-bridge` moves 2.4.0 →
|
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
240
240
|
| `/agent-workflow-kit grounding` | any time | **grounded-review facts assembler** — mechanizes populating `agy-review --facts @f`: slices your entry-point's **Hard Constraints** section verbatim (exactly one match, else a loud stop) and/or a plan's decision-bearing sections (`## Approach` + `## Verification` required, `## Decisions (locked)` when present; duplicates stop), under the same byte budget the agy wrapper enforces (minus `--reserve-bytes` for the artifact share), with a loud tail-trim on overflow. `--autonomy` (AD-044) appends the COMPUTED effective autonomy policy from the git-top `docs/ai/autonomy.json` (every red-line + per-activity level, stated source line; absent file → the computed defaults ARE the policy, exit 0; a malformed policy fails CLOSED, exit 1). `--ledger-summary` (AD-049) appends a COMPUTED review-ledger digest for the single in-flight segment (rounds · origins · classifications · verdicts · overrides — unrelated loops excluded), so the reviewer sees the loop's own history without hand-copying. Prints to stdout; `--out` writes **one scratch file only** — system-temp outside the repo ($TMPDIR / /tmp, rewritable) or a **fresh** gitignored in-repo path (create-only, exclusive write; an existing in-repo file, even gitignored, is refused — the `.env` clobber class); tracked, not-ignored-in-repo, other outside-repo, and symlink/non-regular destinations are all refused. Never commits, never runs a subscription CLI. |
|
|
241
241
|
| `/agent-workflow-kit review-ledger` | any time | **review-round ledger** — turns the review-loop stop rule into a **computed** signal: record each plan-execution review round, its triage, and any recorded **overrides** (`oracle-change` lifts named tampered test files; `red-proof` waives the observed-red proof for exactly one `testId`; v4 adds **`size-cap`** — the exact sanctioned magnitude for a surface over the diff cap, segment-scoped — all loud, durable, auditable waivers, never silent) into a JSONL ledger inside the git dir (never committable), and read the stop decision (`converged > resolved-residual > triage-required > continue`) from the records — never from a remembered rule. **Since v4 every record carries `base` = the HEAD commit and the whole loop is SEGMENT-scoped (AD-048):** round numbering, the caps, and every tooth reset ONLY at a gated commit — a multiphase plan records fully while round 4 within one segment stays refused; `gate-run` records (minted by `gates --record`) make gates-before-review a computed precondition; the triage class **`refuted`** is the honest phantom-finding lane (grounds mandatory); `--telemetry` renders counts-only gate-efficacy data across all loops and both ledgers. `--status` replaces the hand-composed per-round tally; `record --from-receipts` (AD-049) DRAFTS the `backends[]` from the current-fingerprint receipts (verdict per backend, counts from the supplied findings) instead of hand-composing them — a recipe-named backend with no receipt is a loud stop; `--check` is a fail-closed gate exit code for `docs/ai/gates.json` (by hand or via the consent-gated seeder). A `fixable-bug` triage **requires its red→green `testId`** (v1..v3 records stay valid on read — old records never enter a segment). The writer refuses a round (past the hard-max, while a triage is pending, or lacking a grounded review receipt) and refuses an override outside its single in-flight loop. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
|
|
242
242
|
| `/agent-workflow-kit fold-completeness` | any time | **fold-completeness gate** — attests the review loop's folded fixes were proven the HONEST way (no fix theater): the runner executes your suite ONCE under `NODE_V8_COVERAGE`, checks every changed executable line is **executed**, probes every bound `testId` N times (`AW_FOLD_RERUNS`), records content hashes, and scans the test surface for tamper; the `--red` verb observes a test FAILING on the pre-fold tree — **BEFORE the fix** — and mints an **observed-red receipt**. The read-only checker (`--status` / `--check`) then requires, per bound test: the receipt, receipt-precedes-run order, N/N-green probes, and content **custody** (the green test is byte-identical to the one seen failing) — the receipt+custody half waivable for exactly one `testId` by a recorded `red-proof` override; mixed/timed-out probes are QUARANTINE (never converted, no override); **v3 records carry `base` and the proof is segment-scoped (AD-048)** — a committed phase's custody obligations close with its commit, and a receipt never crosses a commit boundary; tampered pre-existing test files fail closed unless covered by a recorded `oracle-change` override. Honest residuals: coverage proves **execution, not assertion**, and records are forgeable — self-discipline, not a security boundary. v1 scope is JS/V8 (pluggable via `AW_FOLD_BOUND_CMD`): changed TS/JSX fails closed, docs/config changes are listed but never block, and **no mutation testing ships**. Wire the gate by hand — the consent-gated seeder deliberately does not offer it yet (JS-only hold). Never commits, never runs a subscription CLI. |
|
|
243
|
-
| `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter) and renders each item as **{what · one-line benefit · the exact consent-gated apply one-liner}
|
|
243
|
+
| `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the sandbox-lane item is **HAND-APPLY** — the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`; convergence is a neutral fingerprint acknowledgement, never a security key). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
|
|
244
244
|
| `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, the ledger's own class/scope vocabulary, and the autonomy-doctor EXIT/status/trusted-dir contract) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is a gate exit code for `docs/ai/gates.json`. Never writes, never commits, never runs a subscription CLI. |
|
|
245
245
|
| `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
|
|
246
246
|
| `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: agent-workflow-kit
|
|
|
3
3
|
description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
|
-
version: '1.
|
|
6
|
+
version: '1.46.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: antigravity-cli-bridge
|
|
3
3
|
description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '2.
|
|
5
|
+
version: '2.5.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# antigravity-cli-bridge
|
|
@@ -194,7 +194,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
|
|
|
194
194
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
195
195
|
# version (drift-guarded by agy-review.test.mjs against capability.json).
|
|
196
196
|
AW_RECEIPT_BACKEND="agy"
|
|
197
|
-
AW_BRIDGE_VERSION="2.
|
|
197
|
+
AW_BRIDGE_VERSION="2.5.0"
|
|
198
198
|
# `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
|
|
199
199
|
AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
|
|
200
200
|
# Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable warning.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "antigravity-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.5.0",
|
|
7
7
|
"provides": ["review", "probe"],
|
|
8
8
|
"roles": {
|
|
9
9
|
"review": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
}
|
|
51
51
|
],
|
|
52
52
|
"networkHosts": ["*.googleapis.com", "accounts.google.com", "antigravity-unleash.goog", "lh3.googleusercontent.com"],
|
|
53
|
+
"writableDirs": [{ "env": null, "default": "~/.gemini/antigravity-cli" }],
|
|
53
54
|
"detect": {
|
|
54
55
|
"installed": {
|
|
55
56
|
"env": "ANTIGRAVITY_CLI_BRIDGE_DIR",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: codex-cli-bridge
|
|
3
3
|
description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '2.
|
|
5
|
+
version: '2.6.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# codex-cli-bridge
|
|
@@ -172,7 +172,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
|
|
|
172
172
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
173
173
|
# version (drift-guarded by codex-review.test.mjs against capability.json).
|
|
174
174
|
AW_RECEIPT_BACKEND="codex"
|
|
175
|
-
AW_BRIDGE_VERSION="2.
|
|
175
|
+
AW_BRIDGE_VERSION="2.6.0"
|
|
176
176
|
CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
|
|
177
177
|
CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
|
|
178
178
|
# Generous hard cap for a slow xhigh review (subscription latency varies).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.6.0",
|
|
7
7
|
"provides": ["execute", "review"],
|
|
8
8
|
"roles": {
|
|
9
9
|
"execute": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
}
|
|
73
73
|
],
|
|
74
74
|
"networkHosts": ["chatgpt.com", "*.chatgpt.com"],
|
|
75
|
+
"writableDirs": [{ "env": "CODEX_HOME", "default": "~/.codex" }],
|
|
75
76
|
"detect": {
|
|
76
77
|
"installed": {
|
|
77
78
|
"env": "CODEX_CLI_BRIDGE_DIR",
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
### Mode: recommendations
|
|
2
2
|
|
|
3
|
-
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter) and renders each item as **{what is sub-optimal · the benefit in ONE plain line · the exact consent-gated apply one-liner}**. The tool computes; **you
|
|
3
|
+
The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
|
|
4
4
|
|
|
5
5
|
Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--json]`:
|
|
6
6
|
|
|
7
7
|
1. **`--cwd` is REQUIRED** — the target project is explicit, never inferred from the shell's current directory (a subdirectory invocation still advises on the named root).
|
|
8
|
-
2. **
|
|
9
|
-
3. **The apply-through-agent lane
|
|
10
|
-
4.
|
|
8
|
+
2. **Verdict first (present-even-when-empty, opening at the `## Recommendations (agent-workflow)` header).** With everything optimal the body is exactly `no recommendations — flow optimal.` — the empty-state line ALONE is the verdict, zero added lines. Every other state opens the body with ONE verdict line composed from the frozen templates: `{K} item(s) need attention` leads when any item's severity is `attention` (a CONFIGURED declaration that is broken, drifted, degrading or invalid — the deployment needs review); `{N} optional recommendation(s), apply any you want` covers the `optional` class (offers to enable an unconfigured capability), led by `nothing is broken` ONLY when no item needs attention AND no probe check was skipped (a skipped probe could hide an attention-class problem — the claim never overreaches); `optimality NOT attested — {M} probe check(s) skipped` is appended last whenever probe checks were skipped. (The `(s)` invariant form IS the pinned pluralization — no singular/plural branching.) Items render attention-first, each tagged `needs attention:` / `optional:`. A failed probe renders as a stated `⚠ skipped item …` line — never a crash, never a fabricated item.
|
|
9
|
+
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). A note-less item's apply is a consent-gated WRITER that carries its own posture surface — relay that writer's preview/notice output before confirming. The user **explicitly confirms**, and **only then** you run **EXACTLY the rendered one-liner — no improvisation** (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** is **never run by you and never written by the kit** — hand the user its rendered line together with its posture note.
|
|
10
|
+
4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**Per-item posture notes (the consent moment — surface BEFORE running or handing over the apply):**
|
|
13
|
+
|
|
14
|
+
- `agy-adddir` — enabling the add-dir offload re-enables the Issue-001 stall risk (the wrapper's hard timeout bounds it); with the knob off an oversized agy code review refuses instead. Risk profile: availability only, no security delta.
|
|
15
|
+
- `sandbox-lane` — surface this note TOGETHER with the sandbox-lanes ladder below (the ladder IS the practical half of the note — inline, never a pointer). Pure DISCOVERABILITY: it surfaces the manifest-declared observed session-sandbox recipe (egress hosts ∪ resolved writable state dirs — `networkHosts` ∪ `writableDirs` of the wired bridges' `capability.json`, the single documentation source) and converges on a NEUTRAL fingerprint acknowledgement (`"agentWorkflow": { "sandboxLaneAck": … }`, either settings scope; a changed recipe re-fires the item). It never claims the settings security keys take effect on any host class, never recommends writing them, and the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur: a network pre-allow widens egress for EVERY sandboxed command; a write allowance on CLI state dirs would expose credential dirs). Posture history: an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring hand-applied settings security keys in BOTH scopes, and codex needs a writable HOME (EROFS `~/.codex` in-sandbox); whether a session's sandbox honors the settings keys is runtime-unknowable from the advisor (a denial-only signal) — which is exactly why the item states only detectable facts and no zero-prompt promise on any host class.
|
|
16
|
+
|
|
17
|
+
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
|
18
|
+
|
|
19
|
+
- **Settings-native sandbox** (the harness reads the `.claude/settings.json` sandbox keys): nothing beyond the `--bridge-tier` wiring — the tier's `excludedCommands` already routes the wrappers OUTSIDE the sandbox, so they never consult `allowedDomains`/`allowWrite`. Record the ack once the tier is confirmed.
|
|
20
|
+
- **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the settings security keys were rejected for (every sandboxed command in the session gains that egress and those writable credential dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session; (3) else the per-run consented bypass. Record the ack once a lane is chosen — choosing the bypass, or consciously declining, also counts.
|
|
21
|
+
- Either way the recipe is **observed-minimal, honestly incomplete** — a blocked host names itself at run time; extend by hand and re-ack (the fingerprint moves with the recipe, not with your extensions).
|
|
22
|
+
|
|
23
|
+
**Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the sandbox-provision item pins via a `cd <root> &&` prefix — the doctor reads its cwd; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the kit never seeds `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` (hand-apply territory; the sandbox-lane convergence is a neutral acknowledgement, never a security key).
|
|
13
24
|
|
|
14
25
|
**Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
|
|
@@ -58,10 +58,10 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
|
|
|
58
58
|
4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
|
|
59
59
|
- **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
|
|
60
60
|
- **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
|
|
61
|
-
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and
|
|
61
|
+
- **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
|
|
62
62
|
- **Print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason). The welcome mat closes on **one** caveat-aware next step (a behind member first, else `setup` / `recipes` / `velocity` / `agents` / `hook`).
|
|
63
63
|
- **Then ask before committing — never auto-commit.** If step 3 added the slot (or anything else changed), report it and ask. If step 3 was a pure zero-diff no-op and nothing else changed, give the plain **settings already current — no update needed** message (the *Success state* contract in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`) and still print the read-only version block (installed package versions) + backend line — but **no `docs/ai` structure version and no two-axes note** (nothing changed, so the number is inert here).
|
|
64
64
|
5. Show the relevant `${CLAUDE_SKILL_DIR}/CHANGELOG.md` diff (entries newer than the project's stamp).
|
|
65
65
|
6. **Collect the migration answers FIRST, then apply.** If `AGENTS.md` is missing BOTH the *Communication language* and *Attribution* blocks — i.e. both blocks are missing (a pre-1.1.0 deployment) — ask the two questions as ONE structured multi-question prompt; record each answer individually, write nothing until ALL are answered, and carry the answers into the migrations below: a migration whose answer was already collected never re-asks (its own "Ask the user" step is the standalone fallback); a single missing block keeps its single ask (step 7). Then apply `${CLAUDE_SKILL_DIR}/migrations/<version>-<slug>.md` in **semver order**, only those newer than the project's stamp. Migrations are **idempotent** — safe to re-run.
|
|
66
66
|
7. Reconcile drift: add any kernel files/scripts the project is missing; never clobber project-authored content (their `decisions.md`, `known_issues.md`, page specs stay). Any user question a migration raises follows the same rule as bootstrap — **structured multiple-choice where supported** (`AskUserQuestion` in Claude Code), otherwise prose. If `AGENTS.md` has no *Communication language* block (pre-1.1.0 deployment), **ask the user their conversational language** and insert the block — see `migrations/1.1.0-communication-language.md`. If it has no *Attribution* block (pre-1.2.0 deployment), **ask whether the agent may attribute work to itself / AI** and insert the block (defaulting to `off`) — see `migrations/1.2.0-agent-attribution.md`. (An answer already collected by the step-6 batched prompt is carried in — never re-asked here.)
|
|
67
|
-
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`2.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** outcome (same outcome set as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root
|
|
67
|
+
8. Re-stamp `docs/ai/.workflow-version` to the **deployment-lineage head** (`2.0.0`, not the package version — mechanics unchanged: the atomic write to the stamp file). In the report, **describe what the upgrade changed in plain human terms** — which parts of their `docs/ai` are now different (the migrations that ran), plus the step-3 **placed-bridge refresh** lines (pasted verbatim), the step-3 **agent-rules lens** outcome (same outcome set as step 4), the step-3 **bridge-settings reconcile** outcome, and the step-3 **autonomy-declaration ensure** outcome (*seeded* / *already present, preserved*) — rather than reciting a version number; **omit the raw structure number**, and do **not** print the two-axes note here (it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`, on demand only). Then **render the mandatory Recommendations section**: run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language (every fact, count and item, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request; present-even-when-empty: `no recommendations — flow optimal.`), then OFFER the consent-gated applies (per picked item: posture note → explicit confirm → run EXACTLY the rendered one-liner; a HAND-APPLY item is never run by you — `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Then **print the report footer** in the canonical order (version block → one-line backend-status line → welcome mat — the shared contracts in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`; rendered from the helpers, same host-can't-run skip-with-reason; the welcome mat closes on one caveat-aware next step). Then **ask before committing** — the pinned order on this exit is: Recommendations block → optional applies → report footer → the commit ask.
|
|
@@ -31,7 +31,7 @@ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage** — the
|
|
|
31
31
|
|
|
32
32
|
**Invariants:** creates `.claude/` if absent and writes **only** `.claude/settings.json` (no other file); **never** allowlists commit/push/publish; **never** writes `settings.local.json`; never commits; opt-in `acceptEdits`, never silent.
|
|
33
33
|
|
|
34
|
-
**The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — BOTH surfaces: `permissions.allow` prefix rules AND the wrapper names in `sandbox.excludedCommands` (the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval). Both land in the **project** `.claude/settings.json` — the file this writer owns; an exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME + a network prompt), so hand-wiring the local file is not a working substitute for this tier. **Honesty note:** a session whose sandbox is imposed by the harness runtime itself (e.g. an IDE-managed session sandbox) may ignore settings-level exclusions entirely — there the wrappers need the session-level sandbox config (or a per-run consented bypass); the tier's seeded posture is correct for the settings-native sandbox and simply prompts again elsewhere (fail-safe, never a silent widening). Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (observed-minimal — a blocked host names itself at run time; read the manifests, this doc deliberately retypes no host list). The kit **never seeds** `sandbox.network.allowedDomains` or `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur): a network pre-allow widens egress for EVERY sandboxed command, and a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command. Per-bridge picture under a harness-managed sandbox (live-observed 2026-07-11/12): an IDE-managed session sandbox ignores hand-applied `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host network consents govern egress; the durable zero-prompt lanes there are the session/host sandbox config (hosts from `networkHosts` + the CLI state-dir writes) or the per-run consented bypass. codex additionally needs a writable HOME (EROFS `~/.codex`); note the apex-vs-wildcard nuance — an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed (the blocked host names itself at run time). The upgrade Recommendations advisor
|
|
34
|
+
**The `--bridge-tier` (own opt-in, AD-044).** Seeds what a promptless council review run needs — BOTH surfaces: `permissions.allow` prefix rules AND the wrapper names in `sandbox.excludedCommands` (the harness runs an excluded command OUTSIDE the sandbox — the wrappers need network — so a plain allowlisted invocation triggers no sandbox-bypass approval). Both land in the **project** `.claude/settings.json` — the file this writer owns; an exclusion placed only in `settings.local.json` was live-observed NOT to route the command outside the sandbox (2026-07-11: the wrapper then starts sandboxed and dies on a read-only HOME + a network prompt), so hand-wiring the local file is not a working substitute for this tier. **Honesty note:** a session whose sandbox is imposed by the harness runtime itself (e.g. an IDE-managed session sandbox) may ignore settings-level exclusions entirely — there the wrappers need the session-level sandbox config (or a per-run consented bypass); the tier's seeded posture is correct for the settings-native sandbox and simply prompts again elsewhere (fail-safe, never a silent widening). Each bridge's observed egress hosts are declared in its `capability.json` `networkHosts` — the single documentation source (observed-minimal — a blocked host names itself at run time; read the manifests, this doc deliberately retypes no host list). The kit **never seeds** `sandbox.network.allowedDomains` or `sandbox.filesystem.allowWrite` (bridge council 2026-07-11, both backends concur): a network pre-allow widens egress for EVERY sandboxed command, and a write allowance on CLI state dirs (`~/.codex`, `~/.gemini/…`) would make credential dirs writable to every sandboxed command. Per-bridge picture under a harness-managed sandbox (live-observed 2026-07-11/12): an IDE-managed session sandbox ignores hand-applied `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH settings scopes — its own per-host network consents govern egress; the durable zero-prompt lanes there are the session/host sandbox config (hosts from `networkHosts` + the CLI state-dir writes) or the per-run consented bypass. codex additionally needs a writable HOME (EROFS `~/.codex`); note the apex-vs-wildcard nuance — an apex domain is NOT covered by its `*.`-wildcard form, so the manifests carry both forms where observed (the blocked host names itself at run time). The upgrade Recommendations advisor surfaces exactly this recipe (hosts ∪ resolved `writableDirs`) as the HAND-APPLY `sandbox-lane` discoverability item — converging on a neutral fingerprint acknowledgement, with the posture notes at the consent moment (`${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`). Membership is the FROZEN review-wrapper constant, **never** the execution/probe wrappers (`codex-exec`, `agy-run` keep their human prompt — delegated execution is not covered by this consent), and only the **`code` review mode** — a `plan`/`diff` invocation takes a file argument that can point OUTSIDE the repo, so those modes keep their prompt; each wrapper entry derives ONLY when its bridge is **placed on PATH** (an absent bridge is a stated skip). The seeded byte-forms (this list IS the documented-invocation source for the bridge tier):
|
|
35
35
|
|
|
36
36
|
- `Bash(codex-review code:*)` — the code-mode prefix, args wildcard; plus `codex-review` in `sandbox.excludedCommands`
|
|
37
37
|
- `Bash(agy-review code:*)` — the code-mode prefix, args wildcard; plus `agy-review` in `sandbox.excludedCommands`
|
|
@@ -76,7 +76,7 @@ Apply these when authoring a plan, reviewing, folding a finding, or editing code
|
|
|
76
76
|
- **Per-round emission.** Every review round emits **{round N · finding-origin tally · per-backend verdict}** so the crossover is a computed, visible signal, not a remembered rule.
|
|
77
77
|
- **Recipe fidelity.** Council runs every backend the recipe names, **every round**; silently dropping a ready backend for quota/convenience is a forbidden downgrade — an unavailable backend is a LOUD, stated degrade, never a quiet drop.
|
|
78
78
|
- **ExitPlanMode ≠ execute.** A harness "approved — start coding" prompt authorizes the PLAN only; this methodology overrides it. Continue into execution only as a DELIBERATE transition after the plan + cold-start prompt exist, never an implicit slide.
|
|
79
|
-
- **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation.
|
|
79
|
+
- **Cost lanes.** Route every step to the **cheapest adequate executor** — L0 deterministic script (the batched gate matrix over `gates.json`, the rotation `--check`s) · L1 cheap subagent (extraction/drafting only; the orchestrator verifies) · L2 subscription bridge · L3 frontier judgment. A step with **no named guardrail does not move down** a lane, and the **red lines never move down** (council review models · real code · ADR/handover/changelog-entry wording · persuasive copy · go/no-go · the approval asks). Own-error repair: salvage recorded state first (L0/L1, batched), never frontier re-derivation. **Prompt economy:** read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator's own shell form is ONE plain pipeline per call (a `;`/`&&` chain or env-prefixed invocation never matches a prefix allow rule); a fan-out launcher that gates per call yields to the agent-spawn lane — capability-gated: without restricted-tool vehicles (generic full-tool spawning does not count), read-only research stays in the orchestrator's own context, never a vehicle mandate a host cannot satisfy. Judgment, code, synthesis stay at the frontier lane (a task that genuinely runs/writes keeps a full-tool subagent); honest limit: no deterministic gate classifies a dispatch — canon at the point of use + placed vehicles + the retro loop.
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -27,7 +27,14 @@ import { SCHEMA_VERSION, REVIEW_CAP, V4_CLASSES, V4_OVERRIDE_SCOPES } from './re
|
|
|
27
27
|
import { HARD_MAX, DEFAULT_DIFF_CAP } from './review-ledger-write.mjs';
|
|
28
28
|
import { RESULT_SCHEMA_VERSION } from './fold-completeness.mjs';
|
|
29
29
|
import { EXIT as DOCTOR_EXIT, STATUS as DOCTOR_STATUS, TRUSTED_DIRS as DOCTOR_TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
RECOMMENDATIONS_SECTION_HEADER,
|
|
32
|
+
RECOMMENDATIONS_EMPTY_LINE,
|
|
33
|
+
VERDICT_ATTENTION_TEMPLATE,
|
|
34
|
+
VERDICT_NOTHING_BROKEN,
|
|
35
|
+
VERDICT_OPTIONAL_TEMPLATE,
|
|
36
|
+
VERDICT_SKIPS_TEMPLATE,
|
|
37
|
+
} from './recommendations.mjs';
|
|
31
38
|
|
|
32
39
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
40
|
|
|
@@ -75,11 +82,16 @@ export const BINDINGS = Object.freeze([
|
|
|
75
82
|
.filter((token) => token !== DOCTOR_STATUS.usage)
|
|
76
83
|
.map((token) => valueBinding(`doctor-status:${token}`, token, token, [AUTONOMY_DOCTOR_DOC])),
|
|
77
84
|
valueBinding('doctor-trusted-dirs', DOCTOR_TRUSTED_DIRS.join(':'), DOCTOR_TRUSTED_DIRS.join(':'), [AUTONOMY_DOCTOR_DOC]),
|
|
78
|
-
// The upgrade Recommendations section contract (AD-044 Plan 4): the section
|
|
79
|
-
// exact empty-state line must render in BOTH the
|
|
80
|
-
// them) — a reworded doc would silently break the
|
|
85
|
+
// The upgrade Recommendations section contract (AD-044 Plan 4 + REC-UX-REWORK D1): the section
|
|
86
|
+
// header, the exact empty-state line, and the frozen verdict templates must render in BOTH the
|
|
87
|
+
// mode doc and upgrade.md (both exits reference them) — a reworded doc would silently break the
|
|
88
|
+
// presentation contract (facts/counts complete, commands byte-exact).
|
|
81
89
|
valueBinding('recommendations-header', RECOMMENDATIONS_SECTION_HEADER, RECOMMENDATIONS_SECTION_HEADER, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
82
90
|
valueBinding('recommendations-empty-line', RECOMMENDATIONS_EMPTY_LINE, RECOMMENDATIONS_EMPTY_LINE, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
91
|
+
valueBinding('verdict-attention', VERDICT_ATTENTION_TEMPLATE, VERDICT_ATTENTION_TEMPLATE, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
92
|
+
valueBinding('verdict-nothing-broken', VERDICT_NOTHING_BROKEN, VERDICT_NOTHING_BROKEN, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
93
|
+
valueBinding('verdict-optional', VERDICT_OPTIONAL_TEMPLATE, VERDICT_OPTIONAL_TEMPLATE, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
94
|
+
valueBinding('verdict-skips', VERDICT_SKIPS_TEMPLATE, VERDICT_SKIPS_TEMPLATE, [RECOMMENDATIONS_DOC, UPGRADE_DOC]),
|
|
83
95
|
].map((b) => Object.freeze(b)));
|
|
84
96
|
|
|
85
97
|
// ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
|
package/tools/manifest/schema.md
CHANGED
|
@@ -19,6 +19,7 @@ expansion), never via the shell. The validator is [`validate.mjs`](./validate.mj
|
|
|
19
19
|
| `detect` | object | no | `installed` (skill on the machine) + `deployed` (substrate set up in cwd) |
|
|
20
20
|
| `settings` | array | no | the bridge's **settings-file surface** (typed; see *Settings*). Unlike `contract`, a malformed entry **fails** `--strict`. |
|
|
21
21
|
| `networkHosts` | string[] | no | the backend CLI's **observed egress host families** (see *Network hosts*). A malformed list **fails** `--strict`. |
|
|
22
|
+
| `writableDirs` | object[] | no | the backend CLI's **writable state-dir declarations** — `{env, default}` entries (see *Writable dirs*). A malformed list **fails** `--strict`. |
|
|
22
23
|
| `install` / `uninstall` | object | no | `install.npm` is a package name, not a path |
|
|
23
24
|
| `cost` / `quota` / `provenance` | misc | no | informational |
|
|
24
25
|
| `available` | boolean | no | `false` = a declared-but-not-installed stub; skips fs/version checks |
|
|
@@ -105,10 +106,36 @@ source** for a hand-applied sandbox/network allowlist (session sandbox config, o
|
|
|
105
106
|
- Observed-minimal, honestly incomplete: a blocked host names itself at run time — extend the
|
|
106
107
|
hand-applied list by hand; the manifest list records what was actually observed.
|
|
107
108
|
|
|
109
|
+
## Writable dirs (REC-UX-REWORK, D6)
|
|
110
|
+
|
|
111
|
+
`writableDirs` declares the backend CLI's **writable state dirs** (credentials, session state) as
|
|
112
|
+
`{env, default}` entries — a bare string cannot be truthful, because a wrapper resolves its state
|
|
113
|
+
dir from an env override first (e.g. `CODEX_HOME` as `~`, `~/…`, absolute or relative), so the
|
|
114
|
+
declared path is only the **default under no override**. Rules:
|
|
115
|
+
|
|
116
|
+
- Each entry: `env` is `null` or an **UPPER_SNAKE_CASE env-var name**; `default` is a
|
|
117
|
+
**`~/`-anchored or absolute POSIX path** — no globs, no trailing slash, no `..` traversal.
|
|
118
|
+
Defaults must be unique. A malformed list **fails** `--strict` (the resolved dir is rendered
|
|
119
|
+
into a hand-applied recipe line by the Recommendations advisor).
|
|
120
|
+
- **Resolution is the advisor's, at run time, mirroring the wrapper's byte-semantics:** a
|
|
121
|
+
NON-EMPTY env value wins (an EMPTY env value ≡ unset — the `${VAR:-default}` form); the
|
|
122
|
+
wrapper's exact case-arms apply — `~`, `~/…` and absolute forms ride as-given, EVERY other
|
|
123
|
+
form (including `~user/…`, which the wrappers never resolve as a home path) anchors like a
|
|
124
|
+
relative path. The advisor anchors those to the **target project root** (its pinned `--cwd`),
|
|
125
|
+
matching what a wrapper invoked from the project root resolves — the documented dispatch form
|
|
126
|
+
(the wrapper itself anchors to its invocation `$PWD`); else the `default` applies.
|
|
127
|
+
- Like `networkHosts`, this is a **documentation source**: the kit never seeds
|
|
128
|
+
`sandbox.filesystem.allowWrite` or any other filesystem allowance — the entries feed the
|
|
129
|
+
sandbox-lane discoverability item (session/host sandbox config is hand-applied territory).
|
|
130
|
+
|
|
108
131
|
## Path-field rules (Windows-safe, traversal-safe)
|
|
109
132
|
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
These rules govern the **repo/project-relative path fields** enumerated below (`source`,
|
|
134
|
+
`template`, `detect.*`) — NOT `writableDirs`, whose `default` entries are deliberately
|
|
135
|
+
`~/`-anchored or absolute **host** paths under their own section's rules above.
|
|
136
|
+
|
|
137
|
+
- **No absolute paths**, **no `..` traversal** in the fields below; **no unresolved placeholders**
|
|
138
|
+
(`{{…}}` / `${…}`) in any field.
|
|
112
139
|
- `source`, `template`, `detect.installed.file` — repo-relative **within the skill**; must exist
|
|
113
140
|
(skipped for `available:false` stubs).
|
|
114
141
|
- `detect.installed.default` — may be **home-relative** (`~/…`); resolved by the installer.
|
|
@@ -339,6 +339,44 @@ export const validateManifest = (skillDir) => {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// `writableDirs` (REC-UX-REWORK, D6): the backend CLI's writable state-dir declarations —
|
|
343
|
+
// {env, default} entries the Recommendations advisor RESOLVES at run time (a NON-EMPTY env
|
|
344
|
+
// value wins, else the default) and renders into the sandbox-lane recipe. Like `networkHosts`
|
|
345
|
+
// this is a DOCUMENTATION source (the kit never seeds filesystem allowances), and a malformed
|
|
346
|
+
// list FAILS --strict for the same reason: the resolved dir is rendered into a hand-applied line.
|
|
347
|
+
const writableDirs = manifest.writableDirs;
|
|
348
|
+
if (writableDirs != null) {
|
|
349
|
+
if (!Array.isArray(writableDirs) || writableDirs.length === 0) {
|
|
350
|
+
errors.push('`writableDirs` must be a non-empty array of {env, default} entries');
|
|
351
|
+
} else {
|
|
352
|
+
const seenDefaults = new Set();
|
|
353
|
+
writableDirs.forEach((entry, i) => {
|
|
354
|
+
const at = `\`writableDirs[${i}]\``;
|
|
355
|
+
if (entry == null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
356
|
+
errors.push(`${at} must be an {env, default} object`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (entry.env !== null && (typeof entry.env !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(entry.env))) {
|
|
360
|
+
errors.push(`${at}.env must be null or an UPPER_SNAKE_CASE env-var name (${JSON.stringify(entry.env)})`);
|
|
361
|
+
}
|
|
362
|
+
const dir = entry.default;
|
|
363
|
+
if (typeof dir !== 'string' || !(dir.startsWith('~/') || dir.startsWith('/'))) {
|
|
364
|
+
errors.push(`${at}.default must be a \`~/\`-anchored or absolute POSIX path (${JSON.stringify(dir)})`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (/[*?[\]]/.test(dir)) errors.push(`${at}.default must not carry glob characters ("${dir}")`);
|
|
368
|
+
// The resolved dir is rendered into a ONE-LINE hand-applied recipe — a control character
|
|
369
|
+
// (newline, CR, NUL, …) would break the line or the shell paste.
|
|
370
|
+
// eslint-disable-next-line no-control-regex
|
|
371
|
+
if (/[\x00-\x1f\x7f]/.test(dir)) errors.push(`${at}.default must not carry control characters (${JSON.stringify(dir)})`);
|
|
372
|
+
if (dir.endsWith('/')) errors.push(`${at}.default must not end with a trailing slash ("${dir}")`);
|
|
373
|
+
if (hasTraversal(dir)) errors.push(`${at}.default must not contain ".." traversal ("${dir}")`);
|
|
374
|
+
if (seenDefaults.has(dir)) errors.push(`duplicate writableDirs default "${dir}" (${at})`);
|
|
375
|
+
else seenDefaults.add(dir);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
342
380
|
if (!isStub) {
|
|
343
381
|
const auth = readAuthoritativeVersion(skillDir);
|
|
344
382
|
if (auth.version == null) errors.push(`could not resolve an authoritative version (${auth.from})`);
|
package/tools/procedures.mjs
CHANGED
|
@@ -297,6 +297,8 @@ const costLanesAdvice = () => [
|
|
|
297
297
|
' • L3 frontier — judgment: plan/fold/synthesis, ADR/handover/changelog-entry wording, persuasive copy, go/no-go, real code.',
|
|
298
298
|
' • A step with no named guardrail does not move down a lane; red lines never move down (council review models · real code · memory/copy wording · the maintainer approval asks).',
|
|
299
299
|
' • Sandbox lanes (under an OS sandbox): the L0 surfaces are sandbox-safe — gates/ledger/state/fold checks, git reads, plain no-network tests; the bridge wrappers are genuinely unsandboxed (network); npm-cache-touching commands are COMMAND-SHAPE dependent — first try the sandbox-safe shape (cache under $TMPDIR, offline/notifier off). Move ONLY the failing command out of the sandbox, never its class; BATCH consecutive unsandboxed calls.',
|
|
300
|
+
' • Prompt economy (autonomy-preserving dispatch): read-only fan-out (research/sweeps/extraction) runs ONLY on restricted-tool vehicles — a full-tool subagent for read-only work is a forbidden lane downgrade (invisible prompt-flood + blast radius), and a subagent is never told to shell out for facts obtainable read-only; the orchestrator\'s own shell form is ONE plain pipeline per call (a ;/&& chain or env-prefixed invocation never matches a prefix allow rule); a fan-out LAUNCHER that gates per call yields to the agent-spawn lane. Capability-gated: on a host with restricted-tool subagent vehicles use them; without restricted-tool vehicles (a host offering only full-tool agents included), read-only research stays in the orchestrator\'s own context — never a vehicle mandate a host cannot satisfy.',
|
|
301
|
+
' • The prompt-economy clause narrows TOOLS for read-only work only — judgment, code, synthesis stay at the frontier lane; a task that genuinely needs to run or write keeps a full-tool subagent. Honest limit: no deterministic gate classifies a dispatch — enforcement is the canon at the point of use + the placed vehicles + the retro loop.',
|
|
300
302
|
];
|
|
301
303
|
|
|
302
304
|
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
@@ -6,19 +6,23 @@
|
|
|
6
6
|
// bridge allowlist, autonomy render drifted, sandbox not provisioned, gates undeclared) — every
|
|
7
7
|
// `upgrade` run therefore ends with a mandatory, deterministic Recommendations section: what is
|
|
8
8
|
// sub-optimal · the benefit in ONE plain line · the exact consent-gated apply one-liner. The agent
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// PRESENTS the section in the user's conversational language — every fact, count and item from
|
|
10
|
+
// the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; the raw
|
|
11
|
+
// tool block shown on request (the AD-032 report-contract lane). The user picks items in plain
|
|
12
|
+
// language; after the per-item consent moment the agent runs EXACTLY the rendered one-liners — no
|
|
13
|
+
// improvisation, each writer's own consent semantics intact.
|
|
12
14
|
//
|
|
13
15
|
// Contract:
|
|
14
16
|
// node recommendations.mjs --cwd <project-root> [--json]
|
|
15
17
|
// --cwd is REQUIRED (subdir-proof: the target project is explicit, never inferred from the shell's
|
|
16
18
|
// current directory). The section renders PRESENT-EVEN-WHEN-EMPTY (the exact empty-state line
|
|
17
|
-
// below)
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// item. The
|
|
21
|
-
// backends concur): the kit never seeds
|
|
19
|
+
// below) and VERDICT-FIRST (D1): every non-optimal state opens with ONE composed verdict line.
|
|
20
|
+
// Registry strings are frozen tool DATA, fact-true, one line under the shape cap (D2); posture/
|
|
21
|
+
// risk prose lives in the mode doc at the consent moment (D3). A probe failure is a stated
|
|
22
|
+
// skipped-item line — never a crash, never a fabricated item. The sandbox-lane item is HAND-APPLY
|
|
23
|
+
// by design (bridge council 2026-07-11, both backends concur): the kit never seeds
|
|
24
|
+
// sandbox.network.allowedDomains / filesystem.allowWrite; its convergence is a NEUTRAL
|
|
25
|
+
// fingerprint-bound acknowledgement, never a security key (D4).
|
|
22
26
|
//
|
|
23
27
|
// Read-only: never writes, never commits, never runs a subscription CLI. The reused probes are all
|
|
24
28
|
// exported read-only surfaces of their owning tools (velocity/autonomy/doctor/backends/recipes/
|
|
@@ -26,6 +30,8 @@
|
|
|
26
30
|
// Dependency-free, Node >= 18. No side effects on import (the isDirectRun idiom).
|
|
27
31
|
|
|
28
32
|
import { readFileSync, readdirSync, lstatSync } from 'node:fs';
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import { homedir } from 'node:os';
|
|
29
35
|
import { dirname, join, resolve } from 'node:path';
|
|
30
36
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
37
|
import {
|
|
@@ -60,24 +66,141 @@ export const RECOMMENDATIONS_EMPTY_LINE = 'no recommendations — flow optimal.'
|
|
|
60
66
|
// The one dual-wording security clause — rides ONLY the items with a real security delta.
|
|
61
67
|
export const DUAL_SECURITY_BENEFIT = 'safer — blast radius bounded by the OS sandbox, not human attention';
|
|
62
68
|
|
|
69
|
+
// ── the verdict-first contract (D1, REC-UX-REWORK) ──────────────────────────────────────────────
|
|
70
|
+
// The optimal state (no items, no skips) renders the frozen empty-state line ALONE — byte-identical
|
|
71
|
+
// to the pre-verdict contract. Every other state opens the body with ONE verdict line composed from
|
|
72
|
+
// these frozen templates ({X}-style placeholders; the "(s)" invariant form IS the pinned
|
|
73
|
+
// pluralization rule — no singular/plural branching). The templates are English tool DATA
|
|
74
|
+
// (doc-parity-bound in both mode docs); user-language rendering is the agent's presentation layer.
|
|
75
|
+
export const VERDICT_ATTENTION_TEMPLATE = '{K} item(s) need attention';
|
|
76
|
+
export const VERDICT_NOTHING_BROKEN = 'nothing is broken';
|
|
77
|
+
export const VERDICT_OPTIONAL_TEMPLATE = '{N} optional recommendation(s), apply any you want';
|
|
78
|
+
export const VERDICT_SKIPS_TEMPLATE = 'optimality NOT attested — {M} probe check(s) skipped';
|
|
79
|
+
|
|
80
|
+
// ── the frozen severity registry (D1; pinned by tests) ─────────────────────────────────────────
|
|
81
|
+
// `attention` — the item reports a CONFIGURED declaration that is broken, drifted, degrading or
|
|
82
|
+
// invalid (the deployment needs review); `optional` — an offer to enable an unconfigured
|
|
83
|
+
// capability. One class per key, frozen registry data; a `<key>.<variant>` entry classes a
|
|
84
|
+
// per-site arm whose semantics differ from its base (the invalid-env arm reports an INVALID
|
|
85
|
+
// configured value — attention — while the unset arm stays an offer).
|
|
86
|
+
export const SEVERITY_ATTENTION = 'attention';
|
|
87
|
+
export const SEVERITY_OPTIONAL = 'optional';
|
|
88
|
+
export const SEVERITIES = Object.freeze({
|
|
89
|
+
'velocity-core': SEVERITY_OPTIONAL,
|
|
90
|
+
'kit-tools-tier': SEVERITY_OPTIONAL,
|
|
91
|
+
'bridge-tier': SEVERITY_OPTIONAL,
|
|
92
|
+
'autonomy-policy': SEVERITY_OPTIONAL,
|
|
93
|
+
'autonomy-render': SEVERITY_ATTENTION,
|
|
94
|
+
'sandbox-provision': SEVERITY_OPTIONAL,
|
|
95
|
+
'review-recipe': SEVERITY_ATTENTION,
|
|
96
|
+
'gates-declaration': SEVERITY_OPTIONAL,
|
|
97
|
+
'gate-hook': SEVERITY_OPTIONAL,
|
|
98
|
+
'family-freshness': SEVERITY_ATTENTION,
|
|
99
|
+
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
100
|
+
'agy-adddir': SEVERITY_OPTIONAL,
|
|
101
|
+
'agy-adddir.invalid-env': SEVERITY_ATTENTION,
|
|
102
|
+
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
103
|
+
});
|
|
104
|
+
// The per-item render tags (frozen presentation data, same language contract as the templates).
|
|
105
|
+
export const SEVERITY_LABELS = Object.freeze({
|
|
106
|
+
[SEVERITY_ATTENTION]: 'needs attention',
|
|
107
|
+
[SEVERITY_OPTIONAL]: 'optional',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// {X}-style template fill (D1/D2): every placeholder must be supplied — a miss is a programming
|
|
111
|
+
// error that surfaces through the probe's stated-skip lane, never a rendered "{K}".
|
|
112
|
+
const fillTemplate = (template, values) => template.replace(/\{([A-Za-z]+)\}/g, (_, name) => {
|
|
113
|
+
if (!(name in values)) throw new Error(`unfilled template placeholder {${name}}`);
|
|
114
|
+
return String(values[name]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// composeVerdict(counts) → the ONE verdict line, or null for the optimal state (D1 state matrix).
|
|
118
|
+
// attention>0 leads; the "nothing is broken" wording renders ONLY when attention==0 AND skipped==0
|
|
119
|
+
// (a skipped probe could hide an attention-class problem, so the claim would overreach; it renders
|
|
120
|
+
// only as the lead-in to the optional offer, never in a skips-only state); the skips part is
|
|
121
|
+
// appended last.
|
|
122
|
+
export const composeVerdict = ({ attention, optional, skipped }) => {
|
|
123
|
+
if (attention === 0 && optional === 0 && skipped === 0) return null;
|
|
124
|
+
const parts = [];
|
|
125
|
+
if (attention > 0) parts.push(fillTemplate(VERDICT_ATTENTION_TEMPLATE, { K: attention }));
|
|
126
|
+
if (optional > 0) {
|
|
127
|
+
const offer = fillTemplate(VERDICT_OPTIONAL_TEMPLATE, { N: optional });
|
|
128
|
+
parts.push(attention === 0 && skipped === 0 ? `${VERDICT_NOTHING_BROKEN} — ${offer}` : offer);
|
|
129
|
+
}
|
|
130
|
+
if (skipped > 0) parts.push(fillTemplate(VERDICT_SKIPS_TEMPLATE, { M: skipped }));
|
|
131
|
+
return parts.join('; ');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ── the frozen WHAT-template registry (D2; pinned by the static shape test) ─────────────────────
|
|
135
|
+
// Every static WHAT template lives here — `<key>` is the item key, `<key>.<variant>` a per-site
|
|
136
|
+
// variant of the same item — so ALL variants are assertable at build time (single line, char cap,
|
|
137
|
+
// banned tokens), never a fixture-coverage gamble. A pure-placeholder template marks a WHAT whose
|
|
138
|
+
// content is fully dynamic (capped at composition by truncation-with-count).
|
|
139
|
+
export const WHATS = Object.freeze({
|
|
140
|
+
'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
|
|
141
|
+
'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
|
|
142
|
+
'bridge-tier': 'council review runs prompt per bridge invocation — {n} bridge-wrappers tier entr(ies) not seeded (placed bridges only, code mode only)',
|
|
143
|
+
'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
|
|
144
|
+
'autonomy-render': 'the declared autonomy policy is not rendered into .claude/settings.json — drift: {drift}',
|
|
145
|
+
'sandbox-provision': 'the OS sandbox is unavailable: {reason}',
|
|
146
|
+
'sandbox-provision.installable': 'the OS sandbox is unavailable: {reason} — installable via the doctor (consent tuple {tuple})',
|
|
147
|
+
'review-recipe': '{degraded}',
|
|
148
|
+
'gates-declaration': 'no declared gate matrix (docs/ai/gates.json absent or empty) — gates prompt one by one; the apply PREVIEWS its --apply line, writes nothing',
|
|
149
|
+
'gate-hook': '{n} declared gate(s) prompt per run — the gate-approval hook is not wired',
|
|
150
|
+
'family-freshness': '{parts}',
|
|
151
|
+
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
152
|
+
'sandbox-masks.stale-real': '{n} sandbox device mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
|
|
153
|
+
'agy-adddir': 'agy-review is placed but AGY_REVIEW_ALLOW_ADDDIR is not set ({file}) — an oversized code review refuses instead of offloading',
|
|
154
|
+
'agy-adddir.invalid-env': 'AGY_REVIEW_ALLOW_ADDDIR is set to an INVALID value ({value}) — refuse-mode applies and the settings file is shadowed while it is set',
|
|
155
|
+
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── the shape contract (D2): registry strings AND composed items stay one line under the cap ────
|
|
159
|
+
export const ITEM_LINE_CAP = 140;
|
|
160
|
+
export const SKIP_REASON_CAP = 200;
|
|
161
|
+
|
|
162
|
+
const oneLineOf = (text) => String(text).replace(/\s*[\r\n]+\s*/g, ' ').trim();
|
|
163
|
+
// Scalar truncation-with-count — a capped value states what it dropped, never a silent cut.
|
|
164
|
+
// GUARANTEED result.length <= cap for every input/budget: when even the count note cannot fit
|
|
165
|
+
// the budget, the tail arm hard-slices to a bare ellipsis instead of overflowing.
|
|
166
|
+
const truncatedTo = (text, cap) => {
|
|
167
|
+
if (text.length <= cap) return text;
|
|
168
|
+
const note = (dropped) => `… (+${dropped} more chars)`;
|
|
169
|
+
let keep = cap;
|
|
170
|
+
while (keep > 0 && keep + note(text.length - keep).length > cap) keep -= 1;
|
|
171
|
+
if (keep === 0) return cap <= 0 ? '' : `${text.slice(0, cap - 1)}…`;
|
|
172
|
+
return text.slice(0, keep) + note(text.length - keep);
|
|
173
|
+
};
|
|
174
|
+
// List truncation-with-count: whole leading entries + " (+N more)" for the dropped tail; if even
|
|
175
|
+
// the first entry overflows, it is scalar-truncated so the count survives.
|
|
176
|
+
const capList = (entries, budget, sep = '; ') => {
|
|
177
|
+
for (let take = entries.length; take >= 1; take -= 1) {
|
|
178
|
+
const joined = entries.slice(0, take).join(sep);
|
|
179
|
+
const tail = take < entries.length ? ` (+${entries.length - take} more)` : '';
|
|
180
|
+
if (joined.length + tail.length <= budget) return joined + tail;
|
|
181
|
+
}
|
|
182
|
+
const tail = entries.length > 1 ? ` (+${entries.length - 1} more)` : '';
|
|
183
|
+
return truncatedTo(entries[0], Math.max(0, budget - tail.length)) + tail;
|
|
184
|
+
};
|
|
185
|
+
// The char budget a template leaves for its placeholder values.
|
|
186
|
+
const templateBudget = (template) => ITEM_LINE_CAP - template.replace(/\{[A-Za-z]+\}/g, '').length;
|
|
187
|
+
|
|
63
188
|
// ── the frozen benefit registry (fact-true; pinned by tests) ────────────────────────────────────
|
|
64
189
|
export const BENEFITS = Object.freeze({
|
|
65
190
|
'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away',
|
|
66
191
|
'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
|
|
67
192
|
'bridge-tier':
|
|
68
|
-
'velocity —
|
|
193
|
+
'velocity — placed review wrappers run code-mode council reviews promptless (plan/diff modes and delegated execution keep their prompt)',
|
|
69
194
|
'autonomy-policy': 'clarity — the per-activity autonomy policy becomes an explicit, versioned declaration instead of implicit computed defaults',
|
|
70
|
-
'autonomy-render': `velocity —
|
|
71
|
-
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting
|
|
195
|
+
'autonomy-render': `velocity — confined commands auto-allow per your declared policy; ${DUAL_SECURITY_BENEFIT}`,
|
|
196
|
+
'sandbox-provision': `velocity — confined ad-hoc commands stop prompting; ${DUAL_SECURITY_BENEFIT}`,
|
|
72
197
|
'review-recipe': 'review coverage — the review recipe you configured actually runs instead of silently degrading',
|
|
73
198
|
'gates-declaration': 'velocity — your project’s gates run as ONE declared batch with a PASS/FAIL table',
|
|
74
199
|
'gate-hook': 'velocity — your own declared gate commands auto-approve byte-exactly (opt-in PreToolUse hook)',
|
|
75
200
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
76
201
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
77
|
-
'agy-adddir':
|
|
78
|
-
|
|
79
|
-
'network-allowlist':
|
|
80
|
-
'unblocks the NETWORK half of in-sandbox bridge runs where the sandbox honors settings keys (settings-native harnesses; the network gate ONLY — no filesystem allowance is recommended); RISK stated plainly: pre-allows egress to these hosts for EVERY sandboxed command (informed hand-consent only). Live-observed 2026-07-12: an IDE-managed session sandbox ignores these settings keys too — there the durable lanes are the harness’s own per-host network consents / session sandbox config, or the per-run consented bypass; codex additionally needs a writable HOME (EROFS ~/.codex)',
|
|
202
|
+
'agy-adddir': 'large reviews — an oversized agy code review offloads to a staging dir instead of refusing',
|
|
203
|
+
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
81
204
|
});
|
|
82
205
|
|
|
83
206
|
// A typed usage failure (exit 2) — the codebase's typed-error idiom (no classes).
|
|
@@ -103,12 +226,12 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
103
226
|
// above owns the real failure modes).
|
|
104
227
|
const core = planVelocityProfile(preflight, {});
|
|
105
228
|
if (core.toAdd.length > 0) {
|
|
106
|
-
add('velocity-core',
|
|
229
|
+
add('velocity-core', fillTemplate(WHATS['velocity-core'], { n: core.toAdd.length }), applyLine(''));
|
|
107
230
|
}
|
|
108
231
|
try {
|
|
109
232
|
const kt = planVelocityProfile(preflight, { kitTools: true });
|
|
110
233
|
if (kt.tierToAdd.length > 0) {
|
|
111
|
-
add('kit-tools-tier',
|
|
234
|
+
add('kit-tools-tier', fillTemplate(WHATS['kit-tools-tier'], { n: kt.tierToAdd.length }), applyLine(' --kit-tools'));
|
|
112
235
|
}
|
|
113
236
|
} catch (err) {
|
|
114
237
|
skip('kit-tools-tier', err);
|
|
@@ -117,7 +240,7 @@ const probeVelocityItems = ({ root, deps, add, skip }) => {
|
|
|
117
240
|
const bt = planVelocityProfile(preflight, { bridgeTier: true, findWrapper: deps.findWrapper });
|
|
118
241
|
const delta = bt.bridgeToAdd.length + bt.excludedToAdd.length;
|
|
119
242
|
if (delta > 0) {
|
|
120
|
-
add('bridge-tier',
|
|
243
|
+
add('bridge-tier', fillTemplate(WHATS['bridge-tier'], { n: delta }), applyLine(' --bridge-tier'));
|
|
121
244
|
}
|
|
122
245
|
} catch (err) {
|
|
123
246
|
skip('bridge-tier', err);
|
|
@@ -134,7 +257,7 @@ const probeAutonomyItems = ({ root, deps, add, skip }) => {
|
|
|
134
257
|
// declaration: its render check still runs below (codex, Segment B closing).
|
|
135
258
|
if (source !== 'none' && isSparseSeedConfig(config)) return;
|
|
136
259
|
if (source === 'none') {
|
|
137
|
-
add('autonomy-policy',
|
|
260
|
+
add('autonomy-policy', fillTemplate(WHATS['autonomy-policy'], { path: AUTONOMY_REL }), '/agent-workflow-kit set-autonomy (run IN the target project — the conversational writer previews, then writes its docs/ai/autonomy.json)');
|
|
138
261
|
}
|
|
139
262
|
} catch (err) {
|
|
140
263
|
skip('autonomy-policy', err);
|
|
@@ -144,7 +267,8 @@ const probeAutonomyItems = ({ root, deps, add, skip }) => {
|
|
|
144
267
|
try {
|
|
145
268
|
const check = checkAutonomyProfile({ cwd: root }, deps);
|
|
146
269
|
if (!check.inSync) {
|
|
147
|
-
|
|
270
|
+
const drift = capList(check.drift, templateBudget(WHATS['autonomy-render']));
|
|
271
|
+
add('autonomy-render', fillTemplate(WHATS['autonomy-render'], { drift }), `node ${q(toolPath('velocity-profile.mjs'))} --autonomy --apply --cwd ${q(root)}`);
|
|
148
272
|
}
|
|
149
273
|
} catch (err) {
|
|
150
274
|
skip('autonomy-render', err);
|
|
@@ -156,10 +280,11 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
|
|
|
156
280
|
const p = probeSandboxAvailability(deps);
|
|
157
281
|
if (p.available) return;
|
|
158
282
|
const plan = deriveDoctorPlan({ probeResult: p, env: deps.env ?? process.env, isExec: deps.isExecutable ?? isExecutableFile });
|
|
159
|
-
const
|
|
283
|
+
const variant = plan.tuple ? 'sandbox-provision.installable' : 'sandbox-provision';
|
|
284
|
+
const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (plan.tuple ? String(plan.tuple).length : 0));
|
|
160
285
|
// The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
|
|
161
286
|
// pins the target project via a cd prefix (codex R2, Segment B).
|
|
162
|
-
add('sandbox-provision',
|
|
287
|
+
add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }), `cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`);
|
|
163
288
|
} catch (err) {
|
|
164
289
|
skip('sandbox-provision', err);
|
|
165
290
|
}
|
|
@@ -179,7 +304,7 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
179
304
|
}
|
|
180
305
|
}
|
|
181
306
|
if (degraded.length > 0) {
|
|
182
|
-
add('review-recipe',
|
|
307
|
+
add('review-recipe', fillTemplate(WHATS['review-recipe'], { degraded: capList(degraded, templateBudget(WHATS['review-recipe'])) }), '/agent-workflow-kit backends');
|
|
183
308
|
}
|
|
184
309
|
} catch (err) {
|
|
185
310
|
skip('review-recipe', err);
|
|
@@ -196,13 +321,13 @@ const probeGates = ({ root, deps, add, skip }) => {
|
|
|
196
321
|
// scripts and writes only on an explicit yes) — never the runner.
|
|
197
322
|
if (!sg.declarationPresent || sg.declaredGates === 0) {
|
|
198
323
|
// The seeder writes ONLY with --apply and consent is per-entry (--only) — the apply field
|
|
199
|
-
// stays a PURE executable command (run-exactly-
|
|
324
|
+
// stays a PURE executable command (run-exactly-as-rendered feeds it to the shell); the
|
|
200
325
|
// two-step preview semantics live in WHAT, never as prose appended to the command.
|
|
201
|
-
add('gates-declaration', '
|
|
326
|
+
add('gates-declaration', fillTemplate(WHATS['gates-declaration'], {}), `node ${q(toolPath('seed-gates.mjs'))} --cwd ${q(root)}`);
|
|
202
327
|
return;
|
|
203
328
|
}
|
|
204
329
|
if (sg.declaredGates > 0 && !sg.wired) {
|
|
205
|
-
add('gate-hook',
|
|
330
|
+
add('gate-hook', fillTemplate(WHATS['gate-hook'], { n: sg.declaredGates }), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`);
|
|
206
331
|
}
|
|
207
332
|
} catch (err) {
|
|
208
333
|
skip('gate-hook', err);
|
|
@@ -228,7 +353,7 @@ const probeFamilyFreshness = ({ deps, add, skip }) => {
|
|
|
228
353
|
// ALL caveats per row — a memory missing BOTH templates must not drop the second (codex).
|
|
229
354
|
...caveated.map((r) => `${r.name}: ${r.caveats.join('; ')}`),
|
|
230
355
|
];
|
|
231
|
-
add('family-freshness',
|
|
356
|
+
add('family-freshness', fillTemplate(WHATS['family-freshness'], { parts: capList(parts, templateBudget(WHATS['family-freshness'])) }), 'npx @sabaiway/agent-workflow-kit@latest init');
|
|
232
357
|
} catch (err) {
|
|
233
358
|
skip('family-freshness', err);
|
|
234
359
|
}
|
|
@@ -239,11 +364,11 @@ const probeMasksItem = ({ root, deps, add, skip }) => {
|
|
|
239
364
|
const p = probeSandboxMasks({ cwd: root, ...deps });
|
|
240
365
|
if (p == null) return; // not a git work tree — the lane is N/A, not sub-optimal
|
|
241
366
|
if (!needsMasksApply(p)) return;
|
|
242
|
-
const
|
|
367
|
+
const variant = p.staleReal.length > 0 ? 'sandbox-masks.stale-real' : 'sandbox-masks';
|
|
243
368
|
// A stale-real-only fence (EMPTY derivation over a non-empty block) makes the plain --apply
|
|
244
369
|
// REFUSE — the exact one-liner must carry --clear there (codex R1, Segment B).
|
|
245
370
|
const apply = p.masks.length === 0 && p.staleReal.length > 0 ? `${p.applyCmd} --clear` : p.applyCmd;
|
|
246
|
-
add('sandbox-masks',
|
|
371
|
+
add('sandbox-masks', fillTemplate(WHATS[variant], { n: p.masks.length, m: p.staleReal.length }), apply);
|
|
247
372
|
} catch (err) {
|
|
248
373
|
skip('sandbox-masks', err);
|
|
249
374
|
}
|
|
@@ -265,7 +390,8 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
|
|
|
265
390
|
if (env.AGY_REVIEW_ALLOW_ADDDIR === '') return;
|
|
266
391
|
// env > file: while ANY env value is set the wrapper ignores the settings file, so the file
|
|
267
392
|
// writer cannot fix an invalid env — the honest apply is to fix/unset the env var (codex).
|
|
268
|
-
|
|
393
|
+
const value = truncatedTo(oneLineOf(JSON.stringify(env.AGY_REVIEW_ALLOW_ADDDIR)), templateBudget(WHATS['agy-adddir.invalid-env']));
|
|
394
|
+
add('agy-adddir', fillTemplate(WHATS['agy-adddir.invalid-env'], { value }), 'HAND-APPLY: unset AGY_REVIEW_ALLOW_ADDDIR in the environment (or export it as 1), THEN configure it durably via the bridge-settings writer', 'agy-adddir.invalid-env');
|
|
269
395
|
return;
|
|
270
396
|
}
|
|
271
397
|
const confPath = settingsPath({ getenv: env, home: deps.home });
|
|
@@ -285,7 +411,7 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
|
|
|
285
411
|
// The settings writer REFUSES a duplicate-carrying file — rendering its command would hand
|
|
286
412
|
// the user a guaranteed failure; the honest apply is fix-duplicates-first (codex terminal).
|
|
287
413
|
const dups = duplicateKeys(parsed);
|
|
288
|
-
const what =
|
|
414
|
+
const what = fillTemplate(WHATS['agy-adddir'], { file: SETTINGS_FILENAME });
|
|
289
415
|
if (dups.length > 0) {
|
|
290
416
|
add('agy-adddir', what, `HAND-APPLY: ${SETTINGS_FILENAME} carries duplicate key(s) (${dups.join(', ')}) and the settings writer refuses to edit it — remove the duplicate lines by hand, THEN run: node ${q(toolPath('bridge-settings.mjs'))} --set AGY_REVIEW_ALLOW_ADDDIR=1 --apply`);
|
|
291
417
|
return;
|
|
@@ -296,16 +422,18 @@ const probeAgyAdddir = ({ deps, add, skip }) => {
|
|
|
296
422
|
}
|
|
297
423
|
};
|
|
298
424
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
425
|
+
// The manifest-declared session-sandbox recipe surfaces of every BUNDLED bridge whose review
|
|
426
|
+
// wrapper is in the wired set — networkHosts ∪ writableDirs, derived from the manifests (the
|
|
427
|
+
// single documentation source), never hardcoded here.
|
|
428
|
+
const bundledSandboxRecipe = (placedWrappers, deps) => {
|
|
302
429
|
const readFile = deps.readFile ?? readFileSync;
|
|
303
430
|
const readDir = deps.readdir ?? readdirSync;
|
|
304
431
|
const bundleRoot = deps.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
|
|
305
432
|
const hosts = [];
|
|
433
|
+
const dirEntries = [];
|
|
306
434
|
for (const dir of readDir(bundleRoot)) {
|
|
307
|
-
// An unreadable/unparsable bundled manifest must NOT thin the
|
|
308
|
-
//
|
|
435
|
+
// An unreadable/unparsable bundled manifest must NOT thin the recipe silently — a partial
|
|
436
|
+
// recipe rendered as complete is worse than no item. The throw reaches the probe's catch and
|
|
309
437
|
// becomes a stated skip.
|
|
310
438
|
let manifest;
|
|
311
439
|
try {
|
|
@@ -316,14 +444,58 @@ const bundledNetworkHosts = (placedWrappers, deps) => {
|
|
|
316
444
|
throw new Error(`bundled manifest unreadable: ${join(dir, 'capability.json')} — ${err?.message ?? err}`);
|
|
317
445
|
}
|
|
318
446
|
const reviewCmd = manifest?.roles?.review?.cmd;
|
|
319
|
-
if (reviewCmd
|
|
447
|
+
if (!reviewCmd || !placedWrappers.includes(reviewCmd)) continue;
|
|
448
|
+
if (Array.isArray(manifest.networkHosts)) {
|
|
320
449
|
for (const h of manifest.networkHosts) if (!hosts.includes(h)) hosts.push(h);
|
|
321
450
|
}
|
|
451
|
+
if (Array.isArray(manifest.writableDirs)) dirEntries.push(...manifest.writableDirs);
|
|
322
452
|
}
|
|
323
|
-
return hosts;
|
|
453
|
+
return { hosts, dirEntries };
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// D6 resolution, mirroring the wrappers' byte-semantics (`${VAR:-default}` + the exact case-arms:
|
|
457
|
+
// `~` / `~/…` / `/…` ride as-given; EVERY other form — including `~user/…`, which the wrappers
|
|
458
|
+
// never resolve as a home path — anchors like a relative path). The advisor anchors to the TARGET
|
|
459
|
+
// PROJECT ROOT (the pinned --cwd), matching what a wrapper invoked from the project root resolves
|
|
460
|
+
// (the documented dispatch form; the wrapper itself anchors to its invocation $PWD).
|
|
461
|
+
const resolveWritableDir = (entry, { env, root }) => {
|
|
462
|
+
const value = entry.env == null ? '' : (env[entry.env] ?? '');
|
|
463
|
+
if (value === '') return entry.default;
|
|
464
|
+
if (value === '~' || value.startsWith('~/') || value.startsWith('/')) return value;
|
|
465
|
+
return resolve(root, value);
|
|
324
466
|
};
|
|
325
467
|
|
|
326
|
-
|
|
468
|
+
// The NEUTRAL recipe fingerprint (D4): a hash over the resolved hosts ∪ dirs data — an
|
|
469
|
+
// acknowledgement token, never a security key. Canonical form is HOME-SYMBOLIC: an
|
|
470
|
+
// absolute dir under the resolved home canonicalizes BACK to its `~/…` form and tilde forms stay
|
|
471
|
+
// symbolic — so `~/.codex` and its absolute expansion acknowledge the SAME recipe AND the default
|
|
472
|
+
// recipe's fingerprint is identical across machines/users (a committed project-scope ack never
|
|
473
|
+
// churns between them); only a genuinely-outside-home absolute override stays absolute
|
|
474
|
+
// (machine-specific by nature). Any change to the recipe re-fires the item.
|
|
475
|
+
export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
476
|
+
const homeAbs = resolve(home);
|
|
477
|
+
const norm = (d) => {
|
|
478
|
+
if (d === '~') return '~';
|
|
479
|
+
if (d.startsWith('~/')) return `~/${d.slice(2)}`;
|
|
480
|
+
const abs = resolve(d);
|
|
481
|
+
if (abs === homeAbs) return '~';
|
|
482
|
+
return abs.startsWith(`${homeAbs}/`) ? `~/${abs.slice(homeAbs.length + 1)}` : abs;
|
|
483
|
+
};
|
|
484
|
+
const canonical = JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() });
|
|
485
|
+
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// The kit-owned neutral ack namespace (D4): project-scoped, hand-applicable as one line, read from
|
|
489
|
+
// BOTH settings scopes; the sandbox/permissions security keys are NEVER consulted as an ack.
|
|
490
|
+
export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
|
|
491
|
+
export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
|
|
492
|
+
|
|
493
|
+
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
494
|
+
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
495
|
+
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
496
|
+
export const RISK_NOTED_KEYS = Object.freeze(['agy-adddir', 'sandbox-lane']);
|
|
497
|
+
|
|
498
|
+
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
327
499
|
try {
|
|
328
500
|
const settings = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root });
|
|
329
501
|
const localSettings = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root });
|
|
@@ -331,8 +503,8 @@ const probeNetworkAllowlist = ({ root, deps, add, skip }) => {
|
|
|
331
503
|
const excluded = Array.isArray(sandbox?.excludedCommands) ? sandbox.excludedCommands : [];
|
|
332
504
|
const probePlaced = deps.findWrapper ?? ((cmd) => findOnPath(cmd, deps).state === 'present');
|
|
333
505
|
// Wired = the FULL two-surface tier proof (excludedCommands + the code-mode allow rule, either
|
|
334
|
-
// scope) — surfacing the
|
|
335
|
-
//
|
|
506
|
+
// scope) — surfacing the recipe while the tier is half-configured would front-run the
|
|
507
|
+
// bridge-tier item (codex terminal). Byte-form from the tier's own constants.
|
|
336
508
|
const allowRules = [
|
|
337
509
|
...(Array.isArray(settings.data?.permissions?.allow) ? settings.data.permissions.allow : []),
|
|
338
510
|
...(Array.isArray(localSettings.data?.permissions?.allow) ? localSettings.data.permissions.allow : []),
|
|
@@ -341,27 +513,26 @@ const probeNetworkAllowlist = ({ root, deps, add, skip }) => {
|
|
|
341
513
|
(w) => excluded.includes(w) && probePlaced(w) && allowRules.includes(`Bash(${w} ${BRIDGE_REVIEW_MODE}:*)`),
|
|
342
514
|
);
|
|
343
515
|
if (wired.length === 0) return; // the tier is not (fully) wired — the bridge-tier item covers first
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const hostsJson = finalList.map((h) => JSON.stringify(h)).join(', ');
|
|
516
|
+
const { hosts, dirEntries } = bundledSandboxRecipe(wired, deps);
|
|
517
|
+
const env = deps.getenv ?? process.env;
|
|
518
|
+
const home = deps.home ?? homedir();
|
|
519
|
+
const dirs = [];
|
|
520
|
+
for (const entry of dirEntries) {
|
|
521
|
+
const resolved = resolveWritableDir(entry, { env, root });
|
|
522
|
+
if (!dirs.includes(resolved)) dirs.push(resolved);
|
|
523
|
+
}
|
|
524
|
+
const fingerprint = recipeFingerprint({ hosts, dirs, home });
|
|
525
|
+
// Convergence is the NEUTRAL fingerprint-bound acknowledgement, read from BOTH scopes — a
|
|
526
|
+
// changed recipe (hosts, dirs, or an env override) re-fires the item (D4).
|
|
527
|
+
const acks = [settings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY], localSettings.data?.[SANDBOX_LANE_ACK_PARENT]?.[SANDBOX_LANE_ACK_KEY]];
|
|
528
|
+
if (acks.includes(fingerprint)) return; // the acknowledged recipe — the item converged
|
|
358
529
|
add(
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
`HAND-APPLY (
|
|
530
|
+
'sandbox-lane',
|
|
531
|
+
fillTemplate(WHATS['sandbox-lane'], {}),
|
|
532
|
+
`HAND-APPLY (a neutral acknowledgement, never a security key): recipe — egress hosts [${hosts.join(', ')}]; writable state dirs [${dirs.join(', ')}] (observed-minimal; a blocked host names itself at run time); what to DO with it per host class: the mode doc's sandbox-lanes section; once handled, record the ack: set "${SANDBOX_LANE_ACK_PARENT}"."${SANDBOX_LANE_ACK_KEY}" to "${fingerprint}" in .claude/settings.json or settings.local.json — a MERGE into the existing ${SANDBOX_LANE_ACK_PARENT} object (keep its other keys; create it only if absent)`,
|
|
362
533
|
);
|
|
363
534
|
} catch (err) {
|
|
364
|
-
skip('
|
|
535
|
+
skip('sandbox-lane', err);
|
|
365
536
|
}
|
|
366
537
|
};
|
|
367
538
|
|
|
@@ -375,31 +546,58 @@ const PROBES = Object.freeze([
|
|
|
375
546
|
probeFamilyFreshness,
|
|
376
547
|
probeMasksItem,
|
|
377
548
|
probeAgyAdddir,
|
|
378
|
-
|
|
549
|
+
probeSandboxLane,
|
|
379
550
|
]);
|
|
380
551
|
|
|
381
552
|
export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
382
553
|
const root = resolve(cwd);
|
|
383
554
|
const items = [];
|
|
384
555
|
const skips = [];
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
556
|
+
// Skip reasons ride arbitrary Error.messages — normalized to ONE line and length-capped so a
|
|
557
|
+
// multiline or oversized message can never rebuild a prose wall (D2).
|
|
558
|
+
const skip = (key, err) => skips.push({ key, reason: truncatedTo(oneLineOf(err?.message ?? String(err)), SKIP_REASON_CAP) });
|
|
559
|
+
// The runtime shape backstop (D2): every COMPOSED item is validated at construction — a
|
|
560
|
+
// violation surfaces through the stated-skip lane, never a crash, never a rendered violation.
|
|
561
|
+
// severityKey defaults to the item key; a per-site arm passes its `<key>.<variant>` entry when
|
|
562
|
+
// its class differs from the base (the invalid-env attention arm).
|
|
563
|
+
const add = (key, what, apply, severityKey = key) => {
|
|
564
|
+
const problems = [];
|
|
565
|
+
if (!(key in BENEFITS)) problems.push(`unregistered item key ${JSON.stringify(key)}`);
|
|
566
|
+
if (!(severityKey in SEVERITIES)) problems.push(`unregistered severity key ${JSON.stringify(severityKey)}`);
|
|
567
|
+
if (/[\r\n]/.test(what)) problems.push('WHAT is not a single line');
|
|
568
|
+
else if (what.length > ITEM_LINE_CAP) problems.push(`WHAT exceeds the ${ITEM_LINE_CAP}-char cap (${what.length})`);
|
|
569
|
+
if (/[\r\n]/.test(apply)) problems.push('apply is not a single line');
|
|
570
|
+
if (problems.length > 0) {
|
|
571
|
+
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
items.push({ key, severity: SEVERITIES[severityKey], what, benefit: BENEFITS[key], apply });
|
|
575
|
+
};
|
|
576
|
+
for (const probe of deps.probes ?? PROBES) probe({ root, deps, add, skip });
|
|
388
577
|
return { root, items, skips };
|
|
389
578
|
};
|
|
390
579
|
|
|
391
|
-
// ── rendering (the agent
|
|
580
|
+
// ── rendering (English tool DATA — the agent presents it in the user's conversational language,
|
|
581
|
+
// facts/counts complete, commands byte-exact; the raw block on request) ─────────────────────────
|
|
392
582
|
export const formatRecommendations = ({ items, skips }) => {
|
|
393
583
|
const lines = [RECOMMENDATIONS_SECTION_HEADER, ''];
|
|
394
|
-
|
|
584
|
+
const attention = items.filter((i) => i.severity === SEVERITY_ATTENTION).length;
|
|
585
|
+
const verdict = composeVerdict({ attention, optional: items.length - attention, skipped: skips.length });
|
|
586
|
+
if (verdict == null) {
|
|
395
587
|
// The flow-optimal claim renders ONLY when every probe ran and none fired — an empty item
|
|
396
588
|
// list beside skipped checks would falsely attest optimality (codex R1, Segment B).
|
|
397
589
|
lines.push(RECOMMENDATIONS_EMPTY_LINE);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
590
|
+
return lines.join('\n');
|
|
591
|
+
}
|
|
592
|
+
lines.push(verdict);
|
|
593
|
+
if (items.length > 0) {
|
|
594
|
+
lines.push('');
|
|
595
|
+
// Attention items lead (stable within each class — the frozen probe order).
|
|
596
|
+
const ordered = [...items].sort(
|
|
597
|
+
(a, b) => (a.severity === SEVERITY_ATTENTION ? 0 : 1) - (b.severity === SEVERITY_ATTENTION ? 0 : 1),
|
|
598
|
+
);
|
|
599
|
+
ordered.forEach((item, i) => {
|
|
600
|
+
lines.push(`${i + 1}. ${SEVERITY_LABELS[item.severity] ?? SEVERITY_LABELS[SEVERITY_OPTIONAL]}: ${item.what}`);
|
|
403
601
|
lines.push(` benefit: ${item.benefit}`);
|
|
404
602
|
lines.push(` apply: ${item.apply}`);
|
|
405
603
|
});
|
|
@@ -415,15 +613,17 @@ const HELP = `recommendations — the read-only upgrade Recommendations advisor
|
|
|
415
613
|
Usage:
|
|
416
614
|
node recommendations.mjs --cwd <project-root> [--json]
|
|
417
615
|
|
|
418
|
-
Computes the deterministic Recommendations section every kit upgrade ends with
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
current directory). The
|
|
422
|
-
a probe failure is a
|
|
423
|
-
|
|
424
|
-
item
|
|
425
|
-
|
|
426
|
-
the
|
|
616
|
+
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
617
|
+
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
618
|
+
sub-optimal · the benefit in one plain line · the exact consent-gated apply one-liner}. --cwd is
|
|
619
|
+
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
620
|
+
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
621
|
+
stated skipped-item line. Apply lines are cwd-independent (absolute tool paths, a pinned --cwd;
|
|
622
|
+
the doctor item pins via a cd prefix; the ONE exception is the set-autonomy item — a
|
|
623
|
+
conversational skill invocation labeled "run IN the target project") and preserve each writer's
|
|
624
|
+
own consent semantics; the sandbox-lane item is HAND-APPLY by design — this tool and the kit
|
|
625
|
+
writers never seed sandbox network/filesystem allowances, and its convergence is a neutral
|
|
626
|
+
fingerprint acknowledgement, never a security key.
|
|
427
627
|
|
|
428
628
|
Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
|
|
429
629
|
rendered (items or empty); 1 error; 2 usage.`;
|