@sabaiway/agent-workflow-kit 10.3.0 → 10.5.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +5 -5
  3. package/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
  6. package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
  7. package/bridges/antigravity-cli-bridge/capability.json +2 -2
  8. package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
  9. package/bridges/codex-cli-bridge/SKILL.md +8 -1
  10. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  11. package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
  12. package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
  13. package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
  14. package/bridges/codex-cli-bridge/capability.json +2 -2
  15. package/capability.json +1 -1
  16. package/package.json +1 -1
  17. package/references/agents/executor.md +40 -0
  18. package/references/agents/review-lens.md +5 -3
  19. package/references/modes/agents.md +9 -4
  20. package/references/modes/procedures.md +21 -8
  21. package/references/modes/recipes.md +7 -4
  22. package/references/modes/recommendations.md +3 -1
  23. package/references/modes/set-recipe.md +23 -6
  24. package/references/modes/status.md +2 -2
  25. package/references/modes/upgrade.md +1 -1
  26. package/references/modes/velocity.md +1 -0
  27. package/references/shared/composition-handoff.md +1 -1
  28. package/references/shared/deploy-tail.md +1 -1
  29. package/references/templates/orchestration.json +1 -1
  30. package/tools/autonomy-config.mjs +1 -1
  31. package/tools/bridge-posture.mjs +48 -0
  32. package/tools/carriers.mjs +152 -0
  33. package/tools/cheap-agents-read.mjs +234 -0
  34. package/tools/cheap-agents.mjs +101 -109
  35. package/tools/commands.mjs +3 -3
  36. package/tools/detect-backends.mjs +2 -2
  37. package/tools/direct-run.mjs +9 -0
  38. package/tools/family-registry.mjs +38 -18
  39. package/tools/flow-check.mjs +2 -7
  40. package/tools/fold-scope.mjs +5 -60
  41. package/tools/grounding.mjs +2 -2
  42. package/tools/inject-methodology.mjs +4 -0
  43. package/tools/orchestration-config.mjs +23 -61
  44. package/tools/orchestration-readme.mjs +70 -0
  45. package/tools/plan-shape-cli.mjs +112 -0
  46. package/tools/plan-shape-facts.mjs +204 -0
  47. package/tools/plan-shape.mjs +348 -0
  48. package/tools/procedures.mjs +197 -83
  49. package/tools/recipes.mjs +183 -230
  50. package/tools/recommendations.mjs +77 -11
  51. package/tools/renderers.mjs +27 -7
  52. package/tools/repo-lex.mjs +40 -0
  53. package/tools/review-roster-resolve.mjs +104 -0
  54. package/tools/review-roster.mjs +128 -0
  55. package/tools/review-rounds-cli.mjs +92 -0
  56. package/tools/review-rounds.mjs +115 -0
  57. package/tools/review-state.mjs +10 -11
  58. package/tools/set-recipe-roster.mjs +167 -0
  59. package/tools/set-recipe.mjs +138 -42
  60. package/tools/velocity-profile.mjs +8 -22
  61. package/tools/view-model.mjs +17 -3
package/tools/recipes.mjs CHANGED
@@ -1,25 +1,31 @@
1
1
  #!/usr/bin/env node
2
- // Recipe planner the pure brain behind the read-only `/agent-workflow-kit recipes` advisor.
3
- //
4
- // A "recipe" is a NAMED orchestration pattern over the family's optional execution-backends (the
5
- // subscription-CLI bridges: codex-cli-bridge → `codex`, antigravity-cli-bridge → `agy`), composed
6
- // into the plan → execute → review flow. The ENGINE owns the canonical narrative
7
- // (agent-workflow-engine/references/orchestration.md — the when/why, kept in lockstep by the
8
- // recipe-name parity guard in recipes.test.mjs); this module owns the EXECUTABLE dispatch: given a
9
- // recipe + the read-only detector's view of the environment, which backend does which role, how it
10
- // degrades when a backend isn't ready, and the advisory quota/health notes.
11
- //
12
- // Invariants (the backends/status posture): pure (no fs/network/CLI in planRecipe/recommendRecipe),
13
- // read-only, NEVER runs a subscription CLI. The kit only surfaces/selects/plans a recipe — the
14
- // orchestrator (the main agent) executes it via the bridge skills and always makes the single commit;
15
- // a backend is advisory or delegated, never autonomous. Dependency-free, Node >= 22.
2
+ // Read-only recipe planner. The engine owns the narrative; carriers.mjs owns the activity/slot
3
+ // registry re-exported here. Pure planning functions never run a subscription CLI.
16
4
 
17
5
  import { existsSync, readFileSync } from 'node:fs';
18
6
  import { dirname, join, resolve } from 'node:path';
19
- // The host-level bridge-settings snapshot (fact-only, best-effort). READ-ONLY core only — never the
20
- // writer — so this read-only advisor never pulls in the atomic-write core.
21
7
  import { settingsSnapshot, DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
22
8
  import { isDirectRun } from './direct-run.mjs';
9
+ import {
10
+ ACTIVITIES,
11
+ POLICY_ACTIVITIES,
12
+ SLOT_RECIPES,
13
+ SUBAGENT_RECIPE,
14
+ CARRY_ROLE,
15
+ EXECUTOR_PROVIDER,
16
+ SWITCH_DEFAULT,
17
+ isSwitchSlot,
18
+ withVehicle,
19
+ vehicleDegradeReason,
20
+ EXECUTOR_APPLY,
21
+ safeLine,
22
+ DISPLAY_ALIASES,
23
+ BACKEND_PRIORITY,
24
+ } from './carriers.mjs';
25
+ import { surveyExecutorVehicle, surveyVehicle } from './cheap-agents-read.mjs';
26
+ import { obligationsOf } from './review-roster.mjs';
27
+ import { resolveRoster, activeLineCell, remedyFor } from './review-roster-resolve.mjs';
28
+ import { posturesByBackend } from './bridge-posture.mjs';
23
29
  import {
24
30
  detectBackends,
25
31
  wrapperCmdFor,
@@ -30,33 +36,35 @@ import {
30
36
  DEGRADED,
31
37
  } from './detect-backends.mjs';
32
38
 
33
- const CODEX = 'codex-cli-bridge';
34
- const AGY = 'antigravity-cli-bridge';
39
+ export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APPLY, safeLine, DISPLAY_ALIASES };
35
40
 
36
- // The manifest-name human-alias map (the detector emits manifest names; humans say codex/agy).
37
- export const DISPLAY_ALIASES = { [CODEX]: 'codex', [AGY]: 'agy' };
41
+ const [CODEX, AGY] = BACKEND_PRIORITY;
38
42
 
39
- // The backend role table, keyed by the manifest name the detector emits (status.name) — NOT the
40
- // display alias. Drift-guarded against each bridge capability.json `provides[]` (recipes.test.mjs).
43
+ // Keyed by readiness-array provider name; executor is the only `carry` provider.
41
44
  export const BACKEND_ROLES = {
42
45
  [CODEX]: ['execute', 'review'],
43
46
  [AGY]: ['review', 'probe'],
47
+ [EXECUTOR_PROVIDER]: [CARRY_ROLE],
44
48
  };
45
49
 
46
- // Review obligations from the CONFIGURED plan-execution.review recipe — the RAW config value,
47
- // never the readiness-degraded effective recipe (a computed readiness-degrade NEVER silently
48
- // becomes solo). Homed HERE (cycle-free) so review-state AND flow-check consume the SAME consumed
49
- // backend set (#42 exact coverage must never fall open on the computed-default class).
50
- // solo → no obligation; reviewed ONE ship-class from ANY backend (minShip 1);
51
- // council → EVERY backend ship-class or degrade-recorded, and never all (minShip 1).
52
- export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false } = {}) => {
53
- const configured = config?.['plan-execution']?.review;
50
+ // Review obligations from the CONFIGURED activity's review recipe — the RAW config value, never
51
+ // the readiness-degraded one. The default preserves review-state and flow-check's backend set (#42).
52
+ export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false, activity = 'plan-execution' } = {}) => {
53
+ if (!Object.hasOwn(ACTIVITIES, activity) || !Object.hasOwn(ACTIVITIES[activity].slots, 'review')) {
54
+ throw new Error(`activity "${activity}" has no review slot`);
55
+ }
56
+ const configured = config?.[activity]?.review;
54
57
  const providers = Object.values(DISPLAY_ALIASES); // every review-capable backend, codex first
55
58
  if (configured == null && detectionFailed) {
56
59
  // No config + no readiness signal: the computed default is UNKNOWABLE — fail closed upstream.
57
60
  return { recipe: null, source: 'default', backends: [], minShip: 0, perBackend: false, unknowable: true };
58
61
  }
59
- const anyReady = readiness.some((b) => b.readiness === READY);
62
+ if (Array.isArray(configured)) {
63
+ return { ...obligationsOf(configured), source: 'config', unknowable: false };
64
+ }
65
+ // Role-filtered: a ready EXECUTOR vehicle is a carry provider, never a reviewer, so it must not
66
+ // turn a silent review config into `reviewed`.
67
+ const anyReady = readyProvidersOf('review', readiness).length >= 1;
60
68
  const recipe = configured ?? (anyReady ? 'reviewed' : 'solo');
61
69
  const source = configured != null ? 'config' : 'default';
62
70
  if (recipe === 'solo') return { recipe, source, backends: [], minShip: 0, perBackend: false, unknowable: false };
@@ -64,10 +72,8 @@ export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], de
64
72
  return { recipe, source, backends: providers, minShip: 1, perBackend: false, unknowable: false };
65
73
  };
66
74
 
67
- // Advisory metadata the DETECTION object does not carry (it has no cost/quota those live only in
68
- // capability.json). cost/quota are drift-guarded against the manifests; the agy `health` advisory is
69
- // static project knowledge (Issue-001: the Antigravity service can stall on substantive prompts —
70
- // invisible to file-presence detection, so it is NOT a readiness signal, only a standing caveat).
75
+ // Advisory metadata the detection object does not carry. cost/quota are drift-guarded against the
76
+ // manifests; agy's `health` (Issue-001) is invisible to detection a caveat, not a readiness signal.
71
77
  export const BACKEND_META = {
72
78
  [CODEX]: { cost: 'subscription', quota: { kind: 'subscription', finite: true } },
73
79
  [AGY]: {
@@ -77,17 +83,13 @@ export const BACKEND_META = {
77
83
  },
78
84
  };
79
85
 
80
- // Deterministic tie-break order: codex before agy. agy is a sound grounded reviewer now, but codex is
81
- // the more reliable default for substantive reviews (agy carries the standing service-stall caveat above).
82
- const BACKEND_PRIORITY = [CODEX, AGY];
83
86
  const priorityIndex = (name) => {
84
87
  const i = BACKEND_PRIORITY.indexOf(name);
85
88
  return i === -1 ? BACKEND_PRIORITY.length : i;
86
89
  };
87
90
 
88
- // The four recipes, in lattice order. `role` is the backend role a recipe needs (null = Solo, no
89
- // backend); `minBackends` is how many READY providers it needs; `degradesTo` is the next-weaker
90
- // recipe when it can't be satisfied (the chain terminates at Solo, which is always satisfiable).
91
+ // The five recipes, in lattice order. `degradesTo` is the next-weaker recipe when a recipe can't be
92
+ // satisfied; every chain terminates at Solo, which always is.
91
93
  export const RECIPES = [
92
94
  {
93
95
  id: 'solo',
@@ -121,33 +123,23 @@ export const RECIPES = [
121
123
  degradesTo: 'solo',
122
124
  summary: 'the orchestrator hands a bounded execution sub-task to a backend (codex exec), then reviews the diff and commits.',
123
125
  },
126
+ SUBAGENT_RECIPE,
124
127
  ];
125
128
 
126
129
  const recipeById = (id) => RECIPES.find((r) => r.id === id);
127
130
 
128
- // The human reason a non-ready readiness yields (read-only file-presence remedies — never a claim
129
- // about whether the backend's service is responsive).
130
- const READINESS_REASON = {
131
- [NEEDS_SKILL]: 'bridge skill not installed — run /agent-workflow-kit setup',
132
- [NEEDS_CLI]: 'the CLI is not installed',
133
- [NEEDS_CREDENTIALS]: 'not signed in (credentials missing)',
134
- [DEGRADED]: 'wrapper not on PATH — run /agent-workflow-kit setup',
135
- };
136
-
137
131
  // ── pure planner ───────────────────────────────────────────────────────────────
138
132
 
139
- // Backends (ready or not) whose role table includes `role`.
140
133
  const providersOf = (role, detection) => detection.filter((b) => (BACKEND_ROLES[b.name] ?? []).includes(role));
141
134
 
142
- // READY providers of `role`, in deterministic priority order (codex before agy) an array of names.
143
- const readyProvidersOf = (role, detection) =>
135
+ // In deterministic priority order, so a dispatch never depends on detection emission order.
136
+ const readyProviderEntriesOf = (role, detection) =>
144
137
  providersOf(role, detection)
145
138
  .filter((b) => b.readiness === READY)
146
- .sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
147
- .map((b) => b.name);
139
+ .sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name));
140
+
141
+ const readyProvidersOf = (role, detection) => readyProviderEntriesOf(role, detection).map((b) => b.name);
148
142
 
149
- // Availability = readiness === READY, full stop. A recipe is satisfiable iff it needs no backend OR
150
- // enough READY providers of its role exist.
151
143
  const isSatisfiable = (recipe, detection) =>
152
144
  recipe.role === null || readyProvidersOf(recipe.role, detection).length >= recipe.minBackends;
153
145
 
@@ -155,41 +147,44 @@ const isSatisfiable = (recipe, detection) =>
155
147
  const degradeReason = (recipe, detection) => {
156
148
  const providers = providersOf(recipe.role, detection);
157
149
  if (providers.length === 0) {
158
- return `${recipe.title} needs a backend providing ${recipe.role}, but no backend provides it`;
150
+ return `${recipe.title} needs a provider providing ${recipe.role}, but no provider provides it`;
159
151
  }
160
152
  const ready = providers.filter((b) => b.readiness === READY);
161
153
  const detail = providers
162
154
  .filter((b) => b.readiness !== READY)
163
- .map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name}: ${READINESS_REASON[b.readiness] ?? b.readiness}`)
155
+ .map((b) => (b.name === EXECUTOR_PROVIDER
156
+ ? vehicleDegradeReason(b.vehicle, EXECUTOR_APPLY)
157
+ : `${DISPLAY_ALIASES[b.name] ?? b.name}: ${remedyFor({ readiness: b.readiness })}`))
164
158
  .join('; ');
165
- return `${recipe.title} needs ${recipe.minBackends} backend(s) providing ${recipe.role}, but only ${ready.length} ready${detail ? ` — ${detail}` : ''}`;
159
+ return `${recipe.title} needs ${recipe.minBackends} provider(s) providing ${recipe.role}, but only ${ready.length} ready${detail ? ` — ${detail}` : ''}`;
166
160
  };
167
161
 
168
- // Per-stage dispatch for an EFFECTIVE (already-satisfiable) recipe: the first `minBackends` READY
169
- // providers, in priority order. Solo dispatches nothing (the orchestrator does it all).
162
+ // Per-stage dispatch for an EFFECTIVE (already-satisfiable) recipe. The executor's step carries its
163
+ // vehicle state, so both renders can name it.
170
164
  const dispatchFor = (recipe, detection) => {
171
165
  if (recipe.role === null) return [];
172
- return readyProvidersOf(recipe.role, detection)
166
+ return readyProviderEntriesOf(recipe.role, detection)
173
167
  .slice(0, recipe.minBackends)
174
- .map((name) => ({ role: recipe.role, backend: name, display: DISPLAY_ALIASES[name] }));
168
+ .map((b) => {
169
+ const step = { role: recipe.role, backend: b.name, display: DISPLAY_ALIASES[b.name] ?? b.name };
170
+ return b.vehicle ? { ...step, vehicle: b.vehicle.state } : step;
171
+ });
175
172
  };
176
173
 
177
174
  const QUOTA_NOTE = "Prefer the cheapest model that fits the task; don't reach for a top-tier model by reflex.";
178
175
  const COUNCIL_QUOTA_NOTE = "Council spends two backends' quota for one decision — reserve it for changes that justify the cost.";
179
176
 
180
- // Advisory notes for an effective recipe: a quota reminder when any backend is dispatched, the
181
- // two-quota caveat for Council, and the agy health advisory whenever the dispatch actually uses agy.
177
+ // The quota reminder rides a subscription-BACKEND dispatch only: the executor vehicle spends no
178
+ // bridge quota, and its recipe is a frontier one, so the cheapest-model reminder would contradict it.
182
179
  const notesFor = (recipe, dispatch) => {
183
180
  const notes = [];
184
- if (dispatch.length > 0) notes.push(QUOTA_NOTE);
181
+ if (dispatch.some((d) => d.role !== CARRY_ROLE)) notes.push(QUOTA_NOTE);
185
182
  if (recipe.id === 'council') notes.push(COUNCIL_QUOTA_NOTE);
186
183
  if (dispatch.some((d) => d.backend === AGY) && BACKEND_META[AGY].health) notes.push(BACKEND_META[AGY].health);
187
184
  return notes;
188
185
  };
189
186
 
190
- // planRecipe(recipe, detection) → pure plan. `recipe` is a recipe id or descriptor. Walks the
191
- // degradation chain (with a stated reason per step) until a satisfiable recipe is reached, then emits
192
- // the per-stage dispatch + advisory notes. Deterministic; never mutates the detection input.
187
+ // planRecipe → pure plan: walks the degradation chain, a stated reason per step, then dispatches.
193
188
  export const planRecipe = (recipe, detection) => {
194
189
  const requested = typeof recipe === 'string' ? recipeById(recipe) : recipe;
195
190
  if (!requested) throw new Error(`unknown recipe: ${recipe}`);
@@ -211,7 +206,7 @@ export const planRecipe = (recipe, detection) => {
211
206
  };
212
207
  };
213
208
 
214
- // How close to ready a non-ready backend is — used to surface the most-actionable remedy first.
209
+ // Rank = how close to ready, so the most actionable remedy surfaces first.
215
210
  const READINESS_RANK = { [DEGRADED]: 3, [NEEDS_CREDENTIALS]: 2, [NEEDS_CLI]: 1, [NEEDS_SKILL]: 0 };
216
211
  const READINESS_REMEDY = {
217
212
  [NEEDS_SKILL]: 'run /agent-workflow-kit setup',
@@ -220,9 +215,8 @@ const READINESS_REMEDY = {
220
215
  [DEGRADED]: 'run /agent-workflow-kit setup (wrapper not on PATH)',
221
216
  };
222
217
 
223
- // recommendRecipe(detection) → { recipe, clause }. Never blank: both ready → Council (Reviewed the
224
- // everyday default); one ready → Reviewed; none installed → Solo + a setup pointer; a backend
225
- // present-but-not-ready → Solo with that backend's specific remedy. Pure.
218
+ // recommendRecipe(detection) → { recipe, clause }, never blank: both ready → Council (Reviewed the
219
+ // everyday default); one → Reviewed; otherwise Solo plus the most actionable remedy. Pure.
226
220
  export const recommendRecipe = (detection) => {
227
221
  const readyReview = readyProvidersOf('review', detection);
228
222
  if (readyReview.length >= 2) {
@@ -231,14 +225,11 @@ export const recommendRecipe = (detection) => {
231
225
  if (readyReview.length === 1) {
232
226
  return { recipe: 'reviewed', clause: `Reviewed available (via ${DISPLAY_ALIASES[readyReview[0]]})` };
233
227
  }
234
- // No ready reviewer Solo. Say how to unlock more: a present-but-not-ready backend names its
235
- // remedy; nothing installed names the setup pointer.
236
- const present = detection.filter((b) => b.readiness !== NEEDS_SKILL && b.readiness !== READY);
228
+ // Role-filtered: the executor vehicle unlocks no review recipe, so it never supplies the remedy.
229
+ const present = providersOf('review', detection).filter((b) => b.readiness !== NEEDS_SKILL && b.readiness !== READY);
237
230
  if (present.length === 0) {
238
231
  return { recipe: 'solo', clause: 'Solo — run /agent-workflow-kit setup to add a backend' };
239
232
  }
240
- // Rank by how close to ready; break ties with the SAME codex-before-agy priority the dispatch path
241
- // uses (priorityIndex) so the recommendation is deterministic regardless of detection emission order.
242
233
  const best = [...present].sort(
243
234
  (a, b) => (READINESS_RANK[b.readiness] ?? -1) - (READINESS_RANK[a.readiness] ?? -1) || priorityIndex(a.name) - priorityIndex(b.name),
244
235
  )[0];
@@ -247,43 +238,20 @@ export const recommendRecipe = (detection) => {
247
238
  };
248
239
 
249
240
  // ── activity procedures: per-slot recipe resolution ────────────────────────────────
250
-
251
- // The named activities and their typed recipe slots the EXECUTABLE mirror of the engine canon
252
- // (agent-workflow-engine/references/procedures.md). Drift-guarded against that canon's `Slots:` lines
253
- // (recipes.test.mjs): the activity ids and each section's slot set must match this table. The slot
254
- // VALUE is the slot's recipe-TYPE (used to look up which recipes are valid for it, SLOT_RECIPES); in
255
- // v1 each slot's key equals its type, but the indirection keeps a future renamed slot expressible.
256
- export const ACTIVITIES = {
257
- 'plan-authoring': { slots: { review: 'review' } },
258
- 'plan-execution': { slots: { execute: 'execute', review: 'review' } },
259
- };
260
-
261
- // Which recipes are valid in each slot type. `review` composes a review DEPTH (Solo / Reviewed /
262
- // Council); `execute` composes Solo / Delegated (delegation is opt-in). A recipe outside its slot's
263
- // list is a config error (the IO shell) or a usage error (an --override) — never silently coerced.
264
- export const SLOT_RECIPES = {
265
- review: ['solo', 'reviewed', 'council'],
266
- execute: ['solo', 'delegated'],
267
- };
268
-
269
- // The computed default for a slot when the config is silent (no file, or no entry for this slot).
270
- // review → Reviewed when ANY review-capable backend is `ready`, else Solo (NEVER Council — Council is
271
- // opt-in; it spends two backends' quota). execute → Solo (Delegated is opt-in only). Readiness-aware,
272
- // so a computed default is always satisfiable and never itself degrades. Deliberately NOT
273
- // recommendRecipe (which returns Council when both are ready — that drives the status line, not a
274
- // per-slot default).
241
+ // The computed default for a silent slot. NEVER Council (opt-in: it spends two backends' quota) and
242
+ // deliberately not recommendRecipe, which drives the status line rather than a per-slot default.
243
+ // Every non-review, non-switch slot floors at Solo, so placing the executor vehicle never flips a
244
+ // default. Readiness-aware, so a computed default is always satisfiable and never itself degrades.
275
245
  const computedDefaultForSlot = (slotType, detection) => {
276
246
  if (slotType === 'review') return readyProvidersOf('review', detection).length >= 1 ? 'reviewed' : 'solo';
277
- return 'solo'; // execute (and any future opt-in slot) floors at Solo
247
+ if (isSwitchSlot(slotType)) return SWITCH_DEFAULT;
248
+ return 'solo';
278
249
  };
279
250
 
280
- // resolveActivityRecipe({ config, readiness, activity, slot, override }) the effective recipe for ONE
281
- // slot of an activity, with graceful-vs-loud degradation. Precedence: an explicit `override` (degrades
282
- // LOUDLY — overrideUnsatisfied, so the agent tells the user) > the `config` entry (degrades gracefully)
283
- // > the computed default (graceful; readiness-aware so it never degrades). Satisfiability + the
284
- // degradation lattice REUSE planRecipe (Council → Reviewed → Solo; Delegated → Solo) — the single source
285
- // of the recipe lattice. `readiness` is the detector array ([{ name, readiness }]). Pure; never mutates.
286
- export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, slot, override } = {}) => {
251
+ // The effective recipe for ONE slot. Precedence: an explicit `override` (degrades LOUDLY, so the
252
+ // agent tells the user) > the `config` entry (graceful) > the computed default. Satisfiability and
253
+ // the lattice REUSE planRecipe one source. Pure; never mutates.
254
+ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, slot, override, surveyLens, postures } = {}) => {
287
255
  const activityDef = ACTIVITIES[activity];
288
256
  if (!activityDef) throw new Error(`unknown activity: ${activity}`);
289
257
  const slotType = activityDef.slots[slot];
@@ -293,12 +261,32 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
293
261
  const requested = override ?? configured ?? computedDefaultForSlot(slotType, readiness);
294
262
  const source = override != null ? 'override' : configured != null ? 'config' : 'default';
295
263
 
296
- // Defensive: the IO shell (config) and CLI (override) validate recipe-for-slot first; a stray value
297
- // here is a programmer error, surfaced loudly rather than silently coerced into a neighbour recipe.
264
+ if (Array.isArray(requested)) {
265
+ if (slotType !== 'review' || source !== 'config') {
266
+ throw new Error(`invalid roster for ${slotType} slot of "${activity}"`);
267
+ }
268
+ const obligations = obligationsOf(requested);
269
+ return {
270
+ recipe: obligations.recipe,
271
+ source,
272
+ degradedFrom: null,
273
+ reason: null,
274
+ overrideUnsatisfied: false,
275
+ roster: resolveRoster({ value: requested, readiness, surveyLens, postures }),
276
+ };
277
+ }
278
+
279
+ // Defensive: the IO shell and the CLI validate first, so a stray value here is a programmer error
280
+ // — surfaced loudly rather than silently coerced into a neighbour recipe.
298
281
  if (!(SLOT_RECIPES[slotType] ?? []).includes(requested)) {
299
282
  throw new Error(`invalid recipe "${requested}" for ${slotType} slot of "${activity}"`);
300
283
  }
301
284
 
285
+ // A switch slot is a flag, not a recipe: it resolves outside the lattice and can never degrade.
286
+ if (isSwitchSlot(slotType)) {
287
+ return { recipe: requested, source, degradedFrom: null, reason: null, overrideUnsatisfied: false };
288
+ }
289
+
302
290
  const plan = planRecipe(requested, readiness);
303
291
  const degraded = plan.degraded;
304
292
  return {
@@ -310,85 +298,43 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
310
298
  };
311
299
  };
312
300
 
313
- // ── the one-line backend status (deterministic-first: the tool speaks, the agent pastes) ───────────
314
-
315
- // composeStatusLine(detection, recommendation) → the ENTIRE one-line backend-status summary the
316
- // bootstrap/upgrade report footers print. Machine-composed so the agent pastes it verbatim and
317
- // composes NOTHING factual (this closes the realistic-example contamination class: a session once
318
- // echoed SKILL.md's canonical example while the detector said otherwise). Display names come from
319
- // DISPLAY_ALIASES — the ONE alias table the recommendation clause already uses; ordering is the
320
- // deterministic BACKEND_PRIORITY (codex before agy), independent of detection emission order.
321
- // Always exactly one line: no part may carry a newline (pinned by tests).
322
- // composeConfiguredPosture(ctx) → the CONFIGURED dispatch posture (strip-the-kit D5), rendered
323
- // from the bundled manifests' VALIDATED posture pins overlaid with the ACTIVE bridge-settings —
324
- // today only the codex tier knob (bridge-settings stays model/effort-free by design, so this
325
- // surface can never carry a settings-file model claim). Returns null when NO bundled bridge
326
- // declares a posture block (a pre-D5 bundle keeps every surface byte-identical). Best-effort:
327
- // a corrupt bundle degrades to null — the posture segment is a footer fact, never a gate.
301
+ // Configured posture from bundled manifest pins plus bridge settings; corruption returns null.
328
302
  export const composeConfiguredPosture = (ctx = {}) => {
329
303
  try {
330
- const bundleRoot = ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
331
- const read = ctx.readFile ?? readFileSync;
332
- const parts = [];
333
- for (const name of BACKEND_PRIORITY) {
334
- let manifest;
335
- try {
336
- manifest = JSON.parse(String(read(join(bundleRoot, name, 'capability.json'), 'utf8')));
337
- } catch {
338
- return null; // an unreadable/corrupt manifest is CORRUPTION — a partial posture would lie
339
- }
340
- if (!Object.hasOwn(manifest, 'posture')) continue; // a pre-D5 bridge — legitimate absence
341
- const posture = manifest.posture;
342
- // The FULL closed shape (the validator/receipt-predicate twin): a present-but-invalid
343
- // block nulls the WHOLE render — fail closed to silence, never a partial/mangled line.
344
- const invalid =
345
- posture === null || typeof posture !== 'object' || Array.isArray(posture) ||
346
- typeof posture.model !== 'string' || posture.model.length === 0 ||
347
- (Object.hasOwn(posture, 'effort') && (typeof posture.effort !== 'string' || posture.effort.length === 0)) ||
348
- (Object.hasOwn(posture, 'tier') && posture.tier !== null && (typeof posture.tier !== 'string' || posture.tier.length === 0)) ||
349
- Object.keys(posture).some((k) => !['model', 'effort', 'tier'].includes(k));
350
- if (invalid) return null;
351
- const seg = [`model=${posture.model}`];
352
- if (Object.hasOwn(posture, 'effort')) seg.push(`effort=${posture.effort}`);
353
- if (Object.hasOwn(posture, 'tier')) {
354
- const knob = (ctx.settings?.active ?? []).find((s) => s.key === 'CODEX_SERVICE_TIER' && s.bridge === name);
355
- seg.push(knob ? `tier=${knob.value} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
356
- }
357
- parts.push(`${DISPLAY_ALIASES[name] ?? name} ${seg.join(' ')}`);
358
- }
304
+ const rows = posturesByBackend({ bundleRoot: ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT, settings: ctx.settings ?? { active: [] }, readFile: ctx.readFile ?? readFileSync });
305
+ if (Object.values(rows).some((row) => row.state === 'unreadable')) return null;
306
+ const parts = Object.entries(rows)
307
+ .filter(([, row]) => row.state === 'valid')
308
+ .map(([receiptId, row]) => `${receiptId} ${row.posture}`);
359
309
  return parts.length ? parts.join(' · ') : null;
360
310
  } catch {
361
311
  return null;
362
312
  }
363
313
  };
364
314
 
315
+ // The bridge half of a readiness array: every backend-status render lists BRIDGES (the things
316
+ // `/agent-workflow-kit backends` sets up); the executor vehicle is a carrier, judged by the agents
317
+ // mode, and only the recipe lattice sees it.
318
+ export const bridgeEntries = (readiness) => readiness.filter((b) => b.name !== EXECUTOR_PROVIDER);
319
+
365
320
  export const composeStatusLine = (detection, recommendation, settings = null, autonomy = null, posture = null) => {
366
- const backends = [...detection]
321
+ const backends = bridgeEntries(detection)
367
322
  .sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
368
323
  .map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`)
369
324
  .join(' · ');
370
325
  const base = `backends: ${backends} — run /agent-workflow-kit backends · recipes: ${recommendation.clause} — see /agent-workflow-kit recipes`;
371
- // Fact-only suffix, ONLY when a bridge knob is actively set (env/file, non-default). Omitted otherwise,
372
- // so the default line is byte-identical to before. A raw env value may (D3) carry newlines/control
373
- // chars — collapse them to a single space so the "exactly one line" backend-status contract holds.
326
+ // A raw env value may carry newlines/control chars collapse them so the one-line contract holds.
374
327
  const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
375
328
  const active = settings?.active ?? [];
376
- // A RETIRED knob is rendered as retired, never as an armed capability (D3 Invariant E): the line
377
- // exists in the user's file, so hiding it would be a silent deletion but reading it as active
378
- // would claim a capability the wrapper no longer has.
329
+ // A RETIRED knob renders as retired: hiding the user's line would be a silent deletion, reading it
330
+ // as active would claim a capability the wrapper no longer has.
379
331
  const suffix = active.length ? ` · settings: ${active.map((s) => `${oneLine(s.key)}=${oneLine(s.value)}${s.retired ? ' (RETIRED — arms nothing)' : ''}`).join(' · ')}` : '';
380
- // The autonomy segment (AD-044 Plan 4): rendered ONLY when the caller supplies the computed
381
- // facts (composeAutonomyFacts) — an omitted param keeps the line byte-identical (the settings-
382
- // suffix precedent). Fact-only: effective per-activity levels + the render-sync state; an absent
383
- // policy says "computed defaults" honestly; a malformed policy surfaces LOUDLY, never omitted.
332
+ // Each optional segment renders ONLY when its facts are supplied; an omitted param changes nothing.
384
333
  const autonomySegment = autonomy == null ? '' : ` · autonomy: ${oneLine(formatAutonomySegment(autonomy))}`;
385
- // The D5 posture segment: rendered ONLY when the caller supplies the composed pins-derived
386
- // posture (composeConfiguredPosture) — an omitted/null param keeps the line byte-identical.
387
334
  const postureSegment = posture == null ? '' : ` · posture: ${oneLine(posture)}`;
388
335
  return base + suffix + autonomySegment + postureSegment;
389
336
  };
390
337
 
391
- // The one-segment autonomy renderer behind composeStatusLine's 4th param.
392
338
  const formatAutonomySegment = (a) => {
393
339
  if (a.error) return `MALFORMED policy — ${a.error}`;
394
340
  const levels = Object.entries(a.activities ?? {}).map(([k, v]) => `${k}=${v.autonomy}`).join(', ');
@@ -400,14 +346,8 @@ const formatAutonomySegment = (a) => {
400
346
  return `${levels} (${state})`;
401
347
  };
402
348
 
403
- // composeAutonomyFacts(cwd) the fact object the status/active lines render (AD-044 Plan 4):
404
- // { source, redlines, activities, renderState } or { error }. Lazy imports — autonomy-config
405
- // statically imports THIS module (ACTIVITIES) and velocity-profile is heavy, so both load at call
406
- // time only (the loadConfig precedent). Never throws: a malformed policy becomes { error } so the
407
- // one-line surfaces render it loudly instead of dying.
408
- // The policy lives at the PROJECT root; the report-footer invokes the paste surfaces without
409
- // --cwd, so a subdirectory shell must still find it. Nearest-.git walk-up (dir or worktree file),
410
- // fs-only — this advisor stays spawn-free; no repo found → the cwd itself (fixture behavior).
349
+ // The policy lives at the PROJECT root and the paste surfaces run without --cwd, so a subdirectory
350
+ // shell must still find it. Fs-only: this advisor stays spawn-free.
411
351
  const projectTopOf = (cwd) => {
412
352
  let dir = resolve(cwd);
413
353
  for (;;) {
@@ -418,9 +358,8 @@ const projectTopOf = (cwd) => {
418
358
  }
419
359
  };
420
360
 
421
- // `deps` is threaded to the render check because that check now PROBES the installed harness: without
422
- // a seam the render state would vary with the machine composing the facts, and a caller could not
423
- // pin it. Production passes nothing and probes for real.
361
+ // The facts the status/active lines render. Lazy imports: autonomy-config statically imports THIS
362
+ // module. Never throws a malformed policy becomes { error }, rendered loudly. `deps` is the probe seam.
424
363
  export const composeAutonomyFacts = async (cwd, deps = {}) => {
425
364
  try {
426
365
  const root = projectTopOf(cwd);
@@ -428,10 +367,8 @@ export const composeAutonomyFacts = async (cwd, deps = {}) => {
428
367
  const { config, source } = loadAutonomy(root);
429
368
  const resolved = resolveAutonomy(config);
430
369
  if (source === 'none') return { source, redlines: resolved.redlines, activities: resolved.activities, renderState: null };
431
- // The STRUCTURAL seed (_README only) is a fresh-deployment NORMAL treating it as "declared"
432
- // would report "render DRIFT" on every fresh upgrade (codex, Segment B). An EXPLICIT policy
433
- // declaring the default values is a real declaration — its render state IS computed
434
- // (structural detection, codex Segment B closing).
370
+ // The structural seed is a fresh-deployment NORMAL: reading it as "declared" would report
371
+ // "render DRIFT" on every fresh upgrade.
435
372
  if (isSparseSeedConfig(config)) {
436
373
  return { source, defaultsEquivalent: true, redlines: resolved.redlines, activities: resolved.activities, renderState: null };
437
374
  }
@@ -452,29 +389,28 @@ export const composeAutonomyFacts = async (cwd, deps = {}) => {
452
389
 
453
390
  // ── the one-line ACTIVE-recipe line (the discovery line — configured, never recommended) ───────────
454
391
 
455
- // composeActiveRecipeLine({ config, source }, detection) → ONE line rendering the CONFIGURED recipe of
456
- // every activity/slot (resolved via resolveActivityRecipe: config entry, else computed default), each
457
- // with its source label, its degradation stated, and its dispatched wrapper set — explicitly labeled
458
- // "configured" and contrasted with the readiness-RECOMMENDED recipe (which composeStatusLine shows and
459
- // which is NOT what runs). This is the machine-composed sibling of composeStatusLine (AD-034): the
460
- // session-start checklist + the handover "Active recipes:" slot paste it verbatim, so no agent composes
461
- // the configured-recipe facts by hand. `{ config, source }` is exactly what loadConfig returns (source
462
- // 'none' when no config file exists). Always exactly one line: no part may carry a newline (pinned).
463
- export const composeActiveRecipeLine = ({ config, source } = {}, detection, autonomy = null) => {
392
+ // ONE line rendering the CONFIGURED recipe of every activity/slot with its source, degradation and
393
+ // dispatched wrappers contrasted with the readiness-RECOMMENDED recipe, which is NOT what runs.
394
+ // The session-start checklist and the handover "Active recipes:" slot paste it verbatim.
395
+ export const composeActiveRecipeLine = ({ config, source } = {}, detection, autonomy = null, rosterDeps = null) => {
464
396
  const cells = [];
465
397
  for (const [activity, def] of Object.entries(ACTIVITIES)) {
466
- // The per-activity autonomy level rides each cell when the caller supplies the facts (AD-044
467
- // Plan 4) — an omitted param keeps the line byte-identical (the composeStatusLine precedent).
468
398
  const level = autonomy?.activities?.[activity]?.autonomy;
469
399
  const auto = level ? `; autonomy ${level}` : '';
470
- for (const slot of Object.keys(def.slots)) {
471
- const r = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity, slot });
472
- const { dispatch } = planRecipe(r.recipe, detection);
400
+ for (const [slot, slotType] of Object.entries(def.slots)) {
401
+ const r = resolveActivityRecipe({
402
+ config: config ?? {}, readiness: detection, activity, slot,
403
+ surveyLens: rosterDeps?.surveyLens, postures: rosterDeps?.postures,
404
+ });
405
+ const dispatch = isSwitchSlot(slotType) || r.roster ? [] : planRecipe(r.recipe, detection).dispatch;
473
406
  const wrappers = dispatch.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
474
407
  const srcLabel = r.source === 'config' ? 'configured' : 'computed default';
408
+ const renderedValue = r.roster
409
+ ? activeLineCell(r.roster, { states: rosterDeps !== null })
410
+ : r.recipe;
475
411
  const head = r.degradedFrom
476
412
  ? `${activity}.${slot} = ${r.degradedFrom} (${srcLabel}; degrades here to ${r.recipe} — ${r.reason}${auto})`
477
- : `${activity}.${slot} = ${r.recipe} (${srcLabel}${auto})`;
413
+ : `${activity}.${slot} = ${renderedValue} (${srcLabel}${isSwitchSlot(slotType) ? '; switch' : ''}${auto})`;
478
414
  const suffix =
479
415
  wrappers.length >= 2
480
416
  ? ` → every backend every round: ${wrappers.join(' + ')}`
@@ -486,18 +422,16 @@ export const composeActiveRecipeLine = ({ config, source } = {}, detection, auto
486
422
  }
487
423
  const rec = recommendRecipe(detection);
488
424
  const origin = source === 'none' || config == null ? 'no config file — computed defaults apply' : `from ${source}`;
489
- // A MALFORMED policy must surface LOUDLY on this paste surface too silently rendering cells
490
- // without levels would hide the required STOP signal (Segment B). One line always.
425
+ // A MALFORMED policy surfaces LOUDLY here too: rendering cells without levels would hide the STOP.
491
426
  const malformed = autonomy?.error
492
427
  ? ` · autonomy: MALFORMED policy — ${String(autonomy.error).replace(/[\s]+/g, ' ').trim()}`
493
428
  : '';
494
- return `active recipes (${origin}): ${cells.join(' · ')} — the configured recipes above are what runs; readiness-recommended here: ${rec.recipe} (informational)${malformed}`;
429
+ return `active recipes (${origin}): ${cells.join(' · ')} — the configured orchestration values above are what runs; readiness-recommended here: ${rec.recipe} (informational)${malformed}`;
495
430
  };
496
431
 
497
432
  // ── report + CLI ─────────────────────────────────────────────────────────────────
498
433
 
499
- // The structured report behind `--json` the recipes, the recommendation, a plan per recipe, and
500
- // (additive) the pasteable one-line backend status composed from the same detection.
434
+ // The structured report behind `--json`, incl. the same one-line status the --status-line mode emits.
501
435
  export const buildReport = (detection, settings = null, autonomy = null, posture = null) => {
502
436
  const recommendation = recommendRecipe(detection);
503
437
  return {
@@ -511,17 +445,15 @@ export const buildReport = (detection, settings = null, autonomy = null, posture
511
445
  })),
512
446
  recommendation,
513
447
  plans: RECIPES.map((r) => planRecipe(r.id, detection)),
514
- // The SAME autonomy facts the --status-line surface renders the --json envelope must never
515
- // expose a stale machine-composed status line (Segment B).
448
+ // The --json envelope must never expose a status line staler than the --status-line surface.
516
449
  statusLine: composeStatusLine(detection, recommendation, settings, autonomy, posture),
517
450
  };
518
451
  };
519
452
 
520
- // formatRecipes(detection) → deterministic human advisor text: the four recipes, the recommendation,
521
- // and the per-recipe plan for the current environment (degradation reasons + dispatch + notes).
453
+ // Deterministic human advisor text: the recipes, the recommendation, and the per-recipe plan here.
522
454
  export const formatRecipes = (detection) => {
523
455
  const lines = [
524
- 'agent-workflow orchestration recipes (read-only — the orchestrator executes via the bridge skills and always commits)',
456
+ 'agent-workflow orchestration recipes (read-only — the orchestrator executes via the bridge skills or the executor vehicle and always commits)',
525
457
  '',
526
458
  ];
527
459
  for (const r of RECIPES) lines.push(` ${r.title} (${r.id}) — ${r.summary}`);
@@ -530,7 +462,9 @@ export const formatRecipes = (detection) => {
530
462
  for (const r of RECIPES) {
531
463
  const p = planRecipe(r.id, detection);
532
464
  const arrow = p.degraded ? ` → ${p.effective}` : '';
533
- const who = p.dispatch.length ? p.dispatch.map((d) => `${d.display} ${d.role}`).join(', ') : 'orchestrator only';
465
+ const who = p.dispatch.length
466
+ ? p.dispatch.map((d) => `${d.display} ${d.role}${d.vehicle ? ` (vehicle ${d.vehicle})` : ''}`).join(', ')
467
+ : 'orchestrator only';
534
468
  lines.push(` ${r.title}${arrow}: ${who}`);
535
469
  for (const step of p.degradation) lines.push(` ↳ ${step.reason}`);
536
470
  for (const note of p.notes) lines.push(` • ${note}`);
@@ -538,22 +472,37 @@ export const formatRecipes = (detection) => {
538
472
  return lines.join('\n');
539
473
  };
540
474
 
541
- // The full argv vocabulary anything else rejects LOUDLY. The old parse silently routed unknown
542
- // args into the multi-line human render; with `--status-line` / `--active-line` (whose output is
543
- // pasted as fact) a mistyped flag masquerading as a mode would be a silent failure, so the parse is
544
- // strict now.
475
+ // Closed argv vocabulary: the mode outputs are pasted as fact, so a mistyped flag masquerading as a
476
+ // mode would be a silent failure.
545
477
  const KNOWN_ARGS = new Set(['--help', '-h', '--json', '--status-line', '--active-line']);
546
478
  const EXCLUSIVE_ARGS = ['--json', '--status-line', '--active-line']; // each owns stdout whole
547
479
 
548
- const main = async (argv) => {
480
+ // The readiness array EVERY mode composes: the detected backends plus the executor vehicle as the
481
+ // one carry provider. Two independent axes: the vehicle is surveyed first, and a bridge detector
482
+ // failure reaches `deps.onDetectError` exactly once (default: a loud stderr line) while the
483
+ // vehicle's readiness survives it — so a caller's fail-closed state for bridges never masks the
484
+ // carrier, and the reverse. `deps` is also the seam a test injects a fake survey or detector through.
485
+ export const composeReadiness = (cwd, deps = {}) => {
486
+ const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(cwd, deps);
487
+ let detection = [];
488
+ try {
489
+ detection = (deps.detect ?? detectBackends)();
490
+ } catch (err) {
491
+ (deps.onDetectError ?? ((e) => console.error(`[agent-workflow-kit] backend detection failed: ${e.message}`)))(err);
492
+ }
493
+ return withVehicle(detection, survey);
494
+ };
495
+
496
+ const main = async (argv, deps = {}) => {
549
497
  if (argv.includes('--help') || argv.includes('-h')) {
550
498
  console.log(`recipes — read-only orchestration-recipe advisor for the agent-workflow family.
551
499
 
552
500
  Usage:
553
501
  node recipes.mjs [--json | --status-line | --active-line]
554
502
 
555
- Lists the four recipes (Solo / Reviewed / Council / Delegated) and, from the read-only backend
556
- detector, plans + recommends one for the current environment. --status-line prints exactly ONE
503
+ Lists the five recipes (Solo / Reviewed / Council / Delegated / Subagent) and, from the read-only
504
+ backend detector plus the executor-vehicle survey, plans + recommends one for the current
505
+ environment. --status-line prints exactly ONE
557
506
  line — the machine-composed backend-status summary the bootstrap/upgrade reports paste verbatim
558
507
  (incl. the per-activity autonomy segment: effective levels + render-sync state, honest
559
508
  computed-defaults wording when no policy file exists). --active-line prints exactly ONE line — the
@@ -574,31 +523,35 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
574
523
  console.error(`[agent-workflow-kit] ${exclusive.join(' and ')} are mutually exclusive — pick one output`);
575
524
  return 1;
576
525
  }
577
- const detection = detectBackends();
526
+ const cwd = process.cwd();
527
+ const readiness = composeReadiness(cwd, deps);
578
528
  if (argv.includes('--active-line')) {
579
- // Lazy import: orchestration-config.mjs statically imports this module (ACTIVITIES/SLOT_RECIPES),
580
- // so the config reader is pulled in at run time only — no static import cycle.
529
+ // Lazy: orchestration-config.mjs statically imports this module — no static cycle.
581
530
  const { loadConfig } = await import('./orchestration-config.mjs');
582
531
  try {
583
- console.log(composeActiveRecipeLine(loadConfig(process.cwd()), detection, await composeAutonomyFacts(process.cwd())));
532
+ const snapshot = settingsSnapshot();
533
+ const surveyLens = deps.surveyLens ?? ((spec) => surveyVehicle(cwd, spec, deps));
534
+ console.log(composeActiveRecipeLine(
535
+ loadConfig(cwd), readiness, await composeAutonomyFacts(cwd),
536
+ { surveyLens, postures: posturesByBackend({ settings: snapshot }) },
537
+ ));
584
538
  } catch (err) {
585
539
  console.error(`[agent-workflow-kit] ${err.message}`);
586
540
  return err.exitCode ?? 1;
587
541
  }
588
542
  } else if (argv.includes('--status-line')) {
589
543
  const snapshot = settingsSnapshot();
590
- console.log(composeStatusLine(detection, recommendRecipe(detection), snapshot, await composeAutonomyFacts(process.cwd()), composeConfiguredPosture({ settings: snapshot })));
544
+ console.log(composeStatusLine(readiness, recommendRecipe(readiness), snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })));
591
545
  } else if (argv.includes('--json')) {
592
546
  const snapshot = settingsSnapshot();
593
- console.log(JSON.stringify(buildReport(detection, snapshot, await composeAutonomyFacts(process.cwd()), composeConfiguredPosture({ settings: snapshot })), null, 2));
547
+ console.log(JSON.stringify(buildReport(readiness, snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })), null, 2));
594
548
  }
595
- else console.log(formatRecipes(detection));
549
+ else console.log(formatRecipes(readiness));
596
550
  return 0;
597
551
  };
598
552
 
599
- // Natural exit via process.exitCode — never process.exit inside the async main (it would drop buffered
600
- // stdio writes on piped stderr), and never a TOP-LEVEL await here: orchestration-config.mjs statically
601
- // imports this module, so awaiting the dynamic import during our own evaluation would deadlock the cycle.
553
+ // Natural exit via process.exitCode — never process.exit inside the async main (it would drop
554
+ // buffered stdio on piped stderr), and never a TOP-LEVEL await (that would deadlock the import cycle).
602
555
  if (isDirectRun(import.meta.url)) {
603
556
  main(process.argv.slice(2)).then(
604
557
  (code) => {