@sabaiway/agent-workflow-kit 10.1.0 → 10.3.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 +58 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +4 -2
- package/bridges/antigravity-cli-bridge/bin/agy-review-harness.test.mjs +288 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-verdict.test.mjs +109 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +19 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +5 -336
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/status.md +1 -1
- package/references/modes/upgrade.md +6 -4
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/agent_rules.md +3 -2
- package/tools/ack-store.mjs +57 -0
- package/tools/ack-write.mjs +1 -1
- package/tools/doc-parity.mjs +8 -0
- package/tools/ensure-ops.mjs +18 -9
- package/tools/ensure-specs.mjs +3 -4
- package/tools/ensure-vocabulary.mjs +5 -2
- package/tools/family-registry.mjs +32 -3
- package/tools/lens-region.mjs +4 -1
- package/tools/node-evidence.mjs +77 -0
- package/tools/recommendations.mjs +68 -67
- package/tools/renderers.mjs +9 -0
- package/tools/spec-adoption.mjs +71 -0
- package/tools/spec-check.mjs +2 -2
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/view-model.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,64 @@ 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
|
+
## 10.3.0 — a deployed project is never in an unnamed spec-adoption state, and a skip names a fact the tree can check (AD-123)
|
|
8
|
+
|
|
9
|
+
A consumer's `upgrade` printed `specs: skipped-no-node — no package.json at the project root` beside
|
|
10
|
+
its own deployed `scripts/check-docs-size.mjs`; the same project's advisor said `no recommendations —
|
|
11
|
+
flow optimal.` over a tree with no `docs/ai/specs/` at all. Both lines were false about the tree they
|
|
12
|
+
described, and nothing in the kit could say so.
|
|
13
|
+
|
|
14
|
+
**Node is PROVEN, never proxied.** `tools/node-evidence.mjs` (contract `docs/ai/specs/kit/node-evidence.md`)
|
|
15
|
+
answers `package-json` · `deployed-node-scripts` (any kit-seeded `scripts/*.mjs`, the list pinned
|
|
16
|
+
against the bundle) · `none` · `unreadable`. The enforcement-script and spec-layer ensures seed on either
|
|
17
|
+
evidence, skip ONLY on `none` with the new token `skipped-no-node-evidence` — a line naming every probe
|
|
18
|
+
and every wrong-kind path — and fail closed on `unreadable` with the new cause
|
|
19
|
+
`node-evidence-unverifiable`. `skipped-no-node` is retired: a skip whose reason the tool could itself
|
|
20
|
+
disprove may not exist. `upgrade.md` and `deploy-tail.md` add the agent-facing half — a skip line that
|
|
21
|
+
contradicts the observed tree is raised as a finding in the report, never pasted as neutral.
|
|
22
|
+
|
|
23
|
+
**The adoption state is a surface.** `tools/spec-adoption.mjs` (contract `kit/spec-adoption`) reads the
|
|
24
|
+
store through `spec-check`'s now-exported census and read (`kit/spec-check` rev 2) and answers
|
|
25
|
+
`not-adopted` · `adopting` · `adopted` · `unreadable`. The advisor's 18th probe renders `spec-adoption`:
|
|
26
|
+
the seed on an absent store (apply: the spec-layer ensure one-liner; recipe line: the decline
|
|
27
|
+
preview as a named hand-apply alternative), the decline while a store holds no live contract — both
|
|
28
|
+
OPTIONAL, the frozen registry keeping attention for a broken configured declaration — and a stated
|
|
29
|
+
skip over a store it cannot read;
|
|
30
|
+
so `flow optimal` cannot render over a store nobody observed. `status` prints one `specs` line in every
|
|
31
|
+
state, with ` — declined` once the `spec-adoption` ack lane (`specAdoptionAck`) records the choice, and
|
|
32
|
+
its own `(decline ack unreadable: …)` when that read fails.
|
|
33
|
+
|
|
34
|
+
**The ack store has one reader.** `tools/ack-store.mjs` (contract `kit/ack-store`) now owns the store
|
|
35
|
+
path, the keys, the closed lane registry, `factFingerprint` and `readAckValue`; the advisor re-exports
|
|
36
|
+
them and `recipeFingerprint` derives through the same leaf. The leaf read moved from a path-based
|
|
37
|
+
`readFile` after `lstat` to the descriptor-bound no-follow door.
|
|
38
|
+
|
|
39
|
+
Docs: `status.md` documents the line and doc-parity binds the four state tokens into it; the
|
|
40
|
+
`recommendations.md` posture note names both arms and the lane; both `agent_rules.md` templates carry
|
|
41
|
+
the re-rendered Spec-first lens, the zero-state wording in §1.2 and the contradicted-skip bullet in
|
|
42
|
+
§2.5 (`COMMS_PRIORS` gains the outgoing state-block canon so a deployed §2.5 refreshes instead of
|
|
43
|
+
reading as a custom edit). Tarball pin 259 → 262.
|
|
44
|
+
|
|
45
|
+
Not shipped, with reason: the brief's mechanical refusal of a bare "zero governing specs" once the
|
|
46
|
+
store exists — no plan-shape checker exists and the citation has no literal grammar; queued with its
|
|
47
|
+
entry condition.
|
|
48
|
+
|
|
49
|
+
## 10.2.0 — a review can no longer pass a change it called broken (AD-122)
|
|
50
|
+
|
|
51
|
+
Ships antigravity-cli-bridge **5.4.0** in the bundled bridge tree. A consumer recorded a clean
|
|
52
|
+
`SHIP WITH NITS` receipt over a review body that named a correctness bug and a security violation
|
|
53
|
+
under `### Blocking` — the wrapper read the verdict line and nothing read the section. Now a
|
|
54
|
+
ship-class verdict beside a numbered Blocking item is a verdict-body contradiction and takes the
|
|
55
|
+
existing failed-review arm: exit 4, no receipt, both halves named, re-run the review. `REWORK` is
|
|
56
|
+
never refused — a veto needs no corroboration, only a pass does — an absent Blocking section is not
|
|
57
|
+
a contradiction, and a repeated heading terminates the scan, so exactly one section is ever judged.
|
|
58
|
+
|
|
59
|
+
The guard was written to a contract first (`docs/ai/specs/bridges/agy-review-verdict.md`, eight
|
|
60
|
+
scenarios), and the bridge's test suite was split in the same change: the shared fixture surface
|
|
61
|
+
moved to `bin/agy-review-harness.test.mjs`, the wrapper suite shrank to 2893 lines with all 213
|
|
62
|
+
existing arms green, and the new contract got its own topic file. Both new files ship with the
|
|
63
|
+
bridge tree, so the tarball count pin moves 257 → 259.
|
|
64
|
+
|
|
7
65
|
## 10.1.0 — no work without a specification, and a ratchet that does not trust its own file (AD-121)
|
|
8
66
|
|
|
9
67
|
A shipped tool that no LIVE contract claims is now a REFUSAL, not a preference. `tools/spec-coverage.mjs`
|
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
231
231
|
| `/agent-workflow-kit backends` | any time | **read-only** check of the optional execution-backends (the `codex` / `agy` bridges): what's set up vs missing and the next step. Never writes, never commits, never runs a subscription CLI (credentials = marker-file presence, not a live login). |
|
|
232
232
|
| `/agent-workflow-kit gates` | any time | **project gate runner** — runs the verification commands **your project itself declares** in `docs/ai/gates.json` (seeded at deploy; hand-editable `{ id, title, cmd }` entries plus the optional boolean `lcovProducer`, each `cmd` one bash line) as one batch: a per-gate **PASS/FAIL table** + one machine-readable summary line, exit 0 iff all green; a failing gate's own output is shown verbatim; `--only <id>` re-runs one. Honest distinct outcomes for a missing / empty / malformed declaration — never a silent green. The runner writes nothing **by default** and never commits — opt-in `--final` runs the FULL declared matrix and mints ONE final-run receipt into the git-dir core-evidence store (status green/red, pre/post tree fingerprints, the declaration, the consumed lcov's sha; the receipt the commit-guard binds); it executes only your own declared commands (a batching convenience, not a sandbox). The velocity tier auto-approves only the exact no-`--final` form — the recording run stays explicit. |
|
|
233
233
|
| `/agent-workflow-kit setup [backend]` | opt-in, any time | **link-only** auto-setup of a bridge: places the bundled bridge skill (only into an absent / empty / managed dir — never overwrites an unmanaged one) + links its wrappers onto `PATH` via managed symlinks (idempotent; refuses to clobber a non-symlink; try `--dry-run` to preview). The binary install + the one-time subscription login stay **manual**: it prints the exact **login** command and points the binary install at each bridge's `setup/README.md`. POSIX wrappers — on Windows use WSL. Never commits, never runs a subscription CLI. |
|
|
234
|
-
| `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
|
|
234
|
+
| `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
|
|
235
235
|
| `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents four named recipes for composing the bridges into plan → execute → review — **Solo / Reviewed / Council / Delegated** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready), and offers the choice. The orchestrator runs it via the bridge skills and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
|
|
236
236
|
| `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + backend readiness (default Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same). `--override <slot>=<recipe>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
|
|
237
237
|
| `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<recipe>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend, never commits**; hand-editing stays fully supported. |
|
|
@@ -242,7 +242,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
242
242
|
| `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
|
|
243
243
|
| `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
|
|
244
244
|
| `/agent-workflow-kit commit-guard` | any time | **the read-only pre-commit guard** (D10) — makes the commit capture the whole current working tree, so «verified» and «about to be committed» are the same bytes (the receipt itself has a stated residual — see the mode doc). FIRST it refuses an **INDEX that lags the verified working tree** (the gates and the fingerprint describe the WORKING tree while `git commit` takes the INDEX alone, and the fingerprint cannot tell them apart — so a lagging index used to ship a strict subset of what was verified): unstaged tracked paths or reviewable untracked-not-ignored paths, named up to a bounded cap with the remainder stated, a dirty tracked **submodule** named separately with its own recovery, and fail-closed on an undecidable probe. This deliberately blocks a partial commit. Then it binds the LATEST completed `run-gates --final` receipt to the EXACT current tree: refuses on a missing/red/stale receipt, fingerprint drift under the run, a dangling later attempt, declaration content drift, evidence-hash or lcov drift, or unsatisfied review obligations (the same review-state decision, recomputed over a sanitized env — forged out-of-repo stores never satisfy). Re-runs NO gate or test. Wire it into `.git/hooks/pre-commit` (the installer writes the RESOLVED invocation). `git commit --no-verify` stays the stated residual. |
|
|
245
|
-
| `/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 · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice,
|
|
245
|
+
| `/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, a feature-spec layer never adopted) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, or the spec-adoption decline preview) · 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 kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
|
|
246
246
|
| `/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. |
|
|
247
247
|
| `/agent-workflow-kit dispatch` | any time | **the delegation engine** — makes "how much does delegating a sub-task actually buy?" a measured number instead of a feeling. `check <file>` validates a sub-task brief's contract block and exits 0/1 naming the first violated field — **form only**: whether the task is genuinely bounded, its design decided and its acceptance adequate stays your judgment, and a well-formed absurdity passes here by construction. `advise --step-class <c>` answers the question that comes BEFORE that one — which vehicle should carry this kind of sub-task, whether it is even present on this machine, and what the ledger has already recorded for threads of that class (finished, failed, closed without a fold, and still open — counted separately) — and it decides nothing: it refuses no dispatch, gates no verb, and a choice that diverges from the advice is noted rather than blocked. It reads the filesystem only: nothing is launched to find out what is installed, an absent ledger prints "no recorded history", an unreadable one prints the store's own words, and either way the advice still prints. The same block appears as a footer under a form-valid `check` — never under a refusal, so it can never hide one. `register` pre-registers an acceptance wave (step classes, pairing key, minimum observations per class, the mean and first-pass thresholds) so the bar can never be chosen after the results it judges — immutable per wave. `observe` records ONE hand-written observation (`solo-construction`, the baseline whose ratio is 1 by construction — except over a scope measuring zero bytes, which has no ratio at all and is recorded ineligible by name — or `self-reported`) — delegated numbers are never hand-written, they are derived. The writer verbs are where that derivation happens: `open` puts a delegated thread on the record with every mint-time field copied from the brief's own header, refusing a deadline the wrapper's cap plus its kill grace would not fit inside, and recording whether the tree it started from was clean (a dirty one makes the result honestly unmeasurable rather than quietly counted); `await` waits for that one dispatch to answer and writes nothing — only the finished receipt satisfies it, a run still holding the nonce means keep waiting, and a wait that runs out says so with its own exit code, names whether the deadline or your own timeout ended it, and authorizes nothing: a wait that ended without an answer is a question for you, never permission to dispatch again; `return` absorbs the receipt the wrapper minted — only a finished one, checked against the contract it actually ran, its deadline and its own report, and refused outright when the tree hides changes from git or moves while the return is being computed — and derives the bytes from git rather than from anyone's claim; `fold` is the acceptance, and it refuses if the tree moved since the return — precisely, it re-confirms **equality of the visible canonical payload**, which is a change detector rather than a cryptographic identity of the tree: that payload is unframed and carries no file mode, so a content or symlink target that imitates the marker opening the next entry can alias two trees, and an executable-bit flip moves nothing (both named as residuals in the mode doc, with the fix queued). Where the payload cannot follow an object's bytes at all — a binary, a non-regular path, a submodule, a symlink whose target is not valid UTF-8 — `return` and `fold` **refuse** rather than promise what they cannot check. `degrade` closes a thread that never earned a fold, on the record and with its reason. `aggregate` prints one wave: the registered thresholds, every observation as context, and per step class the finished threads — a folded success with git-provable bytes contributes its ratio, one whose bytes are unprovable is excluded from the mean but still counts in the first-pass rate, and a failed thread, a degrade-closed one, or one folded after failing acceptance each count as a real zero. Below the registered minimum nothing is computed (insufficient); at or above it is computed and labeled PILOT evidence. It REFUSES rather than guessing: no pre-registration, an unfinished thread in scope, a recorded refusal-to-delegate that never opened a thread, or an ambiguous wave each stop the computation by name. `handoff-return --slug <s>` closes the worktree loop after `land --prepare`: it re-attests that main still holds exactly the prepared tree under the same HEAD (both recorded in the handoff at prepare time), prints the satellite handoff's user-owned content byte for byte with its boundaries, byte lengths and the main-owned destinations each part folds into, prints the handoff digest and both OIDs as its proof, states the after-the-fold order (a fold landed after the gates leaves them stale), and records ONE self-reported worktree-stream observation only when the whole prepared change set is measurable — a deletion, a rename, a symlink, a submodule or a mode-only change ends with a named NOT RECORDED instead, never a partial number. Writes only its own append-only ledger inside the git dir (never committable); never commits, never runs a subscription CLI. |
|
|
248
248
|
| `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
|
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: '10.
|
|
6
|
+
version: '10.3.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 headless behaviour.
|
|
4
4
|
metadata:
|
|
5
|
-
version: '5.
|
|
5
|
+
version: '5.4.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# antigravity-cli-bridge
|
|
@@ -163,7 +163,9 @@ billed turn restating the model's own prose (16,585 → 33,446 tokens on a match
|
|
|
163
163
|
|
|
164
164
|
**Honesty + posture (D4/D5):** a run with no recognized `### Verdict` section — empty output
|
|
165
165
|
included — **exits 4 with NO receipt**: treat it as a *failed review to re-run*, never a fatal
|
|
166
|
-
session error.
|
|
166
|
+
session error. The verdict vocabulary is closed (SHIP / SHIP WITH NITS / REWORK), and a
|
|
167
|
+
ship-class verdict arriving beside a numbered `### Blocking` finding is a verdict-body
|
|
168
|
+
contradiction: the same exit 4, NO receipt, both halves named. One stderr banner states the actual posture (`review posture: model=… timeout=…`)
|
|
167
169
|
and the receipt records the same `posture {model}`; an attesting review with `AGY_MODEL` explicitly
|
|
168
170
|
emptied refuses pre-spend (`AGY_PROBE=1` exempt), and control bytes in a model string refuse
|
|
169
171
|
pre-spend in every mode. The `timeout=` field is **banner-only** (exactly the duration `agy-run`
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Shared harness for the agy-review wrapper suites. PORTING TRAP: agy takes the prompt as the
|
|
2
|
+
// `-p` ARGV value, NOT stdin — the fake captures the -p value from argv.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { after } from 'node:test';
|
|
5
|
+
import {
|
|
6
|
+
mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, readFileSync,
|
|
7
|
+
existsSync, readdirSync, symlinkSync, cpSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join, dirname, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { spawnSync, execFile } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
export const WRAPPER = join(HERE, 'agy-review.sh');
|
|
16
|
+
|
|
17
|
+
const FAKE_ENVELOPE_ENCODER = [
|
|
18
|
+
'const text = require("node:fs").readFileSync(0, "utf8");',
|
|
19
|
+
'const [cid, status, shape] = process.argv.slice(1);',
|
|
20
|
+
'const envelope = { conversation_id: cid, status, response: text, duration_seconds: 1.5, num_turns: 1,',
|
|
21
|
+
' usage: { input_tokens: 10, output_tokens: 5, thinking_tokens: 0, cache_read_tokens: 0, total_tokens: 15 } };',
|
|
22
|
+
'if (shape === "missing") delete envelope.conversation_id;',
|
|
23
|
+
'if (shape === "number") envelope.conversation_id = 42;',
|
|
24
|
+
'process.stdout.write(`${JSON.stringify(envelope)}\\n`);',
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
export const FAKE_AGY = [
|
|
28
|
+
'#!/usr/bin/env bash',
|
|
29
|
+
'set -u',
|
|
30
|
+
// --help / --version answer BEFORE any capture file is touched — a probe is never a paid dispatch.
|
|
31
|
+
'case "${1:-}" in',
|
|
32
|
+
' --help|-h)',
|
|
33
|
+
' if [[ -n "${AGY_FAKE_HELP_EXIT:-}" ]]; then echo "fake agy: help unavailable" >&2; exit "$AGY_FAKE_HELP_EXIT"; fi',
|
|
34
|
+
' for f in --output-format --json-schema --disable-slash-commands --effort --mode; do',
|
|
35
|
+
' if [[ "$f" == "${AGY_FAKE_HELP_OMIT:-}" ]]; then continue; fi',
|
|
36
|
+
' printf " %s fake capability line\\n" "$f"',
|
|
37
|
+
' done',
|
|
38
|
+
' if [[ -n "${AGY_FAKE_HELP_EXTRA:-}" ]]; then printf "%s\\n" "$AGY_FAKE_HELP_EXTRA"; fi',
|
|
39
|
+
' exit 0 ;;',
|
|
40
|
+
' --version) printf "%s\\n" "${AGY_FAKE_VERSION:-1.1.13}"; exit 0 ;;',
|
|
41
|
+
'esac',
|
|
42
|
+
': "${AGY_FAKE_ARGV:=/dev/null}"',
|
|
43
|
+
': "${AGY_FAKE_ENV:=/dev/null}"',
|
|
44
|
+
': "${AGY_FAKE_PROMPT:=/dev/null}"',
|
|
45
|
+
': "${AGY_FAKE_SENTINEL:=/dev/null}"',
|
|
46
|
+
'printf invoked > "$AGY_FAKE_SENTINEL"',
|
|
47
|
+
'printf "%s" "$PWD" > "${AGY_FAKE_CWD:-/dev/null}"',
|
|
48
|
+
'{ for a in "$@"; do printf "%s\\n" "$a"; done; } > "$AGY_FAKE_ARGV"',
|
|
49
|
+
'{ echo "FOO_API_KEY=${FOO_API_KEY:-<unset>}"; echo "ANTIGRAVITY_API_KEY=${ANTIGRAVITY_API_KEY:-<unset>}"; } > "$AGY_FAKE_ENV"',
|
|
50
|
+
'prompt=""',
|
|
51
|
+
'prev=""; for a in "$@"; do if [[ "$prev" == "-p" ]]; then prompt="$a"; printf "%s" "$a" > "$AGY_FAKE_PROMPT"; fi; prev="$a"; done',
|
|
52
|
+
'aw_fmt=""',
|
|
53
|
+
'prev=""; for a in "$@"; do if [[ "$prev" == "--output-format" ]]; then aw_fmt="$a"; fi; prev="$a"; done',
|
|
54
|
+
// AGY_FAKE_BAD_TURN scopes the transport-breaking knobs to ONE turn; AGY_FAKE_RAW_STDOUT bypasses
|
|
55
|
+
// the envelope encoder entirely.
|
|
56
|
+
'aw_fake_emit() {',
|
|
57
|
+
' local body="$1" aw_status="${AGY_FAKE_STATUS:-SUCCESS}" aw_raw=""',
|
|
58
|
+
' if [[ -n "${AGY_FAKE_RAW_STDOUT+x}" ]]; then aw_raw=1; fi',
|
|
59
|
+
' if [[ -n "${AGY_FAKE_BAD_TURN:-}" && "$turn" != "${AGY_FAKE_BAD_TURN}" ]]; then aw_status="SUCCESS"; aw_raw=""; fi',
|
|
60
|
+
' if [[ -n "${AGY_FAKE_STDERR:-}" ]]; then printf "%s\\n" "$AGY_FAKE_STDERR" >&2; fi',
|
|
61
|
+
' if [[ -n "$aw_raw" ]]; then printf "%s" "$AGY_FAKE_RAW_STDOUT"; return 0; fi',
|
|
62
|
+
' if [[ "$aw_fmt" != "json" ]]; then printf "%s\\n" "$body"; return 0; fi',
|
|
63
|
+
` printf "%s\\n" "$body" | node -e '${FAKE_ENVELOPE_ENCODER}' \\`,
|
|
64
|
+
' "${AGY_FAKE_CONV_ID:-11111111-2222-3333-4444-555555555555}" "$aw_status" "${AGY_FAKE_CONV_SHAPE:-ok}"',
|
|
65
|
+
'}',
|
|
66
|
+
'prev=""; for a in "$@"; do',
|
|
67
|
+
' if [[ "$prev" == "--add-dir" ]]; then',
|
|
68
|
+
' printf "%s" "$a" > "${AGY_FAKE_ADDDIR:-/dev/null}"',
|
|
69
|
+
' stat -c "%a" "$a" > "${AGY_FAKE_ADDDIR_MODE:-/dev/null}" 2>/dev/null || true',
|
|
70
|
+
' art="$a/precomputed-change-set"',
|
|
71
|
+
' if [[ -f "$art" ]]; then stat -c "%a" "$art" > "${AGY_FAKE_ARTIFACT_MODE:-/dev/null}" 2>/dev/null || true; cp "$art" "${AGY_FAKE_ARTIFACT_COPY:-/dev/null}" 2>/dev/null || true; fi',
|
|
72
|
+
' fi; prev="$a"',
|
|
73
|
+
'done',
|
|
74
|
+
'if [[ -n "${AGY_FAKE_SLEEP:-}" ]]; then sleep "$AGY_FAKE_SLEEP"; fi',
|
|
75
|
+
// The fed lane needs a PER-TURN record: each invocation writes prompt/argv to "<file>.<turn>".
|
|
76
|
+
'turn=1',
|
|
77
|
+
'if [[ -n "${AGY_FAKE_TURNS:-}" ]]; then',
|
|
78
|
+
' if [[ -s "$AGY_FAKE_TURNS" ]]; then turn=$(( $(cat "$AGY_FAKE_TURNS") + 1 )); fi',
|
|
79
|
+
' printf "%s" "$turn" > "$AGY_FAKE_TURNS"',
|
|
80
|
+
' printf "%s" "$prompt" > "${AGY_FAKE_PROMPT}.$turn"',
|
|
81
|
+
' { for a in "$@"; do printf "%s\\n" "$a"; done; } > "${AGY_FAKE_ARGV}.$turn"',
|
|
82
|
+
'fi',
|
|
83
|
+
'if [[ -n "${AGY_FAKE_FAIL_TURN:-}" && "$turn" == "${AGY_FAKE_FAIL_TURN}" ]]; then',
|
|
84
|
+
' printf "FAKE_TURN_FAILURE\\n" >&2; exit 3',
|
|
85
|
+
'fi',
|
|
86
|
+
// A FEED turn deliberately misbehaves (a premature verdict) so the isolation invariant is proven
|
|
87
|
+
// against the worst case.
|
|
88
|
+
'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"--- BEGIN CHANGE-SET PART "* && "$prompt" != *"Requested addresses"* ]]; then',
|
|
89
|
+
' aw_fake_emit "$(printf "PREMATURE_FEED_CHATTER\\n### Verdict\\nREWORK")"; exit 0',
|
|
90
|
+
'fi',
|
|
91
|
+
// The FINAL turn answers the delivery-proof request honestly: by reading the bodies it was fed.
|
|
92
|
+
'if [[ -z "${AGY_FAKE_OUTPUT+x}" && "$prompt" == *"Requested addresses"* ]]; then',
|
|
93
|
+
' req="$(printf "%s" "$prompt" | awk "/^Requested addresses/{f=1; next} f && /^###/{exit} f{print}")"',
|
|
94
|
+
' entries=()',
|
|
95
|
+
' mapfile -t _items <<< "$req"',
|
|
96
|
+
' for _it in "${_items[@]}"; do',
|
|
97
|
+
' [[ -n "$_it" ]] || continue',
|
|
98
|
+
' k="$(printf "%s" "$_it" | awk "{print \\$2}")"; l="$(printf "%s" "$_it" | awk "{print \\$4}")"',
|
|
99
|
+
' src="$k"',
|
|
100
|
+
' if [[ "${AGY_FAKE_PROOF_DUP:-}" == "1" ]]; then src=1; fi',
|
|
101
|
+
' if [[ "${AGY_FAKE_PROOF_OMIT:-}" == "$k" ]]; then continue; fi',
|
|
102
|
+
' body="$(awk -v want="$l" "f && /^--- END CHANGE-SET PART /{exit} f{c++; if (c==want) {print; exit}} /^--- BEGIN CHANGE-SET PART /{f=1}" "${AGY_FAKE_PROMPT}.$src")"',
|
|
103
|
+
' if [[ "${AGY_FAKE_PROOF_CORRUPT:-}" == "$k" ]]; then body="${body}X"; fi',
|
|
104
|
+
' entry="$(printf "part %s line %s: %s" "$k" "$l" "$body")"',
|
|
105
|
+
' if [[ "${AGY_FAKE_PROOF_BULLET:-}" == "1" ]]; then entry="- $entry"; fi',
|
|
106
|
+
' if [[ "${AGY_FAKE_PROOF_NESTED:-}" == "$k" ]]; then entry="note: I believe $entry"; fi',
|
|
107
|
+
' if [[ "${AGY_FAKE_PROOF_PAD:-}" == "1" ]]; then entry="$(printf "part %02d line %04d: %s" "$k" "$l" "$body")"; fi',
|
|
108
|
+
' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then entry="$(printf "Part %s Line %s: %s" "$k" "$l" "$body")"; fi',
|
|
109
|
+
' if [[ "${AGY_FAKE_PROOF_HUGE:-}" == "$k" ]]; then entry="$(printf "part %s line 99999999999999999999: %s" "$k" "$body")"; fi',
|
|
110
|
+
' entries+=("$entry")',
|
|
111
|
+
' if [[ "${AGY_FAKE_PROOF_TWICE:-}" == "1" ]]; then entries+=("$entry"); fi',
|
|
112
|
+
' done',
|
|
113
|
+
' if [[ "${AGY_FAKE_PROOF_EXTRA:-}" == "1" ]]; then entries+=("part 99 line 1: an address nobody asked for"); fi',
|
|
114
|
+
' if [[ "${AGY_FAKE_PROOF_HUGE_EXTRA:-}" == "1" ]]; then entries+=("part 99999999999999999999 line 1: an invented giant address"); fi',
|
|
115
|
+
' out="$( {',
|
|
116
|
+
' if [[ "${AGY_FAKE_PROOF_LATE:-}" == "1" ]]; then printf "### Verdict\\nSHIP\\n"; fi',
|
|
117
|
+
' if [[ "${AGY_FAKE_PROOF_CASE:-}" == "1" ]]; then printf "### Delivery Proof\\n"; else printf "### Delivery proof\\n"; fi',
|
|
118
|
+
' if [[ "${AGY_FAKE_PROOF_OUTSIDE:-}" == "1" ]]; then',
|
|
119
|
+
' printf "(nothing here)\\n### Verdict\\nSHIP\\n"',
|
|
120
|
+
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
121
|
+
' else',
|
|
122
|
+
' if (( ${#entries[@]} > 0 )); then printf "%s\\n" "${entries[@]}"; fi',
|
|
123
|
+
' printf "### Verdict\\nSHIP\\n"',
|
|
124
|
+
' fi',
|
|
125
|
+
' if [[ -n "${AGY_FAKE_FINAL_APPEND:-}" ]]; then printf "%s\\n" "$AGY_FAKE_FINAL_APPEND"; fi',
|
|
126
|
+
' } )"',
|
|
127
|
+
' aw_fake_emit "$out"',
|
|
128
|
+
' exit 0',
|
|
129
|
+
'fi',
|
|
130
|
+
'if [[ -z "${AGY_FAKE_OUTPUT+x}" ]]; then aw_fake_emit "$(printf "FAKE_AGY_REVIEW_OUTPUT\\n### Verdict\\nSHIP")"; else aw_fake_emit "$AGY_FAKE_OUTPUT"; fi',
|
|
131
|
+
'exit "${AGY_FAKE_EXIT:-0}"',
|
|
132
|
+
'',
|
|
133
|
+
].join('\n');
|
|
134
|
+
|
|
135
|
+
// A PATH whose entries are symlinks to the real PATH binaries EXCEPT the excluded names.
|
|
136
|
+
const makePathWithout = (root, exclude = []) => {
|
|
137
|
+
const skip = new Set(exclude);
|
|
138
|
+
const dir = mkdtempSync(join(root, 'nobin-'));
|
|
139
|
+
for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
|
|
140
|
+
let names;
|
|
141
|
+
try { names = readdirSync(d); } catch { continue; }
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
if (skip.has(name)) continue;
|
|
144
|
+
const link = join(dir, name);
|
|
145
|
+
if (existsSync(link)) continue;
|
|
146
|
+
try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return dir;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Farms and the sandbox template are read-only per invocation, so both are built ONCE and shared.
|
|
153
|
+
const SHARED_ROOT = mkdtempSync(join(tmpdir(), 'agy-review-shared-'));
|
|
154
|
+
after(() => rmSync(SHARED_ROOT, { recursive: true, force: true }));
|
|
155
|
+
const farms = new Map();
|
|
156
|
+
export const farmFor = (exclude) => {
|
|
157
|
+
const key = exclude.join('|');
|
|
158
|
+
if (!farms.has(key)) farms.set(key, makePathWithout(SHARED_ROOT, exclude));
|
|
159
|
+
return farms.get(key);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const TEMPLATE_HOME = (() => {
|
|
163
|
+
const home = join(SHARED_ROOT, 'template-home');
|
|
164
|
+
const bin = join(home, '.local', 'bin');
|
|
165
|
+
mkdirSync(bin, { recursive: true });
|
|
166
|
+
writeFileSync(join(bin, 'agy'), FAKE_AGY, { mode: 0o755 });
|
|
167
|
+
const repo = join(home, 'repo');
|
|
168
|
+
mkdirSync(repo);
|
|
169
|
+
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
170
|
+
g('init', '-q');
|
|
171
|
+
g('config', 'user.email', 'probe@example.com');
|
|
172
|
+
g('config', 'user.name', 'probe');
|
|
173
|
+
writeFileSync(join(repo, 'base.txt'), 'committed base\n');
|
|
174
|
+
g('add', '-A');
|
|
175
|
+
g('commit', '-qm', 'base');
|
|
176
|
+
return home;
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
// `clean: true` leaves a pristine committed tree; the default leaves one untracked file so `code`
|
|
180
|
+
// mode has a diff to review.
|
|
181
|
+
export const makeSandbox = ({ clean = false } = {}) => {
|
|
182
|
+
const home = mkdtempSync(join(tmpdir(), 'agy-review-test-'));
|
|
183
|
+
cpSync(TEMPLATE_HOME, home, { recursive: true });
|
|
184
|
+
const bin = join(home, '.local', 'bin');
|
|
185
|
+
chmodSync(join(bin, 'agy'), 0o755);
|
|
186
|
+
const repo = join(home, 'repo');
|
|
187
|
+
const g = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
188
|
+
if (!clean) writeFileSync(join(repo, 'pending.txt'), 'PENDING_UNTRACKED_BODY\n');
|
|
189
|
+
return { home, bin, repo, g };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let runSeq = 0;
|
|
193
|
+
// Asynchronous on purpose: a blocking spawnSync pinned the whole suite to one core.
|
|
194
|
+
export const run = (sb, { args, env = {}, cwd, wrapper } = {}) => new Promise((settle) => {
|
|
195
|
+
const { home, bin, repo } = sb;
|
|
196
|
+
const farm = farmFor(['agy', 'agy-run']);
|
|
197
|
+
const tag = `cap-${++runSeq}`;
|
|
198
|
+
const cap = {
|
|
199
|
+
argv: join(home, `${tag}-argv`), env: join(home, `${tag}-env`), prompt: join(home, `${tag}-prompt`),
|
|
200
|
+
sentinel: join(home, `${tag}-sentinel`), adddir: join(home, `${tag}-adddir`),
|
|
201
|
+
adddirMode: join(home, `${tag}-adddir-mode`), artifactMode: join(home, `${tag}-artifact-mode`),
|
|
202
|
+
artifactCopy: join(home, `${tag}-artifact-copy`), turns: join(home, `${tag}-turns`),
|
|
203
|
+
dispatchCwd: join(home, `${tag}-dispatch-cwd`),
|
|
204
|
+
};
|
|
205
|
+
const child = execFile('bash', [wrapper || WRAPPER, ...args], {
|
|
206
|
+
cwd: cwd || repo,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
timeout: 30000,
|
|
209
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
210
|
+
env: {
|
|
211
|
+
HOME: home,
|
|
212
|
+
PATH: `${bin}:${farm}`,
|
|
213
|
+
TMPDIR: process.env.TMPDIR ?? '/tmp',
|
|
214
|
+
AGY_FAKE_ARGV: cap.argv, AGY_FAKE_ENV: cap.env, AGY_FAKE_PROMPT: cap.prompt,
|
|
215
|
+
AGY_FAKE_SENTINEL: cap.sentinel, AGY_FAKE_ADDDIR: cap.adddir, AGY_FAKE_ADDDIR_MODE: cap.adddirMode,
|
|
216
|
+
AGY_FAKE_ARTIFACT_MODE: cap.artifactMode, AGY_FAKE_ARTIFACT_COPY: cap.artifactCopy,
|
|
217
|
+
AGY_FAKE_TURNS: cap.turns, AGY_FAKE_CWD: cap.dispatchCwd,
|
|
218
|
+
...env,
|
|
219
|
+
},
|
|
220
|
+
}, (error, stdout, stderr) => {
|
|
221
|
+
const readIf = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : '');
|
|
222
|
+
const turns = existsSync(cap.turns) ? Number(readFileSync(cap.turns, 'utf8')) : 0;
|
|
223
|
+
const prompts = [];
|
|
224
|
+
const argvs = [];
|
|
225
|
+
for (let i = 1; i <= turns; i += 1) {
|
|
226
|
+
prompts.push(readIf(`${cap.prompt}.${i}`));
|
|
227
|
+
argvs.push(readIf(`${cap.argv}.${i}`));
|
|
228
|
+
}
|
|
229
|
+
settle({
|
|
230
|
+
status: error ? (error.code ?? 1) : 0, signal: error?.signal ?? null, stdout, stderr,
|
|
231
|
+
invoked: existsSync(cap.sentinel),
|
|
232
|
+
argv: readIf(cap.argv), capEnv: readIf(cap.env), prompt: readIf(cap.prompt),
|
|
233
|
+
adddir: readIf(cap.adddir).trim(), adddirMode: readIf(cap.adddirMode).trim(),
|
|
234
|
+
artifactMode: readIf(cap.artifactMode).trim(), artifactCopy: readIf(cap.artifactCopy),
|
|
235
|
+
dispatchCwd: readIf(cap.dispatchCwd).trim(), turns, prompts, argvs,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
// Only EPIPE is a refusal working — any other stdin write failure must reach the test.
|
|
239
|
+
child.stdin.on('error', (err) => { if (err.code !== 'EPIPE') throw err; });
|
|
240
|
+
child.stdin.end();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
export const runAsync = run;
|
|
244
|
+
|
|
245
|
+
export const RECEIPTS_REL = join('.git', 'agent-workflow-review-receipts.jsonl');
|
|
246
|
+
export const readReceipts = (repo) => {
|
|
247
|
+
const p = join(repo, RECEIPTS_REL);
|
|
248
|
+
if (!existsSync(p)) return [];
|
|
249
|
+
return readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// The chunked-feed lane: an over-cap change set is DELIVERED over turns and its delivery is PROVEN
|
|
253
|
+
// by verbatim line echoes the final answer must reproduce.
|
|
254
|
+
export const ARTIFACT_HEADER = '## The change set under review (assembled working-tree diff — repo-complete)';
|
|
255
|
+
export const SHAPE_HEADER = '\n## Output — Markdown, this exact shape, nothing else';
|
|
256
|
+
export const FED_CAP = 6000;
|
|
257
|
+
export const MAX_COUNTABLE_PROOF_ADDRESS = 40;
|
|
258
|
+
export const FED_WIDE_CAP = 24000;
|
|
259
|
+
export const isAssemblerBanner = (line) =>
|
|
260
|
+
/^=== (repo file map|git status|staged diff|unstaged diff|untracked)/.test(line) && line.endsWith(' ===');
|
|
261
|
+
|
|
262
|
+
export const seedFedChangeSet = (sb, { lines = 400, multibyte = false } = {}) => {
|
|
263
|
+
const body = Array.from({ length: lines }, (_, i) =>
|
|
264
|
+
multibyte
|
|
265
|
+
? `line ${String(i).padStart(4, '0')} — multibyte marker ${'\u044e'.repeat(20)}`
|
|
266
|
+
: `unique change-set line ${String(i).padStart(4, '0')} — a distinctive body marker ${'x'.repeat(20)}`).join('\n');
|
|
267
|
+
writeFileSync(join(sb.repo, 'oversized.txt'), `${body}\n`);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const inlineArtifactOf = (prompt) => prompt.slice(prompt.indexOf(ARTIFACT_HEADER), prompt.indexOf(SHAPE_HEADER));
|
|
271
|
+
export const bodyOf = (turnPrompt) => {
|
|
272
|
+
const begin = turnPrompt.match(/--- BEGIN CHANGE-SET PART \d+ OF \d+ ---\n/);
|
|
273
|
+
if (!begin) return null;
|
|
274
|
+
const start = begin.index + begin[0].length;
|
|
275
|
+
return turnPrompt.slice(start, turnPrompt.indexOf('\n--- END CHANGE-SET PART ', start));
|
|
276
|
+
};
|
|
277
|
+
export const requestedBlockOf = (finalPrompt) => {
|
|
278
|
+
const start = finalPrompt.indexOf('Requested addresses');
|
|
279
|
+
assert.notEqual(start, -1, 'the final turn states which lines it requires');
|
|
280
|
+
const after2 = finalPrompt.slice(finalPrompt.indexOf('\n', start) + 1);
|
|
281
|
+
return after2.slice(0, after2.indexOf('\n###')).split('\n').filter(Boolean);
|
|
282
|
+
};
|
|
283
|
+
export const requestedOf = (finalPrompt) => requestedBlockOf(finalPrompt).map((item) => {
|
|
284
|
+
const [, part, line] = item.match(/^part (\d+) line (\d+)$/);
|
|
285
|
+
return { part: Number(part), line: Number(line) };
|
|
286
|
+
});
|
|
287
|
+
export const fedRun = (sb, extraEnv = {}) =>
|
|
288
|
+
run(sb, { args: ['code', '--facts', 'grounded fact'], env: { AGY_MAX_PROMPT_BYTES: String(FED_CAP), ...extraEnv } });
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Contract under test: docs/ai/specs/bridges/agy-review-verdict.md (verdict-body honesty).
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { rmSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { makeSandbox, run, readReceipts, seedFedChangeSet, fedRun } from './agy-review-harness.test.mjs';
|
|
8
|
+
|
|
9
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
const CONTRADICTION_OUTPUT = '### Verdict\nSHIP WITH NITS — solid, two nits.\n### Blocking\n1. correctness: drops the last chunk\n2. security: path traversal in the staging dir\n### Non-blocking\nnone\n### Questions\nnone';
|
|
12
|
+
|
|
13
|
+
describe('agy-review.sh — a ship-class verdict cannot ride numbered Blocking findings', { concurrency: 2 }, () => {
|
|
14
|
+
// spec:agy-review-verdict/S1
|
|
15
|
+
it('a ship-class verdict beside a numbered Blocking item exits 4 with NO receipt — fresh and fed lanes alike', async () => {
|
|
16
|
+
const fresh = makeSandbox();
|
|
17
|
+
const r = await run(fresh, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: CONTRADICTION_OUTPUT } });
|
|
18
|
+
const freshReceipts = readReceipts(fresh.repo);
|
|
19
|
+
rmSync(fresh.home, { recursive: true, force: true });
|
|
20
|
+
assert.equal(r.status, 4, r.stderr);
|
|
21
|
+
assert.equal(r.invoked, true, 'the contradiction is judged AFTER the run — a failed review, not a pre-spend refusal');
|
|
22
|
+
assert.equal(freshReceipts.length, 0, 'a contradictory run mints nothing');
|
|
23
|
+
const sb = makeSandbox();
|
|
24
|
+
seedFedChangeSet(sb);
|
|
25
|
+
const fed = await fedRun(sb, { AGY_FAKE_FINAL_APPEND: '### Blocking\n1. a numbered blocker the verdict ignores' });
|
|
26
|
+
const fedReceipts = readReceipts(sb.repo);
|
|
27
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
28
|
+
assert.equal(fed.status, 4, fed.stderr);
|
|
29
|
+
assert.equal(fedReceipts.length, 0, 'the fed lane refuses the same shape after a PROVEN delivery');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// spec:agy-review-verdict/S2
|
|
33
|
+
it('a Blocking body of none — any case, trailing punctuation — mints exactly as today', async () => {
|
|
34
|
+
for (const noneForm of ['none', 'None.', 'NONE']) {
|
|
35
|
+
const sb = makeSandbox();
|
|
36
|
+
const output = `### Verdict\nSHIP WITH NITS — fine.\n### Blocking\n${noneForm}\n### Non-blocking\nnone\n### Questions\nnone`;
|
|
37
|
+
const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: output } });
|
|
38
|
+
const receipts = readReceipts(sb.repo);
|
|
39
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
40
|
+
assert.equal(r.status, 0, `"${noneForm}" must pass: ${r.stderr}`);
|
|
41
|
+
assert.equal(receipts.length, 1);
|
|
42
|
+
assert.equal(receipts[0].verdict, 'SHIP WITH NITS');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// spec:agy-review-verdict/S3
|
|
47
|
+
it('REWORK is never refused by the cross-check, numbered Blocking items or not', async () => {
|
|
48
|
+
const sb = makeSandbox();
|
|
49
|
+
const output = '### Verdict\nREWORK — two real blockers.\n### Blocking\n1. the guard fails open\n2. the receipt lies\n### Non-blocking\nnone\n### Questions\nnone';
|
|
50
|
+
const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: output } });
|
|
51
|
+
const receipts = readReceipts(sb.repo);
|
|
52
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
53
|
+
assert.equal(r.status, 0, r.stderr);
|
|
54
|
+
assert.equal(receipts.length, 1, 'a veto needs no corroboration');
|
|
55
|
+
assert.equal(receipts[0].verdict, 'REWORK');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// spec:agy-review-verdict/S4
|
|
59
|
+
it('an absent Blocking section is not a contradiction — the verdict-only read is unchanged', async () => {
|
|
60
|
+
const sb = makeSandbox();
|
|
61
|
+
const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: '### Verdict\nSHIP — clean, no sections beyond this one.' } });
|
|
62
|
+
const receipts = readReceipts(sb.repo);
|
|
63
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
64
|
+
assert.equal(r.status, 0, r.stderr);
|
|
65
|
+
assert.equal(receipts.length, 1, 'the shape mandate lives in the prompt; absence is not judged here');
|
|
66
|
+
assert.equal(receipts[0].verdict, 'SHIP');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// spec:agy-review-verdict/S5
|
|
70
|
+
it('the continuation lane refuses a contradictory verdict before its informational receipt', async () => {
|
|
71
|
+
const sb = makeSandbox();
|
|
72
|
+
const r = await run(sb, { args: ['--continue'], env: { AGY_FAKE_OUTPUT: CONTRADICTION_OUTPUT } });
|
|
73
|
+
const receipts = readReceipts(sb.repo);
|
|
74
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
75
|
+
assert.equal(r.status, 4, r.stderr);
|
|
76
|
+
assert.equal(receipts.length, 0, 'a fresh:false receipt must not encode a contradiction either');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// spec:agy-review-verdict/S6
|
|
80
|
+
it('the refusal names both halves and the verdict is never rewritten or downgraded', async () => {
|
|
81
|
+
const sb = makeSandbox();
|
|
82
|
+
const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: CONTRADICTION_OUTPUT } });
|
|
83
|
+
const receipts = readReceipts(sb.repo);
|
|
84
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
85
|
+
assert.equal(r.status, 4);
|
|
86
|
+
assert.match(r.stderr, /SHIP WITH NITS — solid, two nits\./, 'the RAW verdict line is named, reason text included');
|
|
87
|
+
assert.match(r.stderr, /drops the last chunk/, 'the first numbered Blocking item is named');
|
|
88
|
+
assert.match(r.stderr, /[Rr]e-run the review/, 'the recovery is the same as every failed review');
|
|
89
|
+
assert.equal(receipts.length, 0, 'no rewritten or downgraded verdict is ever minted');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// spec:agy-review-verdict/S8
|
|
93
|
+
it('a second Blocking heading terminates the scan — only the first section is judged', async () => {
|
|
94
|
+
const sb = makeSandbox();
|
|
95
|
+
const output = '### Verdict\nSHIP — clean.\n### Blocking\nnone\n### Blocking\n1. a numbered item under a duplicate heading\n### Questions\nnone';
|
|
96
|
+
const r = await run(sb, { args: ['code', '--facts', 'a tiny fact'], env: { AGY_FAKE_OUTPUT: output } });
|
|
97
|
+
const receipts = readReceipts(sb.repo);
|
|
98
|
+
rmSync(sb.home, { recursive: true, force: true });
|
|
99
|
+
assert.equal(r.status, 0, r.stderr);
|
|
100
|
+
assert.equal(receipts.length, 1, 'the first section said none — a later heading cannot suppress the receipt');
|
|
101
|
+
assert.equal(receipts[0].verdict, 'SHIP');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// spec:agy-review-verdict/S7
|
|
105
|
+
it('the loadable skill text names the closed verdict vocabulary', async () => {
|
|
106
|
+
const skill = readFileSync(resolve(HERE, '..', 'SKILL.md'), 'utf8');
|
|
107
|
+
assert.match(skill, /SHIP \/ SHIP WITH NITS \/ REWORK/, 'the closed vocabulary is spelled in SKILL.md, not only in --help and failure stderr');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -338,7 +338,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.7 Flash (High)"
|
|
|
338
338
|
# Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
|
|
339
339
|
# version (drift-guarded by agy-review.test.mjs against capability.json).
|
|
340
340
|
AW_RECEIPT_BACKEND="agy"
|
|
341
|
-
AW_BRIDGE_VERSION="5.
|
|
341
|
+
AW_BRIDGE_VERSION="5.4.0" # aw-version-anchor
|
|
342
342
|
# `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
|
|
343
343
|
AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
|
|
344
344
|
# D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
|
|
@@ -1070,6 +1070,14 @@ parse_agy_verdict() { # $1 = captured-output file
|
|
|
1070
1070
|
fi
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
|
+
parse_agy_verdict_line() { # $1 = captured-output file
|
|
1074
|
+
awk '/^### Verdict[[:space:]]*$/{flag=1; next} flag && NF {print; exit}' "$1" 2>/dev/null
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
parse_agy_blocking_first_numbered() { # $1 = captured-output file
|
|
1078
|
+
awk 'flag && /^### /{exit} /^### Blocking[[:space:]]*$/{flag=1; next} flag && /^[0-9]+[.)]/{print; exit}' "$1" 2>/dev/null
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1073
1081
|
# The repo file map is a FIXED cost that scales with REPO SIZE, not change size (measured 28,735
|
|
1074
1082
|
# bytes in the home repo — 24% of agy's 120000-byte single-argv ceiling), so an unbounded map taxes
|
|
1075
1083
|
# the change budget of every review. AW_REVIEW_MAP_BUDGET_BYTES bounds it; the value is set by each
|
|
@@ -2111,6 +2119,16 @@ if [[ $rc -eq 0 ]]; then
|
|
|
2111
2119
|
echo " the review; if it recurs, inspect the captured output for what the model produced." >&2
|
|
2112
2120
|
exit 4
|
|
2113
2121
|
fi
|
|
2122
|
+
if [[ "$verdict" == "SHIP" || "$verdict" == "SHIP WITH NITS" ]]; then
|
|
2123
|
+
first_blocking_item="$(parse_agy_blocking_first_numbered "$review_out_file")"
|
|
2124
|
+
if [[ -n "$first_blocking_item" ]]; then
|
|
2125
|
+
verdict_line="$(parse_agy_verdict_line "$review_out_file")"
|
|
2126
|
+
echo "error: verdict-body contradiction — '### Verdict' reads '$verdict_line' while '### Blocking'" >&2
|
|
2127
|
+
echo " carries a numbered finding ($first_blocking_item). A ship-class verdict cannot" >&2
|
|
2128
|
+
echo " ride blocking findings; NO receipt was written. Re-run the review." >&2
|
|
2129
|
+
exit 4
|
|
2130
|
+
fi
|
|
2131
|
+
fi
|
|
2114
2132
|
if [[ -n "$resume_mode" ]]; then
|
|
2115
2133
|
# A continuation never re-embeds the current artifact (agy holds the ORIGINAL round server-side;
|
|
2116
2134
|
# --facts is rejected above), so it cannot attest the folded tree: fresh:false, artifact /
|