@sabaiway/agent-workflow-kit 1.24.0 → 1.25.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 CHANGED
@@ -4,6 +4,40 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 1.25.0 — The bridge driving contract at the point of use (advisor render + wrapper `--help`)
8
+
9
+ A **feature** release (additive; ships the bundled bridges at **2.1.0**). An agent told to run a
10
+ bridge no longer re-derives the invocation from wrapper source — where it missed documented levers
11
+ (agy's `--facts`/`--decided` grounding, the `agy-review --continue` round-2 delta) and wasted
12
+ subscription runs on ungrounded reviews. The contract is now **delivered at the moment a recipe
13
+ dispatches a backend**, from ONE machine-readable source:
14
+
15
+ - **Manifest as source.** Each bridge `capability.json` dispatchable role (`review`, `execute`) now
16
+ carries a structured `contract`: exact copy-pasteable **invocation descriptors** (operands and
17
+ alternatives included), the **grounding** note, the closed **flag** set (agy-review), the
18
+ **round-2 / resume** descriptors, and codex-exec's **tiered guarded passthrough**
19
+ (always-blocked vs `CODEX_PROBE=1`-relaxable). Documented in `tools/manifest/schema.md`.
20
+ - **Advisor renders it at the point of use.** `/agent-workflow-kit procedures <activity>` prints,
21
+ under every dispatched backend of every slot (review recipes AND `execute=delegated`), the full
22
+ driving contract VERBATIM — e.g. council shows `agy-review code [--facts @f] [--decided @f] …`
23
+ plus the `agy-review --continue` delta beside `codex-review plan|code`. `--json` carries the same
24
+ in an **additive** `slots[*].contracts` field (`backends: string[]` unchanged).
25
+ - **Every wrapper answers `--help`/`-h`** — pre-preflight (no CLI, no login, no git tree, no
26
+ AGENTS.md needed); keyed on the FIRST argument only, so an open wrapper's passthrough payload
27
+ (`codex-exec - -- --help`) is never intercepted. The three **dispatchable** wrappers
28
+ (`codex-review`, `agy-review`, `codex-exec`) print the manifest contract; `agy-run` (probe role —
29
+ never dispatched by a recipe slot) ships a lightweight wrapper-authored help, pinned for
30
+ pre-preflight reachability only, with no manifest pivot by design.
31
+ - **Drift-guarded in both directions (test-as-spec), for the dispatchable wrappers.** The kit
32
+ registry mirror (`wrapperContractFor`) deep-equals each manifest; the advisor's rendered
33
+ descriptor set set-EQUALS the manifest (a missing AND a stale-extra descriptor both fail); each
34
+ dispatchable wrapper's `--help` set-EQUALS the manifest; and a **source-level reverse guard**
35
+ extracts each dispatchable wrapper's real parser arms (mode/flag/resume/passthrough-tier `case`
36
+ arms, heredocs excluded) and pins them to the manifest — adding a wrapper mode or flag without
37
+ updating the surfaced contract fails a test.
38
+ - Stale "unguarded codex flags" wording in the codex bridge docs corrected to the real **guarded**
39
+ passthrough contract.
40
+
7
41
  ## 1.24.0 — Humanize the deploy/version report: hide the internal structure number in the happy path
8
42
 
9
43
  A **feature** release (report-contract only — no logic, migration, or lineage change; the
package/README.md CHANGED
@@ -228,7 +228,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
228
228
  | `/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. |
229
229
  | `/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. |
230
230
  | `/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. |
231
- | `/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). `--override <slot>=<recipe>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
231
+ | `/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. |
232
232
  | `/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. |
233
233
  | `/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. |
234
234
  | `/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 redirection/command-substitution residual remains, closed by a deferred hook); a direct commit/push/publish still asks. `--dry-run` first. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.24.0'
6
+ version: '1.25.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -111,7 +111,7 @@ Pick the mode from the user's invocation. Auto-detect an existing `docs/ai/` to
111
111
  - **`/agent-workflow-kit setup [backend]`** — the **link-only**, opt-in companion to `backends`: place the bundled bridge skill + link its wrappers onto `PATH`. **In-agent only** — `init` (npx) never places bridges. The binary install + the interactive subscription login stay **manual** (it prints the exact commands); idempotent; refuses to clobber a non-symlink; never commits, never runs a subscription CLI.
112
112
  - **`/agent-workflow-kit status`** — read-only view of the **whole family**: which members (kit / memory / engine / the two bridges) are installed, at what version, and — in a project — what is deployed (`docs/ai`, the version stamps, the hidden-mode fence). Never writes, never commits, never runs a subscription CLI.
113
113
  - **`/agent-workflow-kit recipes`** — read-only **orchestration advisor**: present the four recipes (Solo / Reviewed / Council / Delegated) over the bridges' role vocabulary, plan + recommend one for the current environment, and offer the choice. **The orchestrator executes the chosen recipe via the bridge skills and always commits** — the kit only surfaces/selects/plans it; it never executes a recipe, never runs a subscription CLI, never commits.
114
- - **`/agent-workflow-kit procedures <activity>`** — read-only **activity-procedures advisor**: print the ordered steps of a named activity (`plan-authoring` / `plan-execution`) read **live** from the installed engine (`references/procedures.md`), and the **resolved effective recipe per slot** from the per-project `docs/ai/orchestration.json` (strict JSON; agent-writable via `set-recipe` or hand-edit) + backend readiness (default = Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated; graceful default vs loud override degradation). A per-run `--override <slot>=<recipe>` overrides one slot. Composes with `recipes` (which stays read-only); never writes, never commits, never runs a subscription CLI.
114
+ - **`/agent-workflow-kit procedures <activity>`** — read-only **activity-procedures advisor**: print the ordered steps of a named activity (`plan-authoring` / `plan-execution`) read **live** from the installed engine (`references/procedures.md`), and the **resolved effective recipe per slot** from the per-project `docs/ai/orchestration.json` (strict JSON; agent-writable via `set-recipe` or hand-edit) + backend readiness (default = Reviewed when a backend is ready, Council on request, slot-aware incl. Delegated; graceful default vs loud override degradation) — plus, for every dispatched backend, the **full driving contract at the point of use**: the exact copy-pasteable invocation descriptor(s), the grounding levers (agy `--facts`/`--decided`), the round-2 `--continue` delta, and the guarded passthrough tiers, rendered verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same contract). A per-run `--override <slot>=<recipe>` overrides one slot. Composes with `recipes` (which stays read-only); never writes, never commits, never runs a subscription CLI.
115
115
  - **`/agent-workflow-kit set-recipe`** — the **config writer** for `docs/ai/orchestration.json`: turn the user's plain language into explicit `--set <activity>.<slot>=<recipe>` / `--unset` ops; the kit validates, merges, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe). Resolves the effective recipe vs live readiness (degradation honesty on both paths). **Writes only `docs/ai/orchestration.json`; never runs a backend, never commits.** Hand-editing the file stays fully supported.
116
116
  - **`/agent-workflow-kit uninstall`** — the **guarded teardown** companion to `init`/`setup`. Removes what they placed — installed skill dirs + the bridge wrappers — and, in a project, reverses the hidden-mode fence + the marker pre-commit hook. **Never deletes user-authored content**: it prints the exact `rm` for `docs/ai` / `AGENTS.md` and an **edit** for `.claude/settings.json` (the `includeCoAuthoredBy` key + any velocity `permissions.*`), for you to run by hand. `--dry-run` first, always; preflight-then-mutate; never commits.
117
117
  - **`/agent-workflow-kit velocity`** — opt-in onboarding **velocity profile**: seed a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts; 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.** `--dry-run` first.
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
4
4
  metadata:
5
- version: '2.0.0'
5
+ version: '2.1.0'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -44,6 +44,41 @@
44
44
  # staging dir and pass it via --add-dir (re-enables Issue-001 stall risk)
45
45
  set -euo pipefail
46
46
 
47
+ # --- --help / -h (pre-preflight: no agy, no login, no git tree needed) ---------
48
+ # Keyed ONLY on the FIRST argument — never a scan of all args (uniform rule across
49
+ # the four wrappers). Drift-guarded against capability.json roles.review.contract.
50
+ case "${1:-}" in
51
+ --help|-h)
52
+ cat <<'HELP'
53
+ agy-review — grounded read-only ADVISORY review by Google's Antigravity CLI (agy; subscription-only).
54
+
55
+ Usage:
56
+ agy-review code [--facts @f] [--decided @f] [--focus "…"] [extra focus…]
57
+ agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]
58
+ agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]
59
+
60
+ Flags:
61
+ --facts @f — verified facts the review runs AGAINST (omit ⇒ loud ungrounded-review warning)
62
+ --decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)
63
+ --focus "…" — extra focus (repeatable; code mode also takes trailing focus words)
64
+
65
+ Grounding:
66
+ grounded review — agy reads NOTHING by default, an ungrounded review GUESSES:
67
+ --facts @f = the verified facts to review AGAINST; --decided @f = decisions
68
+ already made, do NOT re-raise (anti-circling)
69
+
70
+ Round-2 / resume:
71
+ agy-review --continue [--decided @f] [--focus "…"]
72
+ agy-review --conversation <id> [--decided @f] [--focus "…"]
73
+ (a continuation sends a small delta — agy holds the artifact server-side; --facts is invalid on a continuation)
74
+
75
+ Closed grammar: unknown flags are rejected; no '--' passthrough (the only escapes are AGY_PROBE=1 and AGY_REVIEW_ALLOW_ADDDIR=1).
76
+ Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
77
+ HELP
78
+ exit 0
79
+ ;;
80
+ esac
81
+
47
82
  DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
48
83
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
49
84
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
@@ -439,3 +439,189 @@ describe('agy-review.sh — subdir invocation is repo-complete (13)', () => {
439
439
  assert.match(r.prompt, /SUBDIR_RELATIVE_FACT/, 'a relative --facts path resolves against the invocation cwd, before the cd');
440
440
  });
441
441
  });
442
+
443
+ // ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
444
+ // The manifest roles.review.contract is the single machine-readable source of the
445
+ // driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
446
+ // both directions), (b) the wrapper's REAL parser arms equal the declared sets
447
+ // (source-level reverse guard), (c) each declared mode/flag is really accepted and
448
+ // the CLOSED grammar really rejects an invented flag. Helpers are inline — each
449
+ // bridge test file stays standalone (mirror byte-equality).
450
+
451
+ const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
452
+ const REVIEW_CONTRACT = MANIFEST.roles.review.contract;
453
+ const norm = (s) => s.replace(/\s+/g, ' ').trim();
454
+ const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
455
+ const leadingFlag = (descriptor) => {
456
+ const m = norm(descriptor).match(/(^|\s)(--[a-z-]+)/);
457
+ assert.ok(m, `descriptor "${descriptor}" carries no --flag token`);
458
+ return m[2];
459
+ };
460
+
461
+ // Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd —
462
+ // proving the short-circuit fires BEFORE every preflight.
463
+ const runHelp = (arg) => {
464
+ const root = mkdtempSync(join(tmpdir(), 'agy-review-help-'));
465
+ const nongit = join(root, 'nongit');
466
+ mkdirSync(nongit, { recursive: true });
467
+ const path = makePathWithout(root, ['codex', 'agy', 'git']);
468
+ const r = spawnSync('bash', [WRAPPER, arg], {
469
+ cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
470
+ });
471
+ rmSync(root, { recursive: true, force: true });
472
+ return r;
473
+ };
474
+
475
+ // The lines of a labelled --help section (header line → the next blank line).
476
+ const helpSection = (text, header) => {
477
+ const lines = text.split('\n');
478
+ const i = lines.findIndex((l) => l.trim() === header);
479
+ assert.notEqual(i, -1, `--help must carry a "${header}" section`);
480
+ const out = [];
481
+ for (let j = i + 1; j < lines.length; j += 1) {
482
+ if (lines[j].trim() === '') break;
483
+ out.push(lines[j].trim());
484
+ }
485
+ return out;
486
+ };
487
+
488
+ // Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
489
+ // statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
490
+ // heredoc bodies (a heredoc may carry non-CLI `case` arms — e.g. codex-exec's
491
+ // git-shim). Returns Map(subject → [raw arm label, …]) in source order.
492
+ const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
493
+ const extractArgCaseArms = (source) => {
494
+ const arms = new Map();
495
+ const stack = [];
496
+ let heredoc = null;
497
+ for (const raw of source.split('\n')) {
498
+ if (heredoc) {
499
+ if (raw.trim() === heredoc) heredoc = null;
500
+ continue;
501
+ }
502
+ if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
503
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
504
+ if (hd) { heredoc = hd[1]; continue; }
505
+ const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
506
+ if (cs) { stack.push(cs[1]); continue; }
507
+ if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
508
+ if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
509
+ const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
510
+ if (arm) {
511
+ const subject = stack[stack.length - 1];
512
+ if (!arms.has(subject)) arms.set(subject, []);
513
+ arms.get(subject).push(arm[1].trim());
514
+ }
515
+ }
516
+ }
517
+ return arms;
518
+ };
519
+ const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
520
+
521
+ describe('agy-review.sh — --help contract (manifest-pinned)', () => {
522
+ it('--help and -h exit 0 pre-preflight (no agy, no git)', () => {
523
+ for (const arg of ['--help', '-h']) {
524
+ const r = runHelp(arg);
525
+ assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
526
+ assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
527
+ assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
528
+ }
529
+ });
530
+
531
+ it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
532
+ const help = runHelp('--help').stdout;
533
+ const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('agy-review')).map(norm);
534
+ assert.ok(REVIEW_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
535
+ setEq(got, REVIEW_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
536
+ });
537
+
538
+ it('Flags set-EQUALS the manifest flag descriptors (both directions)', () => {
539
+ const help = runHelp('--help').stdout;
540
+ const got = helpSection(help, 'Flags:').filter((l) => l.startsWith('--')).map(norm);
541
+ assert.ok(REVIEW_CONTRACT.flags.length > 0, 'manifest flags must be non-empty');
542
+ setEq(got, REVIEW_CONTRACT.flags.map(norm), 'help Flags ⟷ manifest flags');
543
+ });
544
+
545
+ it('Grounding renders the manifest grounding note verbatim', () => {
546
+ const help = runHelp('--help').stdout;
547
+ assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(REVIEW_CONTRACT.grounding));
548
+ });
549
+
550
+ it('Round-2 / resume set-EQUALS the manifest continue descriptors', () => {
551
+ const help = runHelp('--help').stdout;
552
+ const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('agy-review')).map(norm);
553
+ assert.ok(REVIEW_CONTRACT.continue.length > 0, 'manifest continue must be non-empty');
554
+ setEq(got, REVIEW_CONTRACT.continue.map(norm), 'help continue ⟷ manifest continue');
555
+ });
556
+ });
557
+
558
+ describe('agy-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
559
+ const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
560
+
561
+ it('the real mode arms equal the manifest modes (adding a mode without the manifest fails here)', () => {
562
+ // Deliberately a UNION over every `case "$mode"` in the wrapper (the CLI dispatch AND the
563
+ // emit_artifact renderer): the union can only be conservative — a mode added to EITHER case
564
+ // without the manifest goes red; no renderer-only arm can make a missing manifest entry green.
565
+ const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
566
+ assert.ok(MANIFEST.roles.review.modes.length > 0, 'manifest modes must be non-empty');
567
+ setEq(new Set(modes), MANIFEST.roles.review.modes, 'parser mode arms ⟷ manifest modes');
568
+ });
569
+
570
+ it('the real flag arms equal the manifest flag set (closed grammar; catch-alls excluded)', () => {
571
+ const flagArms = splitArms(arms.get('"$1"')).filter((a) => !['--', '--*', '*'].includes(a));
572
+ const declared = REVIEW_CONTRACT.flags.map(leadingFlag);
573
+ assert.ok(declared.length > 0, 'manifest flag set must be non-empty');
574
+ setEq(new Set(flagArms), new Set(declared), 'parser flag arms ⟷ manifest flags');
575
+ });
576
+
577
+ it('the first-arg entrypoints are exactly --help/-h + the manifest continue flags', () => {
578
+ const declared = REVIEW_CONTRACT.continue.map(leadingFlag);
579
+ assert.ok(declared.length > 0, 'manifest continue set must be non-empty');
580
+ setEq(new Set(splitArms(arms.get('"${1:-}"'))), new Set(['--help', '-h', ...declared]));
581
+ });
582
+ });
583
+
584
+ describe('agy-review.sh — declared contract is really accepted (forward guard)', () => {
585
+ it('every manifest mode runs green', () => {
586
+ const drive = {
587
+ code: () => ['code', '--facts', 'f'],
588
+ plan: (sb) => { writeFileSync(join(sb.repo, 'p.md'), '# p\n'); return ['plan', 'p.md', '--facts', 'f']; },
589
+ diff: (sb) => { writeFileSync(join(sb.repo, 'c.diff'), 'diff body\n'); return ['diff', 'c.diff', '--facts', 'f']; },
590
+ };
591
+ for (const mode of MANIFEST.roles.review.modes) {
592
+ assert.ok(drive[mode], `no test drive for manifest mode "${mode}" — add one`);
593
+ const sb = makeSandbox();
594
+ const r = run(sb, { args: drive[mode](sb) });
595
+ rmSync(sb.home, { recursive: true, force: true });
596
+ assert.equal(r.status, 0, `mode ${mode}: ${r.stderr}`);
597
+ assert.equal(r.invoked, true, `mode ${mode} must reach agy`);
598
+ }
599
+ });
600
+
601
+ it('every manifest flag is accepted in code mode', () => {
602
+ for (const descriptor of REVIEW_CONTRACT.flags) {
603
+ const flag = leadingFlag(descriptor);
604
+ const sb = makeSandbox();
605
+ const r = run(sb, { args: ['code', flag, 'f'] });
606
+ rmSync(sb.home, { recursive: true, force: true });
607
+ assert.equal(r.status, 0, `${flag}: ${r.stderr}`);
608
+ }
609
+ });
610
+
611
+ it('an invented flag is rejected (closed grammar negative)', () => {
612
+ const sb = makeSandbox();
613
+ const r = run(sb, { args: ['code', '--facts', 'f', '--bogus-flag'] });
614
+ rmSync(sb.home, { recursive: true, force: true });
615
+ assert.equal(r.status, 2);
616
+ assert.match(r.stderr, /unknown flag '--bogus-flag'/);
617
+ assert.equal(r.invoked, false, 'an unknown flag must not spend a run');
618
+ });
619
+
620
+ it('--help NOT in first position is an unknown flag, never an intercepted help', () => {
621
+ const sb = makeSandbox();
622
+ const r = run(sb, { args: ['code', '--facts', 'f', '--help'] });
623
+ rmSync(sb.home, { recursive: true, force: true });
624
+ assert.equal(r.status, 2, 'help is keyed on the FIRST argument only');
625
+ assert.doesNotMatch(r.stdout, /Usage:/);
626
+ });
627
+ });
@@ -34,6 +34,29 @@
34
34
  # # passthrough agy flags (future flows)
35
35
  set -euo pipefail
36
36
 
37
+ # --- --help / -h (pre-preflight: no agy, no login needed) ----------------------
38
+ # Keyed ONLY on the FIRST argument — never a scan of all args, else a passthrough
39
+ # payload like `agy-run "prompt" -- --help` would be intercepted. agy-run is the
40
+ # probe role (not dispatched by any activity slot), so this help is authored here
41
+ # — not manifest-pinned (candidate C only).
42
+ case "${1:-}" in
43
+ --help|-h)
44
+ cat <<'HELP'
45
+ agy-run — thin, flow-agnostic wrapper around Google's Antigravity CLI (agy; subscription-only, hard wall-clock cap).
46
+
47
+ Usage:
48
+ agy-run "your prompt"
49
+ echo "your prompt" | agy-run -
50
+ agy-run @path/to/prompt.md
51
+ agy-run <prompt|-|@file> -- <extra agy flags...>
52
+
53
+ Environment: AGY_MODEL (exact display string from `agy models`; empty ⇒ agy's settings.json), AGY_TIMEOUT / AGY_HARD_TIMEOUT (duration strings), AGY_MAX_PROMPT_BYTES (single-argv byte ceiling; the override only lowers it).
54
+ Requires at run time: the agy CLI on PATH + a Google AI subscription login (--help needs neither).
55
+ HELP
56
+ exit 0
57
+ ;;
58
+ esac
59
+
37
60
  # 1. Make `agy` findable even when ~/.bashrc was not sourced.
38
61
  export PATH="$HOME/.local/bin:$PATH"
39
62
 
@@ -1,8 +1,8 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, existsSync } from 'node:fs';
3
+ import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync, existsSync, readdirSync, symlinkSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { join, dirname } from 'node:path';
5
+ import { join, dirname, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { spawnSync } from 'node:child_process';
8
8
 
@@ -152,3 +152,59 @@ describe('agy.sh — argv byte-ceiling guard (AGY_MAX_PROMPT_BYTES)', () => {
152
152
  assert.equal(invoked, false, 'raising the ceiling past the OS limit must fail loud, not pass through to E2BIG');
153
153
  });
154
154
  });
155
+
156
+ // ── --help (candidate C only — the probe role is not dispatched by any activity
157
+ // slot, so this help is authored in the wrapper, NOT manifest-pinned; the lighter
158
+ // guard pins pre-preflight reachability + the documented usage forms). ──────────
159
+
160
+ // A PATH farm mirroring the real one MINUS the named binaries — so the reachability claim
161
+ // ("--help needs no agy/git") holds even on a host that has the real CLIs installed.
162
+ // Ported from agy-review.test.mjs (inline: each bridge test file stays standalone).
163
+ const makePathWithout = (root, exclude = []) => {
164
+ const skip = new Set(exclude);
165
+ const dir = mkdtempSync(join(root, 'nobin-'));
166
+ for (const d of (process.env.PATH || '').split(':').filter(Boolean)) {
167
+ let names;
168
+ try { names = readdirSync(d); } catch { continue; }
169
+ for (const name of names) {
170
+ if (skip.has(name)) continue;
171
+ const link = join(dir, name);
172
+ if (existsSync(link)) continue;
173
+ try { symlinkSync(resolve(d, name), link); } catch { /* dup / race — ignore */ }
174
+ }
175
+ }
176
+ return dir;
177
+ };
178
+
179
+ describe('agy.sh — --help (pre-preflight, candidate C)', () => {
180
+ it('--help and -h exit 0 with NO agy on PATH and name the documented usage', () => {
181
+ for (const arg of ['--help', '-h']) {
182
+ // A bare HOME with no ~/.local/bin/agy stub AND a PATH farm stripped of agy/git —
183
+ // the help must not need the CLI even when the host has a real agy installed.
184
+ const home = mkdtempSync(join(tmpdir(), 'agy-help-'));
185
+ const r = spawnSync('bash', [WRAPPER, arg], {
186
+ env: { HOME: home, PATH: makePathWithout(home, ['agy', 'git']) },
187
+ encoding: 'utf8',
188
+ timeout: 15000,
189
+ });
190
+ rmSync(home, { recursive: true, force: true });
191
+ assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
192
+ assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
193
+ assert.match(r.stdout, /Usage:/);
194
+ assert.match(r.stdout, /agy-run "your prompt"/);
195
+ assert.match(r.stdout, /agy-run @path\/to\/prompt\.md/);
196
+ assert.match(r.stdout, /agy-run <prompt\|-\|@file> -- <extra agy flags\.\.\.>/);
197
+ }
198
+ });
199
+
200
+ it('--help after the -- separator is passthrough payload, never intercepted', () => {
201
+ const home = makeSandbox(RECORDING_STUB);
202
+ const sentinel = join(home, 'sentinel');
203
+ const r = runArgs(home, { args: ['prompt', '--', '--help'], env: { AGY_STUB_SENTINEL: sentinel } });
204
+ const invoked = existsSync(sentinel);
205
+ rmSync(home, { recursive: true, force: true });
206
+ assert.equal(r.status, 0, r.stderr);
207
+ assert.doesNotMatch(r.stdout, /Usage:/, 'help is keyed on the FIRST argument only');
208
+ assert.equal(invoked, true, 'the run must proceed to agy with the payload');
209
+ });
210
+ });
@@ -3,10 +3,33 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.0.0",
6
+ "version": "2.1.0",
7
7
  "provides": ["review", "probe"],
8
8
  "roles": {
9
- "review": { "cmd": "agy-review", "source": "bin/agy-review.sh", "template": "references/review-prompt.md", "modes": ["code", "plan", "diff"], "output": "advisory" },
9
+ "review": {
10
+ "cmd": "agy-review",
11
+ "source": "bin/agy-review.sh",
12
+ "template": "references/review-prompt.md",
13
+ "modes": ["code", "plan", "diff"],
14
+ "output": "advisory",
15
+ "contract": {
16
+ "invocations": [
17
+ "agy-review code [--facts @f] [--decided @f] [--focus \"…\"] [extra focus…]",
18
+ "agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus \"…\"]",
19
+ "agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus \"…\"]"
20
+ ],
21
+ "grounding": "grounded review — agy reads NOTHING by default, an ungrounded review GUESSES: --facts @f = the verified facts to review AGAINST; --decided @f = decisions already made, do NOT re-raise (anti-circling)",
22
+ "flags": [
23
+ "--facts @f — verified facts the review runs AGAINST (omit ⇒ loud ungrounded-review warning)",
24
+ "--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)",
25
+ "--focus \"…\" — extra focus (repeatable; code mode also takes trailing focus words)"
26
+ ],
27
+ "continue": [
28
+ "agy-review --continue [--decided @f] [--focus \"…\"]",
29
+ "agy-review --conversation <id> [--decided @f] [--focus \"…\"]"
30
+ ]
31
+ }
32
+ },
10
33
  "probe": { "cmd": "agy-run", "source": "bin/agy.sh", "output": "advisory" }
11
34
  },
12
35
  "detect": {
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '2.0.0'
5
+ version: '2.1.0'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -77,7 +77,7 @@ Drive codex only through the two wrappers (installed on `PATH`), run from the ta
77
77
  # EXECUTION (workspace-write sandbox, network OFF, never prompts):
78
78
  codex-exec docs/plans/<slug>.md # drive a plan file
79
79
  echo "apply review fix: ..." | codex-exec - # ad-hoc instruction from stdin
80
- codex-exec <file|-> -- <unguarded codex flags...> # passthrough codex flags after `--` (guarded ones rejected)
80
+ codex-exec <file|-> -- <extra codex flags...> # GUARDED passthrough after `--` (policy/model/capture flags rejected; some relaxed only under CODEX_PROBE=1)
81
81
 
82
82
  # RESUME (iterate on the SAME session without re-sending context):
83
83
  codex-exec --resume-last docs/plans/<slug>.md # continue the last session (id from the sidecar)
@@ -40,6 +40,40 @@
40
40
  # CODEX_PROBE=1 CODEX_MODEL=<slug> codex-exec <file> # throwaway probe (relaxes the guard)
41
41
  set -euo pipefail
42
42
 
43
+ # --- --help / -h (pre-preflight: no codex, no login, no git tree needed) -------
44
+ # Keyed ONLY on the FIRST argument — never a scan of all args, else a passthrough
45
+ # payload like `codex-exec f - -- --help` would be intercepted.
46
+ # The contract below is drift-guarded against capability.json roles.execute.contract.
47
+ case "${1:-}" in
48
+ --help|-h)
49
+ cat <<'HELP'
50
+ codex-exec — delegate plan/instruction EXECUTION to the OpenAI Codex CLI (subscription-only; workspace-write sandbox, network OFF, git writes blocked — the orchestrator commits).
51
+
52
+ Usage:
53
+ codex-exec <plan-file|->
54
+ codex-exec <plan-file|-> -- <extra codex flags...>
55
+
56
+ Grounding:
57
+ automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's
58
+ context and the wrapper prepends the orchestrator execution contract; no
59
+ grounding flags
60
+
61
+ Round-2 / resume:
62
+ codex-exec --resume-last <plan-file|->
63
+ codex-exec --resume <session-id> <plan-file|->
64
+ (resume continues the recorded session without re-sending context; takes no '--' passthrough)
65
+
66
+ Guarded passthrough after '--':
67
+ blocked always: -c* --config* -s* --sandbox* --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --full-auto --oss --local-provider* -p* --profile* -m* --model* -o* --output-last-message* --json* --color* --output-schema* --ephemeral*
68
+ relaxed only under CODEX_PROBE=1: --add-dir* -C* --cd* --skip-git-repo-check --ignore-rules --enable* --disable*
69
+
70
+ Environment: CODEX_HARD_TIMEOUT (seconds, default 3600), CODEX_PROBE=1 (throwaway probe only).
71
+ Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
72
+ HELP
73
+ exit 0
74
+ ;;
75
+ esac
76
+
43
77
  DEFAULT_CODEX_MODEL="gpt-5.5" # frontier coding model (verified locally) — pinned
44
78
  DEFAULT_CODEX_EFFORT="xhigh" # maximum reasoning effort — pinned
45
79
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
@@ -149,7 +149,8 @@ describe('codex-exec.sh — quality-first model/effort guard (1.1)', () => {
149
149
  // capture flags: rejected ALWAYS, even under CODEX_PROBE=1.
150
150
  const ALWAYS_BLOCKED = [
151
151
  ['-s', 'read-only'], ['--sandbox', 'danger-full-access'], ['-c', 'k=v'], ['--config', 'k=v'],
152
- ['--full-auto'], ['--dangerously-bypass-approvals-and-sandbox'], ['--oss'], ['--local-provider', 'x'],
152
+ ['--full-auto'], ['--dangerously-bypass-approvals-and-sandbox'], ['--dangerously-bypass-hook-trust'],
153
+ ['--oss'], ['--local-provider', 'x'],
153
154
  ['-p', 'prof'], ['--profile', 'prof'], ['-m', 'gpt-5.5'], ['--model', 'gpt-5.5'],
154
155
  ['-o', '/x'], ['--output-last-message', '/x'], ['--json'], ['--color', 'always'],
155
156
  ['--output-schema', '/x'], ['--ephemeral'],
@@ -591,3 +592,161 @@ describe('codex-exec.sh — session id absent', () => {
591
592
  assert.match(r.stdout, /FAKE_FINAL_MESSAGE/, 'the run still succeeds');
592
593
  });
593
594
  });
595
+
596
+ // ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
597
+ // The manifest roles.execute.contract is the single machine-readable source of the
598
+ // driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
599
+ // both directions, incl. the TIERED guarded-passthrough sets), (b) the wrapper's
600
+ // REAL parser arms equal the declared sets (source-level reverse guard — the
601
+ // git-shim heredoc's own `case` arms are NOT CLI modes and must be skipped).
602
+ // Helpers are inline — each bridge test file stays standalone (mirror byte-equality).
603
+
604
+ const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
605
+ const EXEC_CONTRACT = MANIFEST.roles.execute.contract;
606
+ const norm = (s) => s.replace(/\s+/g, ' ').trim();
607
+ const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
608
+ const leadingFlag = (descriptor) => {
609
+ const m = norm(descriptor).match(/(^|\s)(--[a-z-]+)/);
610
+ assert.ok(m, `descriptor "${descriptor}" carries no --flag token`);
611
+ return m[2];
612
+ };
613
+
614
+ // Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd with no
615
+ // AGENTS.md — proving the short-circuit fires BEFORE every preflight.
616
+ const runHelp = (arg) => {
617
+ const root = mkdtempSync(join(tmpdir(), 'codex-exec-help-'));
618
+ const nongit = join(root, 'nongit');
619
+ mkdirSync(nongit, { recursive: true });
620
+ const path = makePathWithout(root, ['codex', 'agy', 'git']);
621
+ const r = spawnSync('bash', [WRAPPER, arg], {
622
+ cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
623
+ });
624
+ rmSync(root, { recursive: true, force: true });
625
+ return r;
626
+ };
627
+
628
+ // The lines of a labelled --help section (header line → the next blank line).
629
+ const helpSection = (text, header) => {
630
+ const lines = text.split('\n');
631
+ const i = lines.findIndex((l) => l.trim() === header);
632
+ assert.notEqual(i, -1, `--help must carry a "${header}" section`);
633
+ const out = [];
634
+ for (let j = i + 1; j < lines.length; j += 1) {
635
+ if (lines[j].trim() === '') break;
636
+ out.push(lines[j].trim());
637
+ }
638
+ return out;
639
+ };
640
+
641
+ // Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
642
+ // statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
643
+ // heredoc bodies (codex-exec's git-shim heredoc carries its own `case "$verb"`
644
+ // git-verb arms that are NOT CLI modes). Returns Map(subject → [raw arm label, …]).
645
+ const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
646
+ const extractArgCaseArms = (source) => {
647
+ const arms = new Map();
648
+ const stack = [];
649
+ let heredoc = null;
650
+ for (const raw of source.split('\n')) {
651
+ if (heredoc) {
652
+ if (raw.trim() === heredoc) heredoc = null;
653
+ continue;
654
+ }
655
+ if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
656
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
657
+ if (hd) { heredoc = hd[1]; continue; }
658
+ const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
659
+ if (cs) { stack.push(cs[1]); continue; }
660
+ if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
661
+ if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
662
+ const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
663
+ if (arm) {
664
+ const subject = stack[stack.length - 1];
665
+ if (!arms.has(subject)) arms.set(subject, []);
666
+ arms.get(subject).push(arm[1].trim());
667
+ }
668
+ }
669
+ }
670
+ return arms;
671
+ };
672
+ const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
673
+
674
+ describe('codex-exec.sh — --help contract (manifest-pinned)', () => {
675
+ it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
676
+ for (const arg of ['--help', '-h']) {
677
+ const r = runHelp(arg);
678
+ assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
679
+ assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
680
+ assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
681
+ }
682
+ });
683
+
684
+ it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
685
+ const help = runHelp('--help').stdout;
686
+ const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('codex-exec')).map(norm);
687
+ assert.ok(EXEC_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
688
+ setEq(got, EXEC_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
689
+ });
690
+
691
+ it('Grounding renders the manifest grounding note verbatim', () => {
692
+ const help = runHelp('--help').stdout;
693
+ assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(EXEC_CONTRACT.grounding));
694
+ });
695
+
696
+ it('Round-2 / resume set-EQUALS the manifest continue descriptors', () => {
697
+ const help = runHelp('--help').stdout;
698
+ const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('codex-exec')).map(norm);
699
+ assert.ok(EXEC_CONTRACT.continue.length > 0, 'manifest continue must be non-empty');
700
+ setEq(got, EXEC_CONTRACT.continue.map(norm), 'help continue ⟷ manifest continue');
701
+ });
702
+
703
+ it('the guarded-passthrough TIERS set-EQUAL the manifest tiers (never a flat set)', () => {
704
+ const help = runHelp('--help').stdout;
705
+ const section = helpSection(help, "Guarded passthrough after '--':");
706
+ const tier = (prefix) => {
707
+ const line = section.find((l) => l.startsWith(prefix));
708
+ assert.ok(line, `passthrough section must carry a "${prefix}" line`);
709
+ return line.slice(prefix.length).trim().split(/\s+/);
710
+ };
711
+ assert.ok(EXEC_CONTRACT.passthrough.blocked.length > 0, 'manifest blocked tier must be non-empty');
712
+ assert.ok(EXEC_CONTRACT.passthrough.probeRelaxed.length > 0, 'manifest probe tier must be non-empty');
713
+ setEq(tier('blocked always:'), EXEC_CONTRACT.passthrough.blocked, 'help tier-1 ⟷ manifest blocked');
714
+ setEq(tier('relaxed only under CODEX_PROBE=1:'), EXEC_CONTRACT.passthrough.probeRelaxed, 'help tier-2 ⟷ manifest probeRelaxed');
715
+ });
716
+
717
+ it('--help after the -- separator is passthrough payload, never intercepted', () => {
718
+ const sb = makeSandbox();
719
+ const r = run(sb, { args: ['-', '--', '--help'], input: 'go' });
720
+ rmSync(sb.root, { recursive: true, force: true });
721
+ assert.equal(r.status, 0, r.stderr);
722
+ assert.doesNotMatch(r.stdout, /Usage:/, 'help is keyed on the FIRST argument only');
723
+ assert.match(r.argv, /(^|\n)--help(\n|$)/, 'the payload --help reaches codex argv');
724
+ });
725
+ });
726
+
727
+ describe('codex-exec.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
728
+ const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
729
+
730
+ it('the first-arg entrypoints are exactly --help/-h + the manifest resume flags', () => {
731
+ const declared = EXEC_CONTRACT.continue.map(leadingFlag);
732
+ assert.ok(declared.length > 0, 'manifest resume set must be non-empty');
733
+ setEq(new Set(splitArms(arms.get('"${1:-}"'))), new Set(['--help', '-h', ...declared]));
734
+ });
735
+
736
+ it('the real passthrough tier arms equal the manifest tiers (git-shim heredoc excluded)', () => {
737
+ const tierArms = arms.get('"$_arg"') ?? [];
738
+ assert.equal(tierArms.length, 2, 'exactly two passthrough tiers: always-blocked, probe-relaxed');
739
+ setEq(tierArms[0].split('|'), EXEC_CONTRACT.passthrough.blocked, 'tier-1 arm ⟷ manifest blocked');
740
+ setEq(tierArms[1].split('|'), EXEC_CONTRACT.passthrough.probeRelaxed, 'tier-2 arm ⟷ manifest probeRelaxed');
741
+ });
742
+
743
+ it('the in-test tier samples cover every manifest tier pattern (behavioural forward guard)', () => {
744
+ // ALWAYS_BLOCKED / PROBE_RELAXABLE drive the real behaviour suite above; pin them
745
+ // to the manifest so a tier edit cannot leave the behavioural samples stale.
746
+ const sample = (patterns) => patterns.map((p) => p.replace(/\*$/, ''));
747
+ const covered = (flags, patterns) =>
748
+ sample(patterns).every((p) => flags.some(([f]) => f === p || f.startsWith(p)));
749
+ assert.ok(covered(ALWAYS_BLOCKED, EXEC_CONTRACT.passthrough.blocked), 'every blocked pattern has a behavioural sample');
750
+ assert.ok(covered(PROBE_RELAXABLE, EXEC_CONTRACT.passthrough.probeRelaxed), 'every probe pattern has a behavioural sample');
751
+ });
752
+ });
@@ -31,6 +31,34 @@
31
31
  # on disk remain readable under read-only — read-scoping is a prompt concern).
32
32
  set -euo pipefail
33
33
 
34
+ # --- --help / -h (pre-preflight: no codex, no login, no git tree needed) -------
35
+ # Keyed ONLY on the FIRST argument — never a scan of all args (uniform rule across
36
+ # the four wrappers, so an open wrapper's passthrough payload is never intercepted).
37
+ # The contract below is drift-guarded against capability.json roles.review.contract.
38
+ case "${1:-}" in
39
+ --help|-h)
40
+ cat <<'HELP'
41
+ codex-review — read-only ADVISORY review by the OpenAI Codex CLI (subscription-only; frontier model at max effort).
42
+
43
+ Usage:
44
+ codex-review plan <plan-file>
45
+ codex-review code [extra focus...]
46
+
47
+ Grounding:
48
+ automatic — the wrapper precomputes the full working-tree change set (repo map,
49
+ status, diffs, untracked contents) and codex auto-merges the root AGENTS.md;
50
+ no grounding flags
51
+
52
+ Round-2 / resume:
53
+ (none — one-shot; a follow-up review is a fresh run)
54
+
55
+ Environment: CODEX_REVIEW_SCHEMA=1 (structured JSON findings), CODEX_HARD_TIMEOUT (seconds, default 1800), CODEX_PROBE=1 (throwaway probe only).
56
+ Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
57
+ HELP
58
+ exit 0
59
+ ;;
60
+ esac
61
+
34
62
  DEFAULT_CODEX_MODEL="gpt-5.5"
35
63
  DEFAULT_CODEX_EFFORT="xhigh"
36
64
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
@@ -540,3 +540,130 @@ describe('codex-review.sh — assemble & output edge cases', () => {
540
540
  assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
541
541
  });
542
542
  });
543
+
544
+ // ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
545
+ // The manifest roles.review.contract is the single machine-readable source of the
546
+ // driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
547
+ // both directions), (b) the wrapper's REAL parser arms equal the declared sets
548
+ // (source-level reverse guard), (c) each declared mode is really accepted.
549
+ // Helpers are inline — each bridge test file stays standalone (mirror byte-equality).
550
+
551
+ const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
552
+ const REVIEW_CONTRACT = MANIFEST.roles.review.contract;
553
+ const norm = (s) => s.replace(/\s+/g, ' ').trim();
554
+ const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
555
+
556
+ // Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd with no
557
+ // AGENTS.md — proving the short-circuit fires BEFORE every preflight.
558
+ const runHelp = (arg) => {
559
+ const root = mkdtempSync(join(tmpdir(), 'codex-review-help-'));
560
+ const nongit = join(root, 'nongit');
561
+ mkdirSync(nongit, { recursive: true });
562
+ const path = makePathWithout(root, ['codex', 'agy', 'git']);
563
+ const r = spawnSync('bash', [WRAPPER, arg], {
564
+ cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
565
+ });
566
+ rmSync(root, { recursive: true, force: true });
567
+ return r;
568
+ };
569
+
570
+ // The lines of a labelled --help section (header line → the next blank line).
571
+ const helpSection = (text, header) => {
572
+ const lines = text.split('\n');
573
+ const i = lines.findIndex((l) => l.trim() === header);
574
+ assert.notEqual(i, -1, `--help must carry a "${header}" section`);
575
+ const out = [];
576
+ for (let j = i + 1; j < lines.length; j += 1) {
577
+ if (lines[j].trim() === '') break;
578
+ out.push(lines[j].trim());
579
+ }
580
+ return out;
581
+ };
582
+
583
+ // Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
584
+ // statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
585
+ // heredoc bodies (a heredoc may carry non-CLI `case` arms — e.g. codex-exec's
586
+ // git-shim). Returns Map(subject → [raw arm label, …]) in source order.
587
+ const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
588
+ const extractArgCaseArms = (source) => {
589
+ const arms = new Map();
590
+ const stack = [];
591
+ let heredoc = null;
592
+ for (const raw of source.split('\n')) {
593
+ if (heredoc) {
594
+ if (raw.trim() === heredoc) heredoc = null;
595
+ continue;
596
+ }
597
+ if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
598
+ const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
599
+ if (hd) { heredoc = hd[1]; continue; }
600
+ const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
601
+ if (cs) { stack.push(cs[1]); continue; }
602
+ if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
603
+ if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
604
+ const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
605
+ if (arm) {
606
+ const subject = stack[stack.length - 1];
607
+ if (!arms.has(subject)) arms.set(subject, []);
608
+ arms.get(subject).push(arm[1].trim());
609
+ }
610
+ }
611
+ }
612
+ return arms;
613
+ };
614
+ const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
615
+
616
+ describe('codex-review.sh — --help contract (manifest-pinned)', () => {
617
+ it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
618
+ for (const arg of ['--help', '-h']) {
619
+ const r = runHelp(arg);
620
+ assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
621
+ assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
622
+ assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
623
+ }
624
+ });
625
+
626
+ it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
627
+ const help = runHelp('--help').stdout;
628
+ const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('codex-review')).map(norm);
629
+ assert.ok(REVIEW_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
630
+ setEq(got, REVIEW_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
631
+ });
632
+
633
+ it('Grounding renders the manifest grounding note verbatim', () => {
634
+ const help = runHelp('--help').stdout;
635
+ assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(REVIEW_CONTRACT.grounding));
636
+ });
637
+
638
+ it('Round-2 / resume set-EQUALS the manifest continue descriptors (empty — one-shot)', () => {
639
+ const help = runHelp('--help').stdout;
640
+ const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('codex-review')).map(norm);
641
+ setEq(got, (REVIEW_CONTRACT.continue ?? []).map(norm), 'help continue ⟷ manifest continue');
642
+ assert.deepEqual(REVIEW_CONTRACT.continue, [], 'codex-review is one-shot — no continue descriptor');
643
+ });
644
+ });
645
+
646
+ describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
647
+ const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
648
+
649
+ it('the real mode arms equal the manifest modes (adding a mode without the manifest fails here)', () => {
650
+ const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
651
+ assert.ok(MANIFEST.roles.review.modes.length > 0, 'manifest modes must be non-empty');
652
+ setEq(new Set(modes), MANIFEST.roles.review.modes, 'parser mode arms ⟷ manifest modes');
653
+ });
654
+
655
+ it('the first-arg entrypoints are exactly --help/-h (no undeclared resume/flag entrypoint)', () => {
656
+ setEq(new Set(splitArms(arms.get('"${1:-}"'))), ['--help', '-h']);
657
+ });
658
+
659
+ it('every manifest mode is really accepted (forward guard)', () => {
660
+ const drive = { plan: ['plan', 'plan.md'], code: ['code'] };
661
+ for (const mode of MANIFEST.roles.review.modes) {
662
+ assert.ok(drive[mode], `no test drive for manifest mode "${mode}" — add one`);
663
+ const sb = makeSandbox();
664
+ const r = run(sb, { args: drive[mode] });
665
+ rmSync(sb.root, { recursive: true, force: true });
666
+ assert.equal(r.status, 0, `mode ${mode}: ${r.stderr}`);
667
+ }
668
+ });
669
+ });
@@ -3,11 +3,44 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.0.0",
6
+ "version": "2.1.0",
7
7
  "provides": ["execute", "review"],
8
8
  "roles": {
9
- "execute": { "cmd": "codex-exec", "source": "bin/codex-exec.sh", "output": "diff" },
10
- "review": { "cmd": "codex-review", "source": "bin/codex-review.sh", "modes": ["plan", "code"], "output": "advisory" }
9
+ "execute": {
10
+ "cmd": "codex-exec",
11
+ "source": "bin/codex-exec.sh",
12
+ "output": "diff",
13
+ "contract": {
14
+ "invocations": [
15
+ "codex-exec <plan-file|->",
16
+ "codex-exec <plan-file|-> -- <extra codex flags...>"
17
+ ],
18
+ "grounding": "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
19
+ "continue": [
20
+ "codex-exec --resume-last <plan-file|->",
21
+ "codex-exec --resume <session-id> <plan-file|->"
22
+ ],
23
+ "passthrough": {
24
+ "policy": "guarded",
25
+ "blocked": ["-c*", "--config*", "-s*", "--sandbox*", "--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust", "--full-auto", "--oss", "--local-provider*", "-p*", "--profile*", "-m*", "--model*", "-o*", "--output-last-message*", "--json*", "--color*", "--output-schema*", "--ephemeral*"],
26
+ "probeRelaxed": ["--add-dir*", "-C*", "--cd*", "--skip-git-repo-check", "--ignore-rules", "--enable*", "--disable*"]
27
+ }
28
+ }
29
+ },
30
+ "review": {
31
+ "cmd": "codex-review",
32
+ "source": "bin/codex-review.sh",
33
+ "modes": ["plan", "code"],
34
+ "output": "advisory",
35
+ "contract": {
36
+ "invocations": [
37
+ "codex-review plan <plan-file>",
38
+ "codex-review code [extra focus...]"
39
+ ],
40
+ "grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
41
+ "continue": []
42
+ }
43
+ }
11
44
  },
12
45
  "detect": {
13
46
  "installed": {
@@ -50,7 +50,7 @@ object (raw-text fallback on failure).
50
50
  ```bash
51
51
  codex-exec docs/plans/<slug>.md # drive a plan file
52
52
  echo "apply review fix: tighten the guard in X, keep tests green" | codex-exec -
53
- codex-exec <file|-> -- <unguarded codex flags> # passthrough AFTER `--` (policy/model/capture flags are rejected)
53
+ codex-exec <file|-> -- <extra codex flags> # GUARDED passthrough AFTER `--` (policy/model/capture flags rejected; some relaxed only under CODEX_PROBE=1)
54
54
 
55
55
  codex-exec --resume-last docs/plans/<slug>.md # continue the last session, no re-send
56
56
  echo "now do step 2" | codex-exec --resume <id> -
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.24.0",
6
+ "version": "1.25.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -113,7 +113,7 @@ const CATALOG = [
113
113
  invocation: invocationOf('procedures'),
114
114
  group: 'Orchestrate',
115
115
  kind: READ_ONLY,
116
- oneLine: 'Show a named activity’s steps and which recipe applies at each stage here.',
116
+ oneLine: 'Show a named activity’s steps, the recipe per stage, and each dispatched backend’s exact driving contract (invocation + grounding + round-2 delta).',
117
117
  },
118
118
  {
119
119
  key: 'set-recipe',
@@ -60,6 +60,36 @@ const RAW_BACKENDS = [
60
60
  name: 'codex-cli-bridge',
61
61
  installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
62
62
  roleCmds: { execute: 'codex-exec', review: 'codex-review' },
63
+ // The per-role DRIVING CONTRACT (exact invocation descriptors + grounding + round-2 continue),
64
+ // mirroring the bridge manifest roles[role].contract byte-for-byte — drift-guarded like roleCmds.
65
+ // Scope = dispatchable recipe roles ONLY (review, execute): the probe role is never dispatched by
66
+ // an activity slot, so it carries NO contract here (wrapperContractFor(_, 'probe') → null).
67
+ roleContracts: {
68
+ execute: {
69
+ invocations: [
70
+ 'codex-exec <plan-file|->',
71
+ 'codex-exec <plan-file|-> -- <extra codex flags...>',
72
+ ],
73
+ grounding: "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
74
+ continue: [
75
+ 'codex-exec --resume-last <plan-file|->',
76
+ 'codex-exec --resume <session-id> <plan-file|->',
77
+ ],
78
+ passthrough: {
79
+ policy: 'guarded',
80
+ blocked: ['-c*', '--config*', '-s*', '--sandbox*', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust', '--full-auto', '--oss', '--local-provider*', '-p*', '--profile*', '-m*', '--model*', '-o*', '--output-last-message*', '--json*', '--color*', '--output-schema*', '--ephemeral*'],
81
+ probeRelaxed: ['--add-dir*', '-C*', '--cd*', '--skip-git-repo-check', '--ignore-rules', '--enable*', '--disable*'],
82
+ },
83
+ },
84
+ review: {
85
+ invocations: [
86
+ 'codex-review plan <plan-file>',
87
+ 'codex-review code [extra focus...]',
88
+ ],
89
+ grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
90
+ continue: [],
91
+ },
92
+ },
63
93
  bin: 'codex',
64
94
  credential: { env: 'CODEX_HOME', default: '~/.codex', file: 'auth.json' },
65
95
  setupUrl: 'https://github.com/sabaiway/agent-workflow/blob/main/codex-cli-bridge/setup/README.md',
@@ -72,6 +102,26 @@ const RAW_BACKENDS = [
72
102
  name: 'antigravity-cli-bridge',
73
103
  installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
74
104
  roleCmds: { review: 'agy-review', probe: 'agy-run' },
105
+ // Mirror of the manifest roles.review.contract (see the codex entry note). probe: NO contract.
106
+ roleContracts: {
107
+ review: {
108
+ invocations: [
109
+ 'agy-review code [--facts @f] [--decided @f] [--focus "…"] [extra focus…]',
110
+ 'agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]',
111
+ 'agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]',
112
+ ],
113
+ grounding: 'grounded review — agy reads NOTHING by default, an ungrounded review GUESSES: --facts @f = the verified facts to review AGAINST; --decided @f = decisions already made, do NOT re-raise (anti-circling)',
114
+ flags: [
115
+ '--facts @f — verified facts the review runs AGAINST (omit ⇒ loud ungrounded-review warning)',
116
+ '--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)',
117
+ '--focus "…" — extra focus (repeatable; code mode also takes trailing focus words)',
118
+ ],
119
+ continue: [
120
+ 'agy-review --continue [--decided @f] [--focus "…"]',
121
+ 'agy-review --conversation <id> [--decided @f] [--focus "…"]',
122
+ ],
123
+ },
124
+ },
75
125
  bin: 'agy',
76
126
  credential: { env: null, default: '~/.gemini/antigravity-cli', file: 'antigravity-oauth-token' },
77
127
  setupUrl: 'https://github.com/sabaiway/agent-workflow/blob/main/antigravity-cli-bridge/setup/README.md',
@@ -91,6 +141,15 @@ export const KNOWN_BACKENDS = RAW_BACKENDS.map((entry) => ({ ...entry, wrapperCm
91
141
  export const wrapperCmdFor = (backendName, role) =>
92
142
  KNOWN_BACKENDS.find((b) => b.name === backendName)?.roleCmds?.[role] ?? null;
93
143
 
144
+ // Resolve a dispatched (backend, role) to its structured DRIVING CONTRACT — the registry mirror of
145
+ // the bridge manifest roles[role].contract: exact invocation descriptor(s), the grounding note, the
146
+ // closed flag set (when the wrapper's grammar is closed), the round-2/continue descriptor(s), and
147
+ // the guarded passthrough tiers (codex-exec). The point-of-use advisor (procedures.mjs) renders this
148
+ // VERBATIM so a driving agent never re-derives the contract from wrapper source. `null` when the
149
+ // pair is unknown or the role carries no contract (probe — never dispatched by an activity slot).
150
+ export const wrapperContractFor = (backendName, role) =>
151
+ KNOWN_BACKENDS.find((b) => b.name === backendName)?.roleContracts?.[role] ?? null;
152
+
94
153
  // ── pure helpers ─────────────────────────────────────────────────────────────
95
154
 
96
155
  // Expand a leading "~" / "~/x" against home; absolute and relative paths pass through untouched.
@@ -38,6 +38,21 @@ Each `roles.<role>` is an object:
38
38
  - `template` (string, optional) — an in-skill prompt/template path (e.g.
39
39
  `references/review-prompt.md`); repo-relative, **must exist**.
40
40
  - `modes`, `output` (optional) — e.g. `["plan","code"]`, `"advisory"`.
41
+ - `contract` (object, optional; execution-backend bridges) — the machine-readable **driving
42
+ contract** for a dispatchable recipe role (`review` / `execute`; the `probe` role carries none).
43
+ Rides as a validator-tolerated extra field (format-checked by the bridge/kit drift-guard tests,
44
+ not by `validate.mjs`), and is the single source the point-of-use advisor
45
+ (`/agent-workflow-kit procedures`) and each wrapper's `--help` render verbatim:
46
+ - `invocations` (string[], non-empty) — exact copy-pasteable invocation descriptors, one per
47
+ mode/variant, incl. operand placeholders (`<plan-file>`, `[extra focus...]`).
48
+ - `grounding` (string) — the grounding note (e.g. agy's `--facts @f` / `--decided @f` levers, or
49
+ "automatic — …" when the wrapper grounds itself).
50
+ - `flags` (string[], optional) — the closed per-mode flag descriptor set (closed-grammar
51
+ wrappers only).
52
+ - `continue` (string[]) — round-2 / resume invocation descriptors; `[]` when one-shot.
53
+ - `passthrough` (object, optional) — the guarded `--` passthrough tiers:
54
+ `{ policy: "guarded", blocked: string[], probeRelaxed: string[] }`, matching the wrapper's
55
+ real case-arm patterns (pinned by the source-level reverse-guard test).
41
56
 
42
57
  ## Path-field rules (Windows-safe, traversal-safe)
43
58
 
@@ -19,7 +19,7 @@
19
19
  import { readFileSync, lstatSync } from 'node:fs';
20
20
  import { homedir } from 'node:os';
21
21
  import { pathToFileURL } from 'node:url';
22
- import { detectBackends, wrapperCmdFor } from './detect-backends.mjs';
22
+ import { detectBackends, wrapperCmdFor, wrapperContractFor } from './detect-backends.mjs';
23
23
  import { ACTIVITIES, resolveActivityRecipe, planRecipe } from './recipes.mjs';
24
24
  import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
25
25
  // The config schema/read core lives in orchestration-config.mjs (the single config contract). procedures
@@ -131,10 +131,16 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) =>
131
131
  // The concrete wrapper set this slot's EFFECTIVE recipe dispatches (empty for solo). Reuse
132
132
  // planRecipe's drift-guarded dispatch for WHICH backends, then resolve each (backend, role) to its
133
133
  // manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here.
134
- const backends = planRecipe(resolved.recipe, detection).dispatch
135
- .map((d) => wrapperCmdFor(d.backend, d.role))
136
- .filter(Boolean);
137
- return { slot, ...resolved, backends };
134
+ const { dispatch } = planRecipe(resolved.recipe, detection);
135
+ const backends = dispatch.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
136
+ // The full DRIVING CONTRACT per dispatched (backend, role) — resolved HERE, on the raw dispatch
137
+ // pairs, BEFORE they are flattened to wrapper names (the name array cannot reconstruct the role).
138
+ // Every slot with a non-empty dispatch gets contracts — including execute=delegated; the contract
139
+ // is NEVER gated by REVIEW_RECIPES (that set gates only the review-loop economics block).
140
+ const contracts = dispatch
141
+ .map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
142
+ .filter((c) => c.cmd && c.contract);
143
+ return { slot, ...resolved, backends, contracts };
138
144
  });
139
145
 
140
146
  // An unsatisfiable EXPLICIT override is the only "warning" (loud, flagged for the agent to relay). A
@@ -182,6 +188,28 @@ const reviewLoopAdvice = (slots) =>
182
188
  ]
183
189
  : [];
184
190
 
191
+ // The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
192
+ // the closed flag set, the grounding note, the round-2/continue delta, and the guarded passthrough
193
+ // tiers — every descriptor printed VERBATIM from the registry mirror of the bridge manifest
194
+ // roles[role].contract (drift-guarded), never re-derived or re-worded here. Rendered for EVERY
195
+ // dispatched backend of EVERY slot (review AND execute=delegated); each wrapper's --help prints the
196
+ // same contract, so the agent never needs to open the wrapper source.
197
+ const contractLines = ({ cmd, contract }) => {
198
+ const lines = [` ${cmd} — driving contract (as bundled with this kit; copy-paste — \`${cmd} --help\` prints the same):`];
199
+ for (const inv of contract.invocations) lines.push(` ${inv}`);
200
+ for (const f of contract.flags ?? []) lines.push(` ${f}`);
201
+ if (contract.grounding) lines.push(` grounding: ${contract.grounding}`);
202
+ const cont = contract.continue ?? [];
203
+ if (cont.length) {
204
+ lines.push(' round-2 delta (resume — never re-send the reviewed artifact):');
205
+ for (const c of cont) lines.push(` ${c}`);
206
+ }
207
+ if (contract.passthrough) {
208
+ lines.push(` passthrough after '--' is ${contract.passthrough.policy}: blocked always: ${contract.passthrough.blocked.join(' ')}; relaxed only under CODEX_PROBE=1: ${contract.passthrough.probeRelaxed.join(' ')}`);
209
+ }
210
+ return lines;
211
+ };
212
+
185
213
  const formatHuman = ({ activity, section, slots, warnings }) => {
186
214
  const lines = [
187
215
  section,
@@ -192,6 +220,7 @@ const formatHuman = ({ activity, section, slots, warnings }) => {
192
220
  const arrow = s.degradedFrom ? ` (requested ${s.degradedFrom} → degraded)` : '';
193
221
  lines.push(` ${s.slot}: ${s.recipe} — ${SOURCE_LABEL[s.source]}${arrow}${backendSetLabel(s.backends)}`);
194
222
  if (s.reason) lines.push(` ↳ ${s.reason}`);
223
+ for (const c of s.contracts ?? []) lines.push(...contractLines(c));
195
224
  }
196
225
  const advice = reviewLoopAdvice(slots);
197
226
  if (advice.length) lines.push('', ...advice);
@@ -206,7 +235,9 @@ const buildJson = ({ activity, section, slots, configSource, warnings }) => ({
206
235
  activity,
207
236
  section,
208
237
  slots: Object.fromEntries(
209
- slots.map((s) => [s.slot, { recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends }]),
238
+ // `backends: string[]` is the STABLE pre-existing shape (wrapper names) never repurposed.
239
+ // `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
240
+ slots.map((s) => [s.slot, { recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts }]),
210
241
  ),
211
242
  reviewLoop: reviewLoopAdvice(slots),
212
243
  configSource,