@sabaiway/agent-workflow-kit 10.2.0 → 10.4.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 +72 -0
- package/README.md +7 -7
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/executor.md +40 -0
- package/references/modes/agents.md +9 -4
- package/references/modes/procedures.md +17 -8
- package/references/modes/recipes.md +7 -4
- package/references/modes/recommendations.md +4 -1
- package/references/modes/set-recipe.md +22 -5
- package/references/modes/status.md +3 -3
- package/references/modes/upgrade.md +7 -5
- package/references/shared/composition-handoff.md +1 -1
- package/references/shared/deploy-tail.md +2 -2
- package/references/templates/agent_rules.md +3 -2
- package/references/templates/orchestration.json +1 -1
- package/tools/ack-store.mjs +57 -0
- package/tools/ack-write.mjs +1 -1
- package/tools/autonomy-config.mjs +1 -1
- package/tools/carriers.mjs +140 -0
- package/tools/cheap-agents-read.mjs +172 -0
- package/tools/cheap-agents.mjs +57 -105
- package/tools/commands.mjs +3 -3
- package/tools/direct-run.mjs +6 -0
- package/tools/doc-parity.mjs +8 -0
- package/tools/ensure-ops.mjs +18 -9
- package/tools/ensure-specs.mjs +3 -4
- package/tools/ensure-vocabulary.mjs +5 -2
- package/tools/family-registry.mjs +70 -21
- package/tools/flow-check.mjs +2 -7
- package/tools/inject-methodology.mjs +4 -0
- package/tools/lens-region.mjs +4 -1
- package/tools/node-evidence.mjs +77 -0
- package/tools/orchestration-config.mjs +34 -13
- package/tools/procedures.mjs +65 -52
- package/tools/recipes.mjs +156 -184
- package/tools/recommendations.mjs +145 -78
- package/tools/renderers.mjs +36 -7
- package/tools/review-state.mjs +10 -11
- package/tools/set-recipe.mjs +63 -24
- package/tools/spec-adoption.mjs +71 -0
- package/tools/spec-check.mjs +2 -2
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/view-model.mjs +19 -3
package/tools/recipes.mjs
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Recipe planner — the pure brain behind the read-only `/agent-workflow-kit recipes` advisor.
|
|
3
3
|
//
|
|
4
|
-
// A "recipe" is a NAMED orchestration pattern over the family's optional
|
|
5
|
-
// subscription-CLI bridges
|
|
6
|
-
//
|
|
7
|
-
// (agent-workflow-engine/references/orchestration.md
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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.
|
|
4
|
+
// A "recipe" is a NAMED orchestration pattern over the family's optional carriers: the
|
|
5
|
+
// subscription-CLI bridges (codex-cli-bridge → `codex`, antigravity-cli-bridge → `agy`) and the
|
|
6
|
+
// executor subagent vehicle. The ENGINE owns the canonical narrative
|
|
7
|
+
// (agent-workflow-engine/references/orchestration.md, kept in lockstep by the recipe-name parity
|
|
8
|
+
// guard in recipes.test.mjs); this module owns the EXECUTABLE dispatch. The activity/slot registry
|
|
9
|
+
// lives in carriers.mjs, re-exported here so every importer keeps its './recipes.mjs' path.
|
|
10
|
+
// Invariants: pure (no fs/network/CLI in planRecipe/recommendRecipe), read-only, NEVER runs a
|
|
11
|
+
// subscription CLI. The orchestrator executes a recipe and always makes the single commit; a
|
|
12
|
+
// backend or a subagent is advisory or delegated, never autonomous. Dependency-free, Node >= 22.
|
|
16
13
|
|
|
17
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
18
15
|
import { dirname, join, resolve } from 'node:path';
|
|
19
|
-
// The
|
|
20
|
-
// writer — so this read-only advisor never pulls in the atomic-write core.
|
|
16
|
+
// The READ-ONLY settings core, never the writer: this advisor never pulls in the atomic-write core.
|
|
21
17
|
import { settingsSnapshot, DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
22
18
|
import { isDirectRun } from './direct-run.mjs';
|
|
19
|
+
import {
|
|
20
|
+
ACTIVITIES,
|
|
21
|
+
POLICY_ACTIVITIES,
|
|
22
|
+
SLOT_RECIPES,
|
|
23
|
+
SUBAGENT_RECIPE,
|
|
24
|
+
CARRY_ROLE,
|
|
25
|
+
EXECUTOR_PROVIDER,
|
|
26
|
+
SWITCH_DEFAULT,
|
|
27
|
+
isSwitchSlot,
|
|
28
|
+
withVehicle,
|
|
29
|
+
vehicleDegradeReason,
|
|
30
|
+
EXECUTOR_APPLY,
|
|
31
|
+
safeLine,
|
|
32
|
+
} from './carriers.mjs';
|
|
33
|
+
// The READ core of the vehicle surface, never the writer: this advisor can never reach a module
|
|
34
|
+
// that creates `.claude/agents/`.
|
|
35
|
+
import { surveyExecutorVehicle } from './cheap-agents-read.mjs';
|
|
23
36
|
import {
|
|
24
37
|
detectBackends,
|
|
25
38
|
wrapperCmdFor,
|
|
@@ -30,25 +43,25 @@ import {
|
|
|
30
43
|
DEGRADED,
|
|
31
44
|
} from './detect-backends.mjs';
|
|
32
45
|
|
|
46
|
+
export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APPLY, safeLine };
|
|
47
|
+
|
|
33
48
|
const CODEX = 'codex-cli-bridge';
|
|
34
49
|
const AGY = 'antigravity-cli-bridge';
|
|
35
50
|
|
|
36
51
|
// The manifest-name → human-alias map (the detector emits manifest names; humans say codex/agy).
|
|
37
52
|
export const DISPLAY_ALIASES = { [CODEX]: 'codex', [AGY]: 'agy' };
|
|
38
53
|
|
|
39
|
-
// The
|
|
40
|
-
//
|
|
54
|
+
// The provider → role table, keyed by the name the readiness array carries, NOT the display alias.
|
|
55
|
+
// The bridge rows are drift-guarded against each capability.json `provides[]`; the executor row is
|
|
56
|
+
// the vehicle withVehicle appends, and the ONLY provider of `carry`.
|
|
41
57
|
export const BACKEND_ROLES = {
|
|
42
58
|
[CODEX]: ['execute', 'review'],
|
|
43
59
|
[AGY]: ['review', 'probe'],
|
|
60
|
+
[EXECUTOR_PROVIDER]: [CARRY_ROLE],
|
|
44
61
|
};
|
|
45
62
|
|
|
46
|
-
// Review obligations from the CONFIGURED plan-execution.review recipe — the RAW config value,
|
|
47
|
-
//
|
|
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).
|
|
63
|
+
// Review obligations from the CONFIGURED plan-execution.review recipe — the RAW config value, never
|
|
64
|
+
// the readiness-degraded one. Homed HERE so review-state and flow-check share one backend set (#42).
|
|
52
65
|
export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false } = {}) => {
|
|
53
66
|
const configured = config?.['plan-execution']?.review;
|
|
54
67
|
const providers = Object.values(DISPLAY_ALIASES); // every review-capable backend, codex first
|
|
@@ -56,7 +69,9 @@ export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], de
|
|
|
56
69
|
// No config + no readiness signal: the computed default is UNKNOWABLE — fail closed upstream.
|
|
57
70
|
return { recipe: null, source: 'default', backends: [], minShip: 0, perBackend: false, unknowable: true };
|
|
58
71
|
}
|
|
59
|
-
|
|
72
|
+
// Role-filtered: a ready EXECUTOR vehicle is a carry provider, never a reviewer, so it must not
|
|
73
|
+
// turn a silent review config into `reviewed`.
|
|
74
|
+
const anyReady = readyProvidersOf('review', readiness).length >= 1;
|
|
60
75
|
const recipe = configured ?? (anyReady ? 'reviewed' : 'solo');
|
|
61
76
|
const source = configured != null ? 'config' : 'default';
|
|
62
77
|
if (recipe === 'solo') return { recipe, source, backends: [], minShip: 0, perBackend: false, unknowable: false };
|
|
@@ -64,10 +79,8 @@ export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], de
|
|
|
64
79
|
return { recipe, source, backends: providers, minShip: 1, perBackend: false, unknowable: false };
|
|
65
80
|
};
|
|
66
81
|
|
|
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).
|
|
82
|
+
// Advisory metadata the detection object does not carry. cost/quota are drift-guarded against the
|
|
83
|
+
// manifests; agy's `health` (Issue-001) is invisible to detection — a caveat, not a readiness signal.
|
|
71
84
|
export const BACKEND_META = {
|
|
72
85
|
[CODEX]: { cost: 'subscription', quota: { kind: 'subscription', finite: true } },
|
|
73
86
|
[AGY]: {
|
|
@@ -77,17 +90,15 @@ export const BACKEND_META = {
|
|
|
77
90
|
},
|
|
78
91
|
};
|
|
79
92
|
|
|
80
|
-
// Deterministic tie-break
|
|
81
|
-
// the more reliable default for substantive reviews (agy carries the standing service-stall caveat above).
|
|
93
|
+
// Deterministic tie-break: codex before agy — the more reliable default for substantive reviews.
|
|
82
94
|
const BACKEND_PRIORITY = [CODEX, AGY];
|
|
83
95
|
const priorityIndex = (name) => {
|
|
84
96
|
const i = BACKEND_PRIORITY.indexOf(name);
|
|
85
97
|
return i === -1 ? BACKEND_PRIORITY.length : i;
|
|
86
98
|
};
|
|
87
99
|
|
|
88
|
-
// The
|
|
89
|
-
//
|
|
90
|
-
// recipe when it can't be satisfied (the chain terminates at Solo, which is always satisfiable).
|
|
100
|
+
// The five recipes, in lattice order. `degradesTo` is the next-weaker recipe when a recipe can't be
|
|
101
|
+
// satisfied; every chain terminates at Solo, which always is.
|
|
91
102
|
export const RECIPES = [
|
|
92
103
|
{
|
|
93
104
|
id: 'solo',
|
|
@@ -121,12 +132,12 @@ export const RECIPES = [
|
|
|
121
132
|
degradesTo: 'solo',
|
|
122
133
|
summary: 'the orchestrator hands a bounded execution sub-task to a backend (codex exec), then reviews the diff and commits.',
|
|
123
134
|
},
|
|
135
|
+
SUBAGENT_RECIPE,
|
|
124
136
|
];
|
|
125
137
|
|
|
126
138
|
const recipeById = (id) => RECIPES.find((r) => r.id === id);
|
|
127
139
|
|
|
128
|
-
//
|
|
129
|
-
// about whether the backend's service is responsive).
|
|
140
|
+
// Read-only file-presence remedies — never a claim about whether a backend's service is responsive.
|
|
130
141
|
const READINESS_REASON = {
|
|
131
142
|
[NEEDS_SKILL]: 'bridge skill not installed — run /agent-workflow-kit setup',
|
|
132
143
|
[NEEDS_CLI]: 'the CLI is not installed',
|
|
@@ -136,18 +147,16 @@ const READINESS_REASON = {
|
|
|
136
147
|
|
|
137
148
|
// ── pure planner ───────────────────────────────────────────────────────────────
|
|
138
149
|
|
|
139
|
-
// Backends (ready or not) whose role table includes `role`.
|
|
140
150
|
const providersOf = (role, detection) => detection.filter((b) => (BACKEND_ROLES[b.name] ?? []).includes(role));
|
|
141
151
|
|
|
142
|
-
//
|
|
143
|
-
const
|
|
152
|
+
// In deterministic priority order, so a dispatch never depends on detection emission order.
|
|
153
|
+
const readyProviderEntriesOf = (role, detection) =>
|
|
144
154
|
providersOf(role, detection)
|
|
145
155
|
.filter((b) => b.readiness === READY)
|
|
146
|
-
.sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
|
|
147
|
-
|
|
156
|
+
.sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name));
|
|
157
|
+
|
|
158
|
+
const readyProvidersOf = (role, detection) => readyProviderEntriesOf(role, detection).map((b) => b.name);
|
|
148
159
|
|
|
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
160
|
const isSatisfiable = (recipe, detection) =>
|
|
152
161
|
recipe.role === null || readyProvidersOf(recipe.role, detection).length >= recipe.minBackends;
|
|
153
162
|
|
|
@@ -155,41 +164,44 @@ const isSatisfiable = (recipe, detection) =>
|
|
|
155
164
|
const degradeReason = (recipe, detection) => {
|
|
156
165
|
const providers = providersOf(recipe.role, detection);
|
|
157
166
|
if (providers.length === 0) {
|
|
158
|
-
return `${recipe.title} needs a
|
|
167
|
+
return `${recipe.title} needs a provider providing ${recipe.role}, but no provider provides it`;
|
|
159
168
|
}
|
|
160
169
|
const ready = providers.filter((b) => b.readiness === READY);
|
|
161
170
|
const detail = providers
|
|
162
171
|
.filter((b) => b.readiness !== READY)
|
|
163
|
-
.map((b) =>
|
|
172
|
+
.map((b) => (b.name === EXECUTOR_PROVIDER
|
|
173
|
+
? vehicleDegradeReason(b.vehicle, EXECUTOR_APPLY)
|
|
174
|
+
: `${DISPLAY_ALIASES[b.name] ?? b.name}: ${READINESS_REASON[b.readiness] ?? b.readiness}`))
|
|
164
175
|
.join('; ');
|
|
165
|
-
return `${recipe.title} needs ${recipe.minBackends}
|
|
176
|
+
return `${recipe.title} needs ${recipe.minBackends} provider(s) providing ${recipe.role}, but only ${ready.length} ready${detail ? ` — ${detail}` : ''}`;
|
|
166
177
|
};
|
|
167
178
|
|
|
168
|
-
// Per-stage dispatch for an EFFECTIVE (already-satisfiable) recipe
|
|
169
|
-
//
|
|
179
|
+
// Per-stage dispatch for an EFFECTIVE (already-satisfiable) recipe. The executor's step carries its
|
|
180
|
+
// vehicle state, so both renders can name it.
|
|
170
181
|
const dispatchFor = (recipe, detection) => {
|
|
171
182
|
if (recipe.role === null) return [];
|
|
172
|
-
return
|
|
183
|
+
return readyProviderEntriesOf(recipe.role, detection)
|
|
173
184
|
.slice(0, recipe.minBackends)
|
|
174
|
-
.map((
|
|
185
|
+
.map((b) => {
|
|
186
|
+
const step = { role: recipe.role, backend: b.name, display: DISPLAY_ALIASES[b.name] ?? b.name };
|
|
187
|
+
return b.vehicle ? { ...step, vehicle: b.vehicle.state } : step;
|
|
188
|
+
});
|
|
175
189
|
};
|
|
176
190
|
|
|
177
191
|
const QUOTA_NOTE = "Prefer the cheapest model that fits the task; don't reach for a top-tier model by reflex.";
|
|
178
192
|
const COUNCIL_QUOTA_NOTE = "Council spends two backends' quota for one decision — reserve it for changes that justify the cost.";
|
|
179
193
|
|
|
180
|
-
//
|
|
181
|
-
//
|
|
194
|
+
// The quota reminder rides a subscription-BACKEND dispatch only: the executor vehicle spends no
|
|
195
|
+
// bridge quota, and its recipe is a frontier one, so the cheapest-model reminder would contradict it.
|
|
182
196
|
const notesFor = (recipe, dispatch) => {
|
|
183
197
|
const notes = [];
|
|
184
|
-
if (dispatch.
|
|
198
|
+
if (dispatch.some((d) => d.role !== CARRY_ROLE)) notes.push(QUOTA_NOTE);
|
|
185
199
|
if (recipe.id === 'council') notes.push(COUNCIL_QUOTA_NOTE);
|
|
186
200
|
if (dispatch.some((d) => d.backend === AGY) && BACKEND_META[AGY].health) notes.push(BACKEND_META[AGY].health);
|
|
187
201
|
return notes;
|
|
188
202
|
};
|
|
189
203
|
|
|
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.
|
|
204
|
+
// planRecipe → pure plan: walks the degradation chain, a stated reason per step, then dispatches.
|
|
193
205
|
export const planRecipe = (recipe, detection) => {
|
|
194
206
|
const requested = typeof recipe === 'string' ? recipeById(recipe) : recipe;
|
|
195
207
|
if (!requested) throw new Error(`unknown recipe: ${recipe}`);
|
|
@@ -211,7 +223,7 @@ export const planRecipe = (recipe, detection) => {
|
|
|
211
223
|
};
|
|
212
224
|
};
|
|
213
225
|
|
|
214
|
-
//
|
|
226
|
+
// Rank = how close to ready, so the most actionable remedy surfaces first.
|
|
215
227
|
const READINESS_RANK = { [DEGRADED]: 3, [NEEDS_CREDENTIALS]: 2, [NEEDS_CLI]: 1, [NEEDS_SKILL]: 0 };
|
|
216
228
|
const READINESS_REMEDY = {
|
|
217
229
|
[NEEDS_SKILL]: 'run /agent-workflow-kit setup',
|
|
@@ -220,9 +232,8 @@ const READINESS_REMEDY = {
|
|
|
220
232
|
[DEGRADED]: 'run /agent-workflow-kit setup (wrapper not on PATH)',
|
|
221
233
|
};
|
|
222
234
|
|
|
223
|
-
// recommendRecipe(detection) → { recipe, clause }
|
|
224
|
-
// everyday default); one
|
|
225
|
-
// present-but-not-ready → Solo with that backend's specific remedy. Pure.
|
|
235
|
+
// recommendRecipe(detection) → { recipe, clause }, never blank: both ready → Council (Reviewed the
|
|
236
|
+
// everyday default); one → Reviewed; otherwise Solo plus the most actionable remedy. Pure.
|
|
226
237
|
export const recommendRecipe = (detection) => {
|
|
227
238
|
const readyReview = readyProvidersOf('review', detection);
|
|
228
239
|
if (readyReview.length >= 2) {
|
|
@@ -231,14 +242,11 @@ export const recommendRecipe = (detection) => {
|
|
|
231
242
|
if (readyReview.length === 1) {
|
|
232
243
|
return { recipe: 'reviewed', clause: `Reviewed available (via ${DISPLAY_ALIASES[readyReview[0]]})` };
|
|
233
244
|
}
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
const present = detection.filter((b) => b.readiness !== NEEDS_SKILL && b.readiness !== READY);
|
|
245
|
+
// Role-filtered: the executor vehicle unlocks no review recipe, so it never supplies the remedy.
|
|
246
|
+
const present = providersOf('review', detection).filter((b) => b.readiness !== NEEDS_SKILL && b.readiness !== READY);
|
|
237
247
|
if (present.length === 0) {
|
|
238
248
|
return { recipe: 'solo', clause: 'Solo — run /agent-workflow-kit setup to add a backend' };
|
|
239
249
|
}
|
|
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
250
|
const best = [...present].sort(
|
|
243
251
|
(a, b) => (READINESS_RANK[b.readiness] ?? -1) - (READINESS_RANK[a.readiness] ?? -1) || priorityIndex(a.name) - priorityIndex(b.name),
|
|
244
252
|
)[0];
|
|
@@ -247,42 +255,19 @@ export const recommendRecipe = (detection) => {
|
|
|
247
255
|
};
|
|
248
256
|
|
|
249
257
|
// ── 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).
|
|
258
|
+
// The computed default for a silent slot. NEVER Council (opt-in: it spends two backends' quota) and
|
|
259
|
+
// deliberately not recommendRecipe, which drives the status line rather than a per-slot default.
|
|
260
|
+
// Every non-review, non-switch slot floors at Solo, so placing the executor vehicle never flips a
|
|
261
|
+
// default. Readiness-aware, so a computed default is always satisfiable and never itself degrades.
|
|
275
262
|
const computedDefaultForSlot = (slotType, detection) => {
|
|
276
263
|
if (slotType === 'review') return readyProvidersOf('review', detection).length >= 1 ? 'reviewed' : 'solo';
|
|
277
|
-
|
|
264
|
+
if (isSwitchSlot(slotType)) return SWITCH_DEFAULT;
|
|
265
|
+
return 'solo';
|
|
278
266
|
};
|
|
279
267
|
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
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.
|
|
268
|
+
// The effective recipe for ONE slot. Precedence: an explicit `override` (degrades LOUDLY, so the
|
|
269
|
+
// agent tells the user) > the `config` entry (graceful) > the computed default. Satisfiability and
|
|
270
|
+
// the lattice REUSE planRecipe — one source. Pure; never mutates.
|
|
286
271
|
export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, slot, override } = {}) => {
|
|
287
272
|
const activityDef = ACTIVITIES[activity];
|
|
288
273
|
if (!activityDef) throw new Error(`unknown activity: ${activity}`);
|
|
@@ -293,12 +278,17 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
293
278
|
const requested = override ?? configured ?? computedDefaultForSlot(slotType, readiness);
|
|
294
279
|
const source = override != null ? 'override' : configured != null ? 'config' : 'default';
|
|
295
280
|
|
|
296
|
-
// Defensive: the IO shell
|
|
297
|
-
//
|
|
281
|
+
// Defensive: the IO shell and the CLI validate first, so a stray value here is a programmer error
|
|
282
|
+
// — surfaced loudly rather than silently coerced into a neighbour recipe.
|
|
298
283
|
if (!(SLOT_RECIPES[slotType] ?? []).includes(requested)) {
|
|
299
284
|
throw new Error(`invalid recipe "${requested}" for ${slotType} slot of "${activity}"`);
|
|
300
285
|
}
|
|
301
286
|
|
|
287
|
+
// A switch slot is a flag, not a recipe: it resolves outside the lattice and can never degrade.
|
|
288
|
+
if (isSwitchSlot(slotType)) {
|
|
289
|
+
return { recipe: requested, source, degradedFrom: null, reason: null, overrideUnsatisfied: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
302
292
|
const plan = planRecipe(requested, readiness);
|
|
303
293
|
const degraded = plan.degraded;
|
|
304
294
|
return {
|
|
@@ -310,21 +300,12 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
310
300
|
};
|
|
311
301
|
};
|
|
312
302
|
|
|
313
|
-
// ── the one-line backend status (
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
//
|
|
318
|
-
//
|
|
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.
|
|
303
|
+
// ── the one-line backend status (the tool speaks, the agent pastes) ───────────────────────────────
|
|
304
|
+
// Machine-composed so the agent composes NOTHING factual (a session once echoed SKILL.md's canonical
|
|
305
|
+
// example while the detector said otherwise). Always exactly one line.
|
|
306
|
+
|
|
307
|
+
// The CONFIGURED dispatch posture: the bundled manifests' pins overlaid with the active
|
|
308
|
+
// bridge-settings. No pins or a corrupt bundle → null; the posture segment is a fact, never a gate.
|
|
328
309
|
export const composeConfiguredPosture = (ctx = {}) => {
|
|
329
310
|
try {
|
|
330
311
|
const bundleRoot = ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
|
|
@@ -339,8 +320,7 @@ export const composeConfiguredPosture = (ctx = {}) => {
|
|
|
339
320
|
}
|
|
340
321
|
if (!Object.hasOwn(manifest, 'posture')) continue; // a pre-D5 bridge — legitimate absence
|
|
341
322
|
const posture = manifest.posture;
|
|
342
|
-
//
|
|
343
|
-
// block nulls the WHOLE render — fail closed to silence, never a partial/mangled line.
|
|
323
|
+
// A present-but-invalid block nulls the WHOLE render — never a partial/mangled line.
|
|
344
324
|
const invalid =
|
|
345
325
|
posture === null || typeof posture !== 'object' || Array.isArray(posture) ||
|
|
346
326
|
typeof posture.model !== 'string' || posture.model.length === 0 ||
|
|
@@ -362,33 +342,29 @@ export const composeConfiguredPosture = (ctx = {}) => {
|
|
|
362
342
|
}
|
|
363
343
|
};
|
|
364
344
|
|
|
345
|
+
// The bridge half of a readiness array: every backend-status render lists BRIDGES (the things
|
|
346
|
+
// `/agent-workflow-kit backends` sets up); the executor vehicle is a carrier, judged by the agents
|
|
347
|
+
// mode, and only the recipe lattice sees it.
|
|
348
|
+
export const bridgeEntries = (readiness) => readiness.filter((b) => b.name !== EXECUTOR_PROVIDER);
|
|
349
|
+
|
|
365
350
|
export const composeStatusLine = (detection, recommendation, settings = null, autonomy = null, posture = null) => {
|
|
366
|
-
const backends =
|
|
351
|
+
const backends = bridgeEntries(detection)
|
|
367
352
|
.sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
|
|
368
353
|
.map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`)
|
|
369
354
|
.join(' · ');
|
|
370
355
|
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.
|
|
356
|
+
// A raw env value may carry newlines/control chars — collapse them so the one-line contract holds.
|
|
374
357
|
const oneLine = (s) => String(s).replace(/[\s]+/g, ' ').trim();
|
|
375
358
|
const active = settings?.active ?? [];
|
|
376
|
-
// A RETIRED knob
|
|
377
|
-
//
|
|
378
|
-
// would claim a capability the wrapper no longer has.
|
|
359
|
+
// A RETIRED knob renders as retired: hiding the user's line would be a silent deletion, reading it
|
|
360
|
+
// as active would claim a capability the wrapper no longer has.
|
|
379
361
|
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.
|
|
362
|
+
// Each optional segment renders ONLY when its facts are supplied; an omitted param changes nothing.
|
|
384
363
|
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
364
|
const postureSegment = posture == null ? '' : ` · posture: ${oneLine(posture)}`;
|
|
388
365
|
return base + suffix + autonomySegment + postureSegment;
|
|
389
366
|
};
|
|
390
367
|
|
|
391
|
-
// The one-segment autonomy renderer behind composeStatusLine's 4th param.
|
|
392
368
|
const formatAutonomySegment = (a) => {
|
|
393
369
|
if (a.error) return `MALFORMED policy — ${a.error}`;
|
|
394
370
|
const levels = Object.entries(a.activities ?? {}).map(([k, v]) => `${k}=${v.autonomy}`).join(', ');
|
|
@@ -400,14 +376,8 @@ const formatAutonomySegment = (a) => {
|
|
|
400
376
|
return `${levels} (${state})`;
|
|
401
377
|
};
|
|
402
378
|
|
|
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).
|
|
379
|
+
// The policy lives at the PROJECT root and the paste surfaces run without --cwd, so a subdirectory
|
|
380
|
+
// shell must still find it. Fs-only: this advisor stays spawn-free.
|
|
411
381
|
const projectTopOf = (cwd) => {
|
|
412
382
|
let dir = resolve(cwd);
|
|
413
383
|
for (;;) {
|
|
@@ -418,9 +388,8 @@ const projectTopOf = (cwd) => {
|
|
|
418
388
|
}
|
|
419
389
|
};
|
|
420
390
|
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// pin it. Production passes nothing and probes for real.
|
|
391
|
+
// The facts the status/active lines render. Lazy imports: autonomy-config statically imports THIS
|
|
392
|
+
// module. Never throws — a malformed policy becomes { error }, rendered loudly. `deps` is the probe seam.
|
|
424
393
|
export const composeAutonomyFacts = async (cwd, deps = {}) => {
|
|
425
394
|
try {
|
|
426
395
|
const root = projectTopOf(cwd);
|
|
@@ -428,10 +397,8 @@ export const composeAutonomyFacts = async (cwd, deps = {}) => {
|
|
|
428
397
|
const { config, source } = loadAutonomy(root);
|
|
429
398
|
const resolved = resolveAutonomy(config);
|
|
430
399
|
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).
|
|
400
|
+
// The structural seed is a fresh-deployment NORMAL: reading it as "declared" would report
|
|
401
|
+
// "render DRIFT" on every fresh upgrade.
|
|
435
402
|
if (isSparseSeedConfig(config)) {
|
|
436
403
|
return { source, defaultsEquivalent: true, redlines: resolved.redlines, activities: resolved.activities, renderState: null };
|
|
437
404
|
}
|
|
@@ -452,29 +419,22 @@ export const composeAutonomyFacts = async (cwd, deps = {}) => {
|
|
|
452
419
|
|
|
453
420
|
// ── the one-line ACTIVE-recipe line (the discovery line — configured, never recommended) ───────────
|
|
454
421
|
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
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).
|
|
422
|
+
// ONE line rendering the CONFIGURED recipe of every activity/slot with its source, degradation and
|
|
423
|
+
// dispatched wrappers — contrasted with the readiness-RECOMMENDED recipe, which is NOT what runs.
|
|
424
|
+
// The session-start checklist and the handover "Active recipes:" slot paste it verbatim.
|
|
463
425
|
export const composeActiveRecipeLine = ({ config, source } = {}, detection, autonomy = null) => {
|
|
464
426
|
const cells = [];
|
|
465
427
|
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
428
|
const level = autonomy?.activities?.[activity]?.autonomy;
|
|
469
429
|
const auto = level ? `; autonomy ${level}` : '';
|
|
470
|
-
for (const slot of Object.
|
|
430
|
+
for (const [slot, slotType] of Object.entries(def.slots)) {
|
|
471
431
|
const r = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity, slot });
|
|
472
|
-
const
|
|
432
|
+
const dispatch = isSwitchSlot(slotType) ? [] : planRecipe(r.recipe, detection).dispatch;
|
|
473
433
|
const wrappers = dispatch.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
474
434
|
const srcLabel = r.source === 'config' ? 'configured' : 'computed default';
|
|
475
435
|
const head = r.degradedFrom
|
|
476
436
|
? `${activity}.${slot} = ${r.degradedFrom} (${srcLabel}; degrades here to ${r.recipe} — ${r.reason}${auto})`
|
|
477
|
-
: `${activity}.${slot} = ${r.recipe} (${srcLabel}${auto})`;
|
|
437
|
+
: `${activity}.${slot} = ${r.recipe} (${srcLabel}${isSwitchSlot(slotType) ? '; switch' : ''}${auto})`;
|
|
478
438
|
const suffix =
|
|
479
439
|
wrappers.length >= 2
|
|
480
440
|
? ` → every backend every round: ${wrappers.join(' + ')}`
|
|
@@ -486,18 +446,16 @@ export const composeActiveRecipeLine = ({ config, source } = {}, detection, auto
|
|
|
486
446
|
}
|
|
487
447
|
const rec = recommendRecipe(detection);
|
|
488
448
|
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.
|
|
449
|
+
// A MALFORMED policy surfaces LOUDLY here too: rendering cells without levels would hide the STOP.
|
|
491
450
|
const malformed = autonomy?.error
|
|
492
451
|
? ` · autonomy: MALFORMED policy — ${String(autonomy.error).replace(/[\s]+/g, ' ').trim()}`
|
|
493
452
|
: '';
|
|
494
|
-
return `active recipes (${origin}): ${cells.join(' · ')} — the configured
|
|
453
|
+
return `active recipes (${origin}): ${cells.join(' · ')} — the configured orchestration values above are what runs; readiness-recommended here: ${rec.recipe} (informational)${malformed}`;
|
|
495
454
|
};
|
|
496
455
|
|
|
497
456
|
// ── report + CLI ─────────────────────────────────────────────────────────────────
|
|
498
457
|
|
|
499
|
-
// The structured report behind `--json
|
|
500
|
-
// (additive) the pasteable one-line backend status composed from the same detection.
|
|
458
|
+
// The structured report behind `--json`, incl. the same one-line status the --status-line mode emits.
|
|
501
459
|
export const buildReport = (detection, settings = null, autonomy = null, posture = null) => {
|
|
502
460
|
const recommendation = recommendRecipe(detection);
|
|
503
461
|
return {
|
|
@@ -511,17 +469,15 @@ export const buildReport = (detection, settings = null, autonomy = null, posture
|
|
|
511
469
|
})),
|
|
512
470
|
recommendation,
|
|
513
471
|
plans: RECIPES.map((r) => planRecipe(r.id, detection)),
|
|
514
|
-
// The
|
|
515
|
-
// expose a stale machine-composed status line (Segment B).
|
|
472
|
+
// The --json envelope must never expose a status line staler than the --status-line surface.
|
|
516
473
|
statusLine: composeStatusLine(detection, recommendation, settings, autonomy, posture),
|
|
517
474
|
};
|
|
518
475
|
};
|
|
519
476
|
|
|
520
|
-
//
|
|
521
|
-
// and the per-recipe plan for the current environment (degradation reasons + dispatch + notes).
|
|
477
|
+
// Deterministic human advisor text: the recipes, the recommendation, and the per-recipe plan here.
|
|
522
478
|
export const formatRecipes = (detection) => {
|
|
523
479
|
const lines = [
|
|
524
|
-
'agent-workflow orchestration recipes (read-only — the orchestrator executes via the bridge skills and always commits)',
|
|
480
|
+
'agent-workflow orchestration recipes (read-only — the orchestrator executes via the bridge skills or the executor vehicle and always commits)',
|
|
525
481
|
'',
|
|
526
482
|
];
|
|
527
483
|
for (const r of RECIPES) lines.push(` ${r.title} (${r.id}) — ${r.summary}`);
|
|
@@ -530,7 +486,9 @@ export const formatRecipes = (detection) => {
|
|
|
530
486
|
for (const r of RECIPES) {
|
|
531
487
|
const p = planRecipe(r.id, detection);
|
|
532
488
|
const arrow = p.degraded ? ` → ${p.effective}` : '';
|
|
533
|
-
const who = p.dispatch.length
|
|
489
|
+
const who = p.dispatch.length
|
|
490
|
+
? p.dispatch.map((d) => `${d.display} ${d.role}${d.vehicle ? ` (vehicle ${d.vehicle})` : ''}`).join(', ')
|
|
491
|
+
: 'orchestrator only';
|
|
534
492
|
lines.push(` ${r.title}${arrow}: ${who}`);
|
|
535
493
|
for (const step of p.degradation) lines.push(` ↳ ${step.reason}`);
|
|
536
494
|
for (const note of p.notes) lines.push(` • ${note}`);
|
|
@@ -538,22 +496,37 @@ export const formatRecipes = (detection) => {
|
|
|
538
496
|
return lines.join('\n');
|
|
539
497
|
};
|
|
540
498
|
|
|
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.
|
|
499
|
+
// Closed argv vocabulary: the mode outputs are pasted as fact, so a mistyped flag masquerading as a
|
|
500
|
+
// mode would be a silent failure.
|
|
545
501
|
const KNOWN_ARGS = new Set(['--help', '-h', '--json', '--status-line', '--active-line']);
|
|
546
502
|
const EXCLUSIVE_ARGS = ['--json', '--status-line', '--active-line']; // each owns stdout whole
|
|
547
503
|
|
|
548
|
-
|
|
504
|
+
// The readiness array EVERY mode composes: the detected backends plus the executor vehicle as the
|
|
505
|
+
// one carry provider. Two independent axes: the vehicle is surveyed first, and a bridge detector
|
|
506
|
+
// failure reaches `deps.onDetectError` exactly once (default: a loud stderr line) while the
|
|
507
|
+
// vehicle's readiness survives it — so a caller's fail-closed state for bridges never masks the
|
|
508
|
+
// carrier, and the reverse. `deps` is also the seam a test injects a fake survey or detector through.
|
|
509
|
+
export const composeReadiness = (cwd, deps = {}) => {
|
|
510
|
+
const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(cwd, deps);
|
|
511
|
+
let detection = [];
|
|
512
|
+
try {
|
|
513
|
+
detection = (deps.detect ?? detectBackends)();
|
|
514
|
+
} catch (err) {
|
|
515
|
+
(deps.onDetectError ?? ((e) => console.error(`[agent-workflow-kit] backend detection failed: ${e.message}`)))(err);
|
|
516
|
+
}
|
|
517
|
+
return withVehicle(detection, survey);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const main = async (argv, deps = {}) => {
|
|
549
521
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
550
522
|
console.log(`recipes — read-only orchestration-recipe advisor for the agent-workflow family.
|
|
551
523
|
|
|
552
524
|
Usage:
|
|
553
525
|
node recipes.mjs [--json | --status-line | --active-line]
|
|
554
526
|
|
|
555
|
-
Lists the
|
|
556
|
-
detector, plans + recommends one for the current
|
|
527
|
+
Lists the five recipes (Solo / Reviewed / Council / Delegated / Subagent) and, from the read-only
|
|
528
|
+
backend detector plus the executor-vehicle survey, plans + recommends one for the current
|
|
529
|
+
environment. --status-line prints exactly ONE
|
|
557
530
|
line — the machine-composed backend-status summary the bootstrap/upgrade reports paste verbatim
|
|
558
531
|
(incl. the per-activity autonomy segment: effective levels + render-sync state, honest
|
|
559
532
|
computed-defaults wording when no policy file exists). --active-line prints exactly ONE line — the
|
|
@@ -574,31 +547,30 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
|
|
|
574
547
|
console.error(`[agent-workflow-kit] ${exclusive.join(' and ')} are mutually exclusive — pick one output`);
|
|
575
548
|
return 1;
|
|
576
549
|
}
|
|
577
|
-
const
|
|
550
|
+
const cwd = process.cwd();
|
|
551
|
+
const readiness = composeReadiness(cwd, deps);
|
|
578
552
|
if (argv.includes('--active-line')) {
|
|
579
|
-
// Lazy
|
|
580
|
-
// so the config reader is pulled in at run time only — no static import cycle.
|
|
553
|
+
// Lazy: orchestration-config.mjs statically imports this module — no static cycle.
|
|
581
554
|
const { loadConfig } = await import('./orchestration-config.mjs');
|
|
582
555
|
try {
|
|
583
|
-
console.log(composeActiveRecipeLine(loadConfig(
|
|
556
|
+
console.log(composeActiveRecipeLine(loadConfig(cwd), readiness, await composeAutonomyFacts(cwd)));
|
|
584
557
|
} catch (err) {
|
|
585
558
|
console.error(`[agent-workflow-kit] ${err.message}`);
|
|
586
559
|
return err.exitCode ?? 1;
|
|
587
560
|
}
|
|
588
561
|
} else if (argv.includes('--status-line')) {
|
|
589
562
|
const snapshot = settingsSnapshot();
|
|
590
|
-
console.log(composeStatusLine(
|
|
563
|
+
console.log(composeStatusLine(readiness, recommendRecipe(readiness), snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })));
|
|
591
564
|
} else if (argv.includes('--json')) {
|
|
592
565
|
const snapshot = settingsSnapshot();
|
|
593
|
-
console.log(JSON.stringify(buildReport(
|
|
566
|
+
console.log(JSON.stringify(buildReport(readiness, snapshot, await composeAutonomyFacts(cwd), composeConfiguredPosture({ settings: snapshot })), null, 2));
|
|
594
567
|
}
|
|
595
|
-
else console.log(formatRecipes(
|
|
568
|
+
else console.log(formatRecipes(readiness));
|
|
596
569
|
return 0;
|
|
597
570
|
};
|
|
598
571
|
|
|
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.
|
|
572
|
+
// Natural exit via process.exitCode — never process.exit inside the async main (it would drop
|
|
573
|
+
// buffered stdio on piped stderr), and never a TOP-LEVEL await (that would deadlock the import cycle).
|
|
602
574
|
if (isDirectRun(import.meta.url)) {
|
|
603
575
|
main(process.argv.slice(2)).then(
|
|
604
576
|
(code) => {
|