@sabaiway/agent-workflow-kit 5.9.0 → 5.11.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 +139 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +32 -11
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.mjs +160 -0
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.test.mjs +235 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +23 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +242 -38
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +482 -38
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +45 -12
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +6 -3
- package/bridges/antigravity-cli-bridge/setup/README.md +18 -5
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/state-block-guard.mjs +107 -45
- package/references/modes/commit-guard.md +11 -8
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +32 -10
- package/references/modes/set-recipe.md +8 -5
- package/references/modes/state-block-guard.md +39 -31
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/check-docs-size-cli.test.mjs +2 -2
- package/references/shared/report-footer.md +2 -2
- package/references/templates/agent_rules.md +1 -0
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/commands.mjs +2 -2
- package/tools/commit-guard.mjs +74 -17
- package/tools/core-evidence.mjs +10 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +68 -14
- package/tools/ensure-configs.mjs +4 -4
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +20 -5
- package/tools/lens-region.mjs +13 -1
- package/tools/observation-builder.mjs +123 -0
- package/tools/satellite-locator.mjs +179 -0
- package/tools/source-size-scope.mjs +3 -1
- package/tools/worktree-handoff-return.mjs +369 -0
- package/tools/worktree-prompt.mjs +190 -0
- package/tools/worktrees-record.mjs +171 -0
- package/tools/worktrees.mjs +308 -297
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,145 @@ 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
|
+
## 5.11.0 — the fan-out half: which vehicle carries a sub-task, what a satellite is told, and what its handoff brings back (AD-100)
|
|
8
|
+
|
|
9
|
+
**A delegated thread could be recorded end to end, and the two questions around it still had no
|
|
10
|
+
mechanism.** The one BEFORE it — which vehicle carries this step class, and is that vehicle even
|
|
11
|
+
present on this host — lived in canon prose and in a remembered rule. The one AFTER it, one level up:
|
|
12
|
+
`worktrees` provisions a satellite and lands its diff, but nothing composed the prompt that satellite
|
|
13
|
+
starts from, and the handoff coming back was a convention with no rung — nothing read it, nothing
|
|
14
|
+
delivered what it held, nothing counted what it bought. Three new verbs close both.
|
|
15
|
+
|
|
16
|
+
- **`dispatch advise --step-class <c>`, and the same block as a footer on a form-VALID `dispatch
|
|
17
|
+
check`.** It answers which vehicle carries the class on THIS host, what the ledger has recorded for
|
|
18
|
+
it, and what the fallback is — and it can never decide. `check`'s exit code and FIRST line are
|
|
19
|
+
byte-identical whether the advised vehicle is present or absent, and a form-INVALID contract still
|
|
20
|
+
prints only its first violated field, so the advice cannot mask a refusal. Where the advice differs
|
|
21
|
+
from `vehicle.selected`, it NOTEs the divergence — never a refusal — and names `vehicle.requested`
|
|
22
|
+
only where that in turn differs from what the contract selected.
|
|
23
|
+
- **Capability is read off the filesystem, and each kind of ignorance keeps its own name.** The
|
|
24
|
+
advisor module itself writes nothing and spawns nothing, and the verb's only subprocesses are
|
|
25
|
+
read-only git probes: the repository top-level the cheap vehicles are anchored at, and the
|
|
26
|
+
delegation store path unless `AW_DELEGATION_STORE` names it outright. It never runs a vehicle, a
|
|
27
|
+
subscription CLI, or anything that writes. The execute backend resolves through the existing
|
|
28
|
+
detector, the cheap vehicles through the
|
|
29
|
+
presence of `.claude/agents/<name>.md` anchored at the repository top-level. That lane is
|
|
30
|
+
FOUR-valued — present, missing, `unanchored` (no root resolved, so a file found here proves nothing
|
|
31
|
+
against a nested shadow copy) and `probe-error` — because folding either ignorance into "missing"
|
|
32
|
+
asserts something about a root the module does not know. The bundled vehicles are portable rows;
|
|
33
|
+
`doc-research` renders HOST-LOCAL and names the solo fallback when absent; the harness-subagent lane
|
|
34
|
+
renders ASSUMED/manual with no availability verdict at all.
|
|
35
|
+
- **Recorded history arrives through the ledger's existing single door**, over the closed state set
|
|
36
|
+
`folded · failure-terminal · degrade-closed · open` — `open` counted separately, never as a closed
|
|
37
|
+
thread. An absent store prints `history: no recorded history`; an unreadable one answers
|
|
38
|
+
`history: unavailable — ` and then the store's own reason, verbatim. The advice prints either way,
|
|
39
|
+
and the advisor never refuses.
|
|
40
|
+
- **`worktrees prompt <slug>` — the satellite's cold-start prompt, read-only — and `provision` now
|
|
41
|
+
ends its report with the same composed text.** It carries the worktree path and branch, the seeded
|
|
42
|
+
plan, where the shared series index lives, that landing runs from MAIN, and that the handoff is the
|
|
43
|
+
one channel back. Every value is derived LIVE: the record freezes MAIN's paths at provision time, so
|
|
44
|
+
a moved MAIN or a hand-edited field would otherwise put a stale RUNNABLE command in front of a
|
|
45
|
+
satellite — a recorded value that no longer matches is NAMED beside the live one, and the install
|
|
46
|
+
posture, which is probed on the satellite itself, carries its own cause rather than borrowing
|
|
47
|
+
"MAIN moved". Commands are attributed: `MAIN $ …` / `HERE $ …`, because the landing command mutates
|
|
48
|
+
MAIN — exactly what the satellite is forbidden to do — while a dependency-bearing checkout's install
|
|
49
|
+
command runs HERE, and an unattributed `$` line read as an instruction to whoever held the prompt.
|
|
50
|
+
- **`dispatch handoff-return` — deliver, prove, then count.** It prints every user-owned fragment of
|
|
51
|
+
the handoff — the content before and after `## Provision record` alike — BYTE VERBATIM, each
|
|
52
|
+
fragment's OPENING boundary carrying its byte length so a fragment that itself imitates a boundary
|
|
53
|
+
line cannot hide where it really ends, and names the MAIN-owned destinations that content folds into;
|
|
54
|
+
the fold stays yours and the rung claims nothing about it. Its window is bound by TWO facts, because
|
|
55
|
+
one cannot close it: a clean post-commit index reproduces the committed tree, so `prepared-tree`
|
|
56
|
+
equality survives the commit. `land --prepare` now also records `prepared-head`, and the rung
|
|
57
|
+
requires an UNCHANGED HEAD and a staged write-tree equal to `prepared-tree`, re-attesting both
|
|
58
|
+
immediately before either answer. A record written by an earlier kit, carrying no `prepared-head`,
|
|
59
|
+
refuses by name and names `land --prepare` as the fix. It is MAIN-side by construction and refuses
|
|
60
|
+
from inside a satellite, where the shared common dir would let it measure the wrong tree.
|
|
61
|
+
- **One observation, recorded WHOLLY or not at all.** The `worktree-stream` numerator is the ATTESTED
|
|
62
|
+
tree's blob bytes read through a fail-closed `cat-file` — never off disk, so an edit made after the
|
|
63
|
+
prepare cannot move it — with component identity the canonical path. Anything unrepresentable is a
|
|
64
|
+
NAMED `observation: NOT RECORDED` at exit 0: a deletion, a rename's absent old side, a symlink, a
|
|
65
|
+
submodule, a non-UTF-8 path name, and a mode-only change, which has no measurable byte change at all
|
|
66
|
+
and gets its own rule. A regular BINARY file is inside the domain. A number that silently omitted
|
|
67
|
+
the deletions half of a landing would be worse than a named non-record.
|
|
68
|
+
- **A fold that lands AFTER the gates leaves those gates stale, and the fix is a printed command.**
|
|
69
|
+
The rung and `references/modes/worktrees.md` carry one order: fold, re-stage, the configured review,
|
|
70
|
+
`run-gates --final` over the current staged tree, `commit-guard --check`, the commit ask. "One
|
|
71
|
+
writer per worktree" is stated at both points of use and in the composed prompt as the BAR it is —
|
|
72
|
+
nothing new refuses a second writer.
|
|
73
|
+
- **Content-free evidence stops deciding commits on the two rungs where it had failed closed.**
|
|
74
|
+
A degrade minted on a CLEAN tree binds the empty-payload fingerprint — the one value every clean
|
|
75
|
+
moment of every repository shares — and the `#65` correlation resolved it to 19 distinct bases and
|
|
76
|
+
failed closed forever, blocking every commit through `commit-guard`. The `#65` red rung and the `#64` ordering rung now
|
|
77
|
+
step over content-free records and RECORD the step in the advisory channel, and `commit-guard`
|
|
78
|
+
splits its content-free lanes by the INDEX rather than the payload: a dirty index means staged bytes
|
|
79
|
+
the fingerprint cannot see and REFUSES, naming the `submodule.<name>.ignore` /
|
|
80
|
+
`diff.ignoreSubmodules` configuration as the recovery; a clean one SKIPS the RECEIPT arms and PASSES
|
|
81
|
+
while stating it attests NOTHING. That pass is scoped, never blanket — store HEALTH is deliberately
|
|
82
|
+
not waived, and a flow refusal still refuses there, because neither is a correlation. The tree-bound
|
|
83
|
+
correlations drop out only where the CALLER declares `treeCarriesBytes: false` — a routine
|
|
84
|
+
clean-tree check still wants those rungs.
|
|
85
|
+
- **Seven new modules, and every moved name is re-exported.** `dispatch-advisor.mjs`,
|
|
86
|
+
`advisor-matrix.mjs`, `worktrees-record.mjs`, `satellite-locator.mjs`, `worktree-prompt.mjs`,
|
|
87
|
+
`worktree-handoff-return.mjs`, and `observation-builder.mjs` — the last extracted so `observe` and
|
|
88
|
+
the rung build the identical record by ONE path. `worktrees.mjs` re-exports everything the leaves
|
|
89
|
+
took, and the characterization claim held literally: no existing assertion or fixture in the
|
|
90
|
+
worktrees suites changed.
|
|
91
|
+
|
|
92
|
+
**Known limitation, accepted.** The `observation` record's key set is closed and carries no artifact
|
|
93
|
+
digest, so the handoff digest and the two attested OIDs are the rung's PRINTED proof rather than
|
|
94
|
+
ledger fields — a later reader of the ledger alone cannot re-derive which handoff bytes and which tree
|
|
95
|
+
an observation was taken over. Widening a closed key set is a schema change with its own migration; it
|
|
96
|
+
is tracked as a follow-up.
|
|
97
|
+
|
|
98
|
+
## 5.10.0 — the bundled agy review reads the CLI's own envelope, and refuses a host that cannot honour the dispatch (AD-098; antigravity-cli-bridge 5.3.0, memory 4.5.0)
|
|
99
|
+
|
|
100
|
+
**A review used to read whatever the CLI happened to print, and recover the conversation id by
|
|
101
|
+
scraping the CLI's run log with a pattern its own comment called "agy's own to change".** Antigravity
|
|
102
|
+
CLI 1.1.13 answers `--output-format json` with one object whose `response` carries the model's
|
|
103
|
+
Markdown verbatim and whose `conversation_id` is a first-class field. The bundled bridge now drives
|
|
104
|
+
every dispatch that way and reads named fields instead of guessing — and the guess that used to rot
|
|
105
|
+
silently is deleted rather than hardened.
|
|
106
|
+
|
|
107
|
+
- **The envelope is transport, not contract.** The model is asked for exactly what it was asked for
|
|
108
|
+
before, so the delivery-proof echo, the mandated section shape, the receipt and the "no recognized
|
|
109
|
+
verdict" arm keep their semantics. On a successful run the wrapper still PRINTS the review text,
|
|
110
|
+
never JSON. An unreadable envelope on a zero exit is its own loud failure (exit 5) with no receipt,
|
|
111
|
+
distinct from a verdict-less answer (exit 4); a non-zero CLI exit keeps the CLI's own code and
|
|
112
|
+
message, because the envelope is parsed only on a zero exit.
|
|
113
|
+
- **A pre-spend capability door, not a version floor.** Before anything is spent, the wrapper probes
|
|
114
|
+
the DECLARED option tokens of `agy --help` for `--output-format` and `--disable-slash-commands`,
|
|
115
|
+
requires `node >= 22` and the reader module, and refuses with the cost of the capability that is
|
|
116
|
+
actually missing, the installed `agy` version and the recovery command. A `--help` that itself
|
|
117
|
+
fails is never read as "capability present". A guessed version floor would have refused working
|
|
118
|
+
installs — the release that introduced the flag is not measurable from one build.
|
|
119
|
+
- **The conversation id comes from the envelope; the log scrape and its `--continue` fallback are
|
|
120
|
+
gone.** The UUID grammar the scrape validated is kept and applied to the field, so a missing,
|
|
121
|
+
wrong-typed or malformed id stops the run before the next turn is spent instead of routing it at
|
|
122
|
+
an arbitrary conversation.
|
|
123
|
+
- **`--disable-slash-commands` on every review dispatch.** A change-set line that begins with a slash
|
|
124
|
+
command stays BODY, so the model reviews the delivered bytes rather than an expansion of them.
|
|
125
|
+
- **`--json-schema` was measured and REFUSED, and the reference says why.** It is not a constrained
|
|
126
|
+
decode: the model answers in prose and the CLI spends a second turn asking it to restate that
|
|
127
|
+
answer in schema shape. Matched control, same prompt and model, schema off vs on: 16,585 vs 33,446
|
|
128
|
+
total tokens, with the structured reason coming back reworded rather than quoted. `stream-json`
|
|
129
|
+
stays unadopted, with its two real gains named in the reference.
|
|
130
|
+
- **Node is now required for the review role** (the JSON parse lives in `bin/agy-envelope.mjs`, not
|
|
131
|
+
in bash); `SKILL.md`, `setup/README.md` and the flags reference carry the requirement and the
|
|
132
|
+
probed capability list in place of the old "1.1.13 or newer" line.
|
|
133
|
+
- **The state-block guard ships ONE vocabulary — English — and enumerates no other language.** A
|
|
134
|
+
shipped phrase list is a guess about somebody else's dialogue and it never stops growing; a
|
|
135
|
+
deployment whose dialogue language differs extends its own placed copy, and the mode contract
|
|
136
|
+
states that silence instead of hiding it. The always-loaded rules template gains the closing
|
|
137
|
+
state-block rule in the same shape: the slot LABELS stay English, because they are what lets a
|
|
138
|
+
checker find the block at all, while everything written into a slot is in the project's dialogue
|
|
139
|
+
language.
|
|
140
|
+
- **Two smaller consequences of that sweep, both load-bearing.** `lens-region.mjs` now recognizes the
|
|
141
|
+
intermediate communications canon that shipped between the plain-language bullet and the
|
|
142
|
+
state-block rule, so a deployment sitting on it is still detected as canon rather than flagged as a
|
|
143
|
+
custom edit; and the index reader behind the size practice carries each entry's object id, so a
|
|
144
|
+
consumer can read what the index HOLDS instead of what the worktree happens to show.
|
|
145
|
+
|
|
7
146
|
## 5.9.0 — the always-loaded navigator becomes something every deploy path actually creates (AD-096; memory 4.4.0)
|
|
8
147
|
|
|
9
148
|
**A fresh deployment's entry point declared `docs/ai/index.md` always-loaded, and no step ever
|
package/README.md
CHANGED
|
@@ -243,8 +243,8 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
243
243
|
| `/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. |
|
|
244
244
|
| `/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, or the agents hidden-mode reconcile follow-up) · 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. |
|
|
245
245
|
| `/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. |
|
|
246
|
-
| `/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. `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. Writes only its own append-only ledger inside the git dir (never committable); never commits, never runs a subscription CLI. |
|
|
247
|
-
| `/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); 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); `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. |
|
|
246
|
+
| `/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. |
|
|
247
|
+
| `/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. |
|
|
248
248
|
| `/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. |
|
|
249
249
|
| `/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. |
|
|
250
250
|
| `/agent-workflow-kit agents` | Claude Code · opt-in | **read-only subagents** — places bundled subagent definitions into `.claude/agents/`. **No vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your review recipe, advisory like every review). Writing code and running gates stay on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized file is **preserved, never overwritten**; never touches `settings*.json`, never commits. |
|
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: '5.
|
|
6
|
+
version: '5.11.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.3.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# antigravity-cli-bridge
|
|
@@ -95,11 +95,11 @@ AGY_HARD_TIMEOUT=8m agy-run "..." # hard wall-clock cap via timeout(
|
|
|
95
95
|
agy-run "..." -- --add-dir . # passthrough agy flags (never a permission widener)
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
`agy` is **headless-only** here (`-p`/`--print`)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
`agy` is **headless-only** here (`-p`/`--print`). `agy-run` stays **text**: `--output-format json`
|
|
99
|
+
can ride passthrough (`-- --output-format json`) but this wrapper adds no parsing/validation, so if
|
|
100
|
+
you need structure, ask for Markdown with explicit headings and validate it yourself. (The `review`
|
|
101
|
+
role is the one that adopted the JSON envelope — see below.) Wrapper inputs: first argument is the
|
|
102
|
+
prompt (`text`, `-` for stdin, or `@file`);
|
|
103
103
|
`AGY_MODEL` (default `Gemini 3.7 Flash (High)`); `AGY_TIMEOUT` → `--print-timeout` (default `5m`);
|
|
104
104
|
`AGY_HARD_TIMEOUT` → hard `timeout(1)` wall-clock cap (default = `AGY_TIMEOUT`); extra `agy` flags
|
|
105
105
|
after `--`. Full detail: [`references/models-and-flags.md`](references/models-and-flags.md).
|
|
@@ -142,6 +142,25 @@ agy-review diff <diff-file> [--facts @f] … # review a supplied diff
|
|
|
142
142
|
agy-review --continue --decided @round1.md --focus "still-open items" # round-2 delta, no re-assembly
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
+
**Transport + host requirements (probed, never a version floor).** Every review dispatch runs
|
|
146
|
+
`--output-format json` and `--disable-slash-commands`, and the wrapper reads the returned envelope
|
|
147
|
+
in node ([`bin/agy-envelope.mjs`](bin/agy-envelope.mjs)) instead of guessing at raw stdout — a
|
|
148
|
+
change-set line beginning with a slash command therefore stays BODY, and the answer TEXT plus the
|
|
149
|
+
conversation id arrive as **named fields**. The **review contract stays prose-shaped**: the verdict
|
|
150
|
+
is still parsed from that text's `### Verdict` section, unchanged. **On a zero exit** what
|
|
151
|
+
`agy-review` prints is the review text, never JSON; when the **single dispatch or the FINAL fed
|
|
152
|
+
turn** exits non-zero the captured stdout is published as-is (the CLI's own failure wins and the
|
|
153
|
+
envelope is parsed only on a zero exit), so a failing run may print a JSON or partial payload — an
|
|
154
|
+
**intermediate** feed turn is the exception, its output stays private (Invariant E) and its failure
|
|
155
|
+
prints only a named error. So it **probes before
|
|
156
|
+
spending anything**: `agy --help` must advertise both flags, **Node ≥ 22** must be on `PATH`, and
|
|
157
|
+
`bin/agy-envelope.mjs` must be present — each refusal names the missing capability and its recovery.
|
|
158
|
+
A version floor is deliberately not used (the release that introduced a flag is not measurable from
|
|
159
|
+
one installed build, so a guessed floor would refuse working installs), and a failed probe is never
|
|
160
|
+
read as "capability present". `--json-schema` is **rejected on measurement** — it costs a second
|
|
161
|
+
billed turn restating the model's own prose (16,585 → 33,446 tokens on a matched control); see
|
|
162
|
+
[`references/models-and-flags.md`](references/models-and-flags.md).
|
|
163
|
+
|
|
145
164
|
**Honesty + posture (D4/D5):** a run with no recognized `### Verdict` section — empty output
|
|
146
165
|
included — **exits 4 with NO receipt**: treat it as a *failed review to re-run*, never a fatal
|
|
147
166
|
session error. One stderr banner states the actual posture (`review posture: model=… timeout=…`)
|
|
@@ -249,9 +268,10 @@ checklist, prompt templates, output handling). Essentials:
|
|
|
249
268
|
- Subdirectory `CLAUDE.md` files are **not** auto-loaded by `agy` (only the cwd context file +
|
|
250
269
|
`.agents/skills/`). Put cross-cutting rules in the root context file, or include local rules in the
|
|
251
270
|
prompt when they matter.
|
|
252
|
-
-
|
|
253
|
-
|
|
254
|
-
**no `agy inspect`**:
|
|
271
|
+
- **`agy-run` leaves JSON unparsed** — `--output-format json|stream-json` can ride its passthrough
|
|
272
|
+
with no first-class parsing/validation. (`agy-review` is the exception: it **adopted `json`** and
|
|
273
|
+
parses the envelope; only `stream-json` stays deferred there.) And there is **no `agy inspect`**:
|
|
274
|
+
no machine-readable introspection.
|
|
255
275
|
- Model names must match the `agy models` display strings **exactly**.
|
|
256
276
|
- **Quota is finite.** Heavy use of Pro/Claude models can exhaust the subscription; prefer Flash for
|
|
257
277
|
cheap work.
|
|
@@ -260,5 +280,6 @@ checklist, prompt templates, output handling). Essentials:
|
|
|
260
280
|
kill (a run was seen surviving 32 min past a 10m `--print-timeout`). A heavy `--add-dir` agentic
|
|
261
281
|
prompt on a slow model (e.g. `Gemini 3.1 Pro (High)`) can run unbounded — prefer a faster model or
|
|
262
282
|
a **self-contained prompt** (no `--add-dir`); an "exceeded the hard cap" error is the guard firing.
|
|
263
|
-
- `agy` output
|
|
264
|
-
|
|
283
|
+
- `agy` output may be incomplete or out of date — treat it as advisory until the main agent verifies
|
|
284
|
+
it. (Its FORMAT is not one thing: see the transport section for which wrapper prints text, when,
|
|
285
|
+
and what a failing run prints instead.)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agy-envelope.mjs — read ONE captured `agy --output-format json` payload and hand the review
|
|
3
|
+
// wrapper the fields it needs, or fail with a DISTINGUISHABLE cause.
|
|
4
|
+
//
|
|
5
|
+
// Why a module and not bash: the CLI answers with ONE JSON object whose `response` carries the
|
|
6
|
+
// model's Markdown VERBATIM, and JSON parsing in bash is a defect farm. The trade — node becomes a
|
|
7
|
+
// hard runtime requirement for every review — is paid deliberately and bought back by the wrapper's
|
|
8
|
+
// pre-spend capability door, which refuses BEFORE a subscription turn is spent rather than after
|
|
9
|
+
// one is wasted.
|
|
10
|
+
//
|
|
11
|
+
// Every failure is LOUD: a named cause, a non-zero exit. There is no degraded verdict and no
|
|
12
|
+
// fallback to parsing raw stdout — a review whose answer cannot be read has not happened, and
|
|
13
|
+
// saying otherwise would attest a tree nobody reviewed.
|
|
14
|
+
//
|
|
15
|
+
// The parse is a PURE function so it can be tested without a subprocess; the CLI below is a thin
|
|
16
|
+
// file-I/O shell around it, exercised end-to-end by the wrapper's own acceptance tests.
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
const EXIT_NAMED_CAUSE = 1;
|
|
23
|
+
const EXIT_USAGE = 2;
|
|
24
|
+
const SUCCESS_STATUS = 'SUCCESS';
|
|
25
|
+
// The SAME grammar the retired `capture_conversation_id` log scrape validated. A named envelope
|
|
26
|
+
// field can still carry a malformed or wrong-typed value, and an unvalidated one would route every
|
|
27
|
+
// later feed turn at an arbitrary conversation.
|
|
28
|
+
const CONVERSATION_ID_GRAMMAR = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
29
|
+
|
|
30
|
+
const failed = (cause, sentence) => ({ ok: false, cause, sentence });
|
|
31
|
+
|
|
32
|
+
// Read one captured payload. `requireConversationId` is the caller's declaration that it will ROUTE
|
|
33
|
+
// a later turn at this conversation: only then is the field required and validated, so a lane that
|
|
34
|
+
// never routes one cannot fail over a value it does not use.
|
|
35
|
+
export const parseAgyEnvelope = (text, { requireConversationId = false } = {}) => {
|
|
36
|
+
if (text.length === 0) {
|
|
37
|
+
return failed('empty-payload', 'the captured payload is empty — the CLI printed nothing at all.');
|
|
38
|
+
}
|
|
39
|
+
// Parsed-ness is carried SEPARATELY from the parsed value: `null` is both a legal JSON document
|
|
40
|
+
// and the obvious failure sentinel, and conflating them reported valid JSON `null` as "not JSON".
|
|
41
|
+
const parsed = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return { parsed: true, value: JSON.parse(text) };
|
|
44
|
+
} catch {
|
|
45
|
+
return { parsed: false, value: null };
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
if (!parsed.parsed) {
|
|
49
|
+
return failed('not-json', 'the captured payload is not JSON — the dispatch did not return an envelope.');
|
|
50
|
+
}
|
|
51
|
+
const envelope = parsed.value;
|
|
52
|
+
if (envelope === null || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
53
|
+
return failed('not-an-envelope', 'the captured payload is JSON but not a single object.');
|
|
54
|
+
}
|
|
55
|
+
if (envelope.status !== SUCCESS_STATUS) {
|
|
56
|
+
return failed('status', `the envelope reports status ${JSON.stringify(envelope.status ?? null)}, not "${SUCCESS_STATUS}".`);
|
|
57
|
+
}
|
|
58
|
+
// An EMPTY response is a real (if useless) answer and rides through to the wrapper's verdict-less
|
|
59
|
+
// arm; only an ABSENT or wrong-typed field means the answer is not in the envelope at all.
|
|
60
|
+
if (typeof envelope.response !== 'string') {
|
|
61
|
+
return failed('response', 'the envelope carries no string `response` field — the model\'s answer is not in it.');
|
|
62
|
+
}
|
|
63
|
+
if (!requireConversationId) {
|
|
64
|
+
return { ok: true, response: envelope.response, conversationId: '' };
|
|
65
|
+
}
|
|
66
|
+
if (typeof envelope.conversation_id !== 'string') {
|
|
67
|
+
return failed('conversation-id', 'the envelope carries no string `conversation_id` field, so a later turn could not be routed at this conversation.');
|
|
68
|
+
}
|
|
69
|
+
if (!CONVERSATION_ID_GRAMMAR.test(envelope.conversation_id)) {
|
|
70
|
+
return failed('conversation-id', 'the envelope\'s `conversation_id` does not match the UUID grammar — routing a later turn at it would target an arbitrary conversation.');
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, response: envelope.response, conversationId: envelope.conversation_id };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const FLAG_KEYS = {
|
|
76
|
+
'--envelope': 'envelope',
|
|
77
|
+
'--response-out': 'responseOut',
|
|
78
|
+
'--conversation-id-out': 'conversationIdOut',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const USAGE = [
|
|
82
|
+
'agy-envelope — read ONE captured `agy --output-format json` payload, or fail with a named cause.',
|
|
83
|
+
'',
|
|
84
|
+
'Usage:',
|
|
85
|
+
' node agy-envelope.mjs --envelope <path> --response-out <path> [--conversation-id-out <path>]',
|
|
86
|
+
'',
|
|
87
|
+
' --envelope <path> the captured stdout of ONE `agy --output-format json` dispatch',
|
|
88
|
+
' --response-out <path> write the envelope\'s `response` VERBATIM here',
|
|
89
|
+
' --conversation-id-out <path> ALSO require `conversation_id`, validate it against the UUID',
|
|
90
|
+
' grammar, and write it here',
|
|
91
|
+
'',
|
|
92
|
+
'Exit: 0 parsed; 1 a named failure cause on stderr; 2 usage.',
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
const usageOutcome = (message) => ({ code: EXIT_USAGE, message: `agy-envelope: ${message}\n\n${USAGE}\n` });
|
|
96
|
+
const causeOutcome = (cause, sentence) => ({ code: EXIT_NAMED_CAUSE, message: `agy-envelope: ${cause} — ${sentence}\n` });
|
|
97
|
+
const isOutcome = (value) => value !== null && typeof value === 'object' && typeof value.code === 'number';
|
|
98
|
+
|
|
99
|
+
const parseArguments = (argv) =>
|
|
100
|
+
argv.reduce((options, token, index) => {
|
|
101
|
+
if (isOutcome(options)) return options;
|
|
102
|
+
if (!Object.hasOwn(FLAG_KEYS, token)) {
|
|
103
|
+
if (index > 0 && Object.hasOwn(FLAG_KEYS, argv[index - 1])) return options;
|
|
104
|
+
return usageOutcome(`unknown argument '${token}'`);
|
|
105
|
+
}
|
|
106
|
+
const value = argv[index + 1];
|
|
107
|
+
if (value === undefined || value.startsWith('--')) return usageOutcome(`${token} needs a value`);
|
|
108
|
+
return { ...options, [FLAG_KEYS[token]]: value };
|
|
109
|
+
}, { envelope: '', responseOut: '', conversationIdOut: '' });
|
|
110
|
+
|
|
111
|
+
const readPayloadText = (path) => {
|
|
112
|
+
const bytes = (() => {
|
|
113
|
+
try {
|
|
114
|
+
return readFileSync(path);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
119
|
+
if (bytes === null) return causeOutcome('unreadable-payload', `the captured payload '${path}' could not be read.`);
|
|
120
|
+
try {
|
|
121
|
+
// A BOM is stripped rather than fed to JSON.parse; any other invalid byte refuses loudly.
|
|
122
|
+
return { text: new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes) };
|
|
123
|
+
} catch {
|
|
124
|
+
return causeOutcome('not-utf8', `the captured payload '${path}' is not valid UTF-8.`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const writeField = (path, value, field) => {
|
|
129
|
+
try {
|
|
130
|
+
writeFileSync(path, value, 'utf8');
|
|
131
|
+
return null;
|
|
132
|
+
} catch {
|
|
133
|
+
return causeOutcome('write-failed', `the envelope's ${field} could not be written to '${path}'.`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// The CLI as a PURE-ish function: every arm RETURNS its outcome instead of exiting, so each one can
|
|
138
|
+
// be driven in-process by a test. Writing and exiting happen once, at the entry point below — the
|
|
139
|
+
// single impure boundary. (Subprocess tests could not close this: coverage is collected in the test
|
|
140
|
+
// process, so an arm only reachable through a spawn is an arm nothing can prove was exercised.)
|
|
141
|
+
export const runEnvelopeCli = (argv) => {
|
|
142
|
+
const options = parseArguments(argv);
|
|
143
|
+
if (isOutcome(options)) return options;
|
|
144
|
+
if (!options.envelope) return usageOutcome('--envelope is required.');
|
|
145
|
+
if (!options.responseOut) return usageOutcome('--response-out is required.');
|
|
146
|
+
const payload = readPayloadText(options.envelope);
|
|
147
|
+
if (isOutcome(payload)) return payload;
|
|
148
|
+
const result = parseAgyEnvelope(payload.text, { requireConversationId: Boolean(options.conversationIdOut) });
|
|
149
|
+
if (!result.ok) return causeOutcome(result.cause, result.sentence);
|
|
150
|
+
const wroteResponse = writeField(options.responseOut, result.response, 'response');
|
|
151
|
+
if (wroteResponse !== null) return wroteResponse;
|
|
152
|
+
if (!options.conversationIdOut) return { code: 0 };
|
|
153
|
+
const wroteId = writeField(options.conversationIdOut, result.conversationId, 'conversation_id');
|
|
154
|
+
return wroteId === null ? { code: 0 } : wroteId;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Entry-point guard (not import.meta.main — that landed after the family's Node >= 22 floor). ONE
|
|
158
|
+
// statement so the line is executed on import and carries no unreachable-by-test body.
|
|
159
|
+
const entryOutcome = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) ? runEnvelopeCli(process.argv.slice(2)) : null;
|
|
160
|
+
if (entryOutcome !== null) { if (entryOutcome.message) process.stderr.write(entryOutcome.message); process.exit(entryOutcome.code); }
|