@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.
- package/CHANGELOG.md +85 -0
- package/README.md +5 -5
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
- package/bridges/antigravity-cli-bridge/capability.json +2 -2
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
- package/bridges/codex-cli-bridge/capability.json +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/executor.md +40 -0
- package/references/agents/review-lens.md +5 -3
- package/references/modes/agents.md +9 -4
- package/references/modes/procedures.md +21 -8
- package/references/modes/recipes.md +7 -4
- package/references/modes/recommendations.md +3 -1
- package/references/modes/set-recipe.md +23 -6
- package/references/modes/status.md +2 -2
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/references/shared/composition-handoff.md +1 -1
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/orchestration.json +1 -1
- package/tools/autonomy-config.mjs +1 -1
- package/tools/bridge-posture.mjs +48 -0
- package/tools/carriers.mjs +152 -0
- package/tools/cheap-agents-read.mjs +234 -0
- package/tools/cheap-agents.mjs +101 -109
- package/tools/commands.mjs +3 -3
- package/tools/detect-backends.mjs +2 -2
- package/tools/direct-run.mjs +9 -0
- package/tools/family-registry.mjs +38 -18
- package/tools/flow-check.mjs +2 -7
- package/tools/fold-scope.mjs +5 -60
- package/tools/grounding.mjs +2 -2
- package/tools/inject-methodology.mjs +4 -0
- package/tools/orchestration-config.mjs +23 -61
- package/tools/orchestration-readme.mjs +70 -0
- package/tools/plan-shape-cli.mjs +112 -0
- package/tools/plan-shape-facts.mjs +204 -0
- package/tools/plan-shape.mjs +348 -0
- package/tools/procedures.mjs +197 -83
- package/tools/recipes.mjs +183 -230
- package/tools/recommendations.mjs +77 -11
- package/tools/renderers.mjs +27 -7
- package/tools/repo-lex.mjs +40 -0
- package/tools/review-roster-resolve.mjs +104 -0
- package/tools/review-roster.mjs +128 -0
- package/tools/review-rounds-cli.mjs +92 -0
- package/tools/review-rounds.mjs +115 -0
- package/tools/review-state.mjs +10 -11
- package/tools/set-recipe-roster.mjs +167 -0
- package/tools/set-recipe.mjs +138 -42
- package/tools/velocity-profile.mjs +8 -22
- package/tools/view-model.mjs +17 -3
package/tools/recipes.mjs
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
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
|
-
|
|
34
|
-
const AGY = 'antigravity-cli-bridge';
|
|
39
|
+
export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APPLY, safeLine, DISPLAY_ALIASES };
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
export const DISPLAY_ALIASES = { [CODEX]: 'codex', [AGY]: 'agy' };
|
|
41
|
+
const [CODEX, AGY] = BACKEND_PRIORITY;
|
|
38
42
|
|
|
39
|
-
//
|
|
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
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
//
|
|
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
|
|
89
|
-
//
|
|
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
|
-
//
|
|
143
|
-
const
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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}
|
|
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
|
|
169
|
-
//
|
|
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
|
|
166
|
+
return readyProviderEntriesOf(recipe.role, detection)
|
|
173
167
|
.slice(0, recipe.minBackends)
|
|
174
|
-
.map((
|
|
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
|
-
//
|
|
181
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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 }
|
|
224
|
-
// everyday default); one
|
|
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
|
-
//
|
|
235
|
-
|
|
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
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
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
|
-
|
|
247
|
+
if (isSwitchSlot(slotType)) return SWITCH_DEFAULT;
|
|
248
|
+
return 'solo';
|
|
278
249
|
};
|
|
279
250
|
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
//
|
|
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
|
|
331
|
-
|
|
332
|
-
const parts =
|
|
333
|
-
|
|
334
|
-
|
|
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 =
|
|
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
|
-
//
|
|
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
|
|
377
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
404
|
-
//
|
|
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
|
-
//
|
|
422
|
-
//
|
|
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
|
|
432
|
-
//
|
|
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
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
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.
|
|
471
|
-
const r = resolveActivityRecipe({
|
|
472
|
-
|
|
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} = ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
542
|
-
//
|
|
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
|
-
|
|
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
|
|
556
|
-
detector, plans + recommends one for the current
|
|
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
|
|
526
|
+
const cwd = process.cwd();
|
|
527
|
+
const readiness = composeReadiness(cwd, deps);
|
|
578
528
|
if (argv.includes('--active-line')) {
|
|
579
|
-
// Lazy
|
|
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
|
-
|
|
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(
|
|
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(
|
|
547
|
+
console.log(JSON.stringify(buildReport(readiness, snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })), null, 2));
|
|
594
548
|
}
|
|
595
|
-
else console.log(formatRecipes(
|
|
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
|
|
600
|
-
// stdio
|
|
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) => {
|