@sabaiway/agent-workflow-kit 10.3.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 +30 -0
- package/README.md +5 -5
- 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 +3 -1
- package/references/modes/set-recipe.md +22 -5
- package/references/modes/status.md +2 -2
- package/references/modes/upgrade.md +1 -1
- 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/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/family-registry.mjs +38 -18
- package/tools/flow-check.mjs +2 -7
- package/tools/inject-methodology.mjs +4 -0
- package/tools/orchestration-config.mjs +34 -13
- package/tools/procedures.mjs +65 -52
- package/tools/recipes.mjs +156 -184
- package/tools/recommendations.mjs +77 -11
- package/tools/renderers.mjs +27 -7
- package/tools/review-state.mjs +10 -11
- package/tools/set-recipe.mjs +63 -24
- package/tools/view-model.mjs +17 -3
|
@@ -51,7 +51,7 @@ import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-confi
|
|
|
51
51
|
import { deriveDoctorPlan } from './autonomy-doctor.mjs';
|
|
52
52
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
53
53
|
import { isDirectRun } from './direct-run.mjs';
|
|
54
|
-
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
54
|
+
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
55
55
|
import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
56
56
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
57
57
|
import { shellQuoteArg } from './review-state.mjs';
|
|
@@ -82,7 +82,9 @@ import { loadConfig } from './orchestration-config.mjs';
|
|
|
82
82
|
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
83
83
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
84
84
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
85
|
-
import { preflightCheapAgents } from './cheap-agents.mjs';
|
|
85
|
+
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
86
|
+
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
87
|
+
import { surveyExecutorVehicle, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
|
|
86
88
|
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
87
89
|
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
88
90
|
import {
|
|
@@ -176,6 +178,10 @@ export const SEVERITIES = Object.freeze({
|
|
|
176
178
|
'mcp-channel.masked': SEVERITY_OPTIONAL,
|
|
177
179
|
'mcp-channel.differing': SEVERITY_ATTENTION,
|
|
178
180
|
agents: SEVERITY_OPTIONAL,
|
|
181
|
+
// The `agents` row above is an OFFER to place vehicles. This one reports a config that ALREADY
|
|
182
|
+
// names the subagent carrier over a vehicle that cannot carry it — a configured declaration that
|
|
183
|
+
// is broken, which is what attention means.
|
|
184
|
+
'executor-vehicle': SEVERITY_ATTENTION,
|
|
179
185
|
'family-freshness': SEVERITY_ATTENTION,
|
|
180
186
|
'adr-store-migration': SEVERITY_ATTENTION,
|
|
181
187
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
@@ -250,7 +256,8 @@ export const WHATS = Object.freeze({
|
|
|
250
256
|
'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
|
|
251
257
|
'mcp-channel.masked': '{rel} is a {className} here (a sandbox device mask is the usual cause), so the entry to merge is printed instead',
|
|
252
258
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
253
|
-
agents: '{n}
|
|
259
|
+
agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
|
|
260
|
+
'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
|
|
254
261
|
'family-freshness': '{parts}',
|
|
255
262
|
'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
|
|
256
263
|
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
@@ -309,7 +316,8 @@ export const BENEFITS = Object.freeze({
|
|
|
309
316
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
310
317
|
'state-block': 'no silent stalls — a turn ending on «you are not needed», or on work it never started, warns at once instead of waiting to be spotted',
|
|
311
318
|
'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
|
|
312
|
-
agents: 'cost and quiet — mechanical work
|
|
319
|
+
agents: 'cost and quiet — cheap-model mechanical work, no shell on a read-only vehicle (no prompt flood); the executor carries slices you verify',
|
|
320
|
+
'executor-vehicle': 'carrier readiness — a slot you configured subagent dispatches the subagent it names instead of silently running solo',
|
|
313
321
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
314
322
|
'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
|
|
315
323
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
@@ -367,6 +375,10 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
367
375
|
// which is why the review-recipe benefit is worded for either slot rather than for review alone.
|
|
368
376
|
{ id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
369
377
|
{ id: 'agents', mode: 'agents', advisorKey: 'agents' },
|
|
378
|
+
// A DISTINCT capability from the offer above: the offer converges the moment nothing is left to
|
|
379
|
+
// PLACE, which a customized-but-unusable executor also satisfies — so it can never observe the
|
|
380
|
+
// state that makes a configured subagent carrier run solo.
|
|
381
|
+
{ id: 'executor-vehicle', mode: 'agents', advisorKey: 'executor-vehicle' },
|
|
370
382
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
371
383
|
// a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
|
|
372
384
|
// same doctrine that keeps sandbox network/filesystem allowances HAND-APPLY); velocity presents the
|
|
@@ -477,11 +489,12 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
477
489
|
// The VALIDATED reader (Segment B): a schema-invalid config (unknown activity/slot,
|
|
478
490
|
// bad recipe) throws here and becomes a stated skip — raw JSON.parse would silently ignore it.
|
|
479
491
|
const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
480
|
-
const
|
|
492
|
+
const readiness = composeReadiness(root, { ...deps, detect: deps.detect ?? (() => detectBackends(deps)), onDetectError: (err) => { throw err; } });
|
|
481
493
|
const degraded = [];
|
|
482
494
|
for (const [activity, def] of Object.entries(ACTIVITIES)) {
|
|
483
495
|
for (const slot of Object.keys(def.slots)) {
|
|
484
|
-
|
|
496
|
+
if (config?.[activity]?.[slot] === 'subagent') continue; // the executor vehicle has its own probe
|
|
497
|
+
const r = resolveActivityRecipe({ config, readiness, activity, slot });
|
|
485
498
|
if (r.degradedFrom) degraded.push(`${activity}.${slot}: configured ${r.degradedFrom} degrades to ${r.recipe} (${r.reason})`);
|
|
486
499
|
}
|
|
487
500
|
}
|
|
@@ -952,11 +965,9 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
952
965
|
// skipped the per-vehicle plan the user is supposed to see before consenting.
|
|
953
966
|
// The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
|
|
954
967
|
// VISIBLE deployment, and the apply slot must stay one pure executable command.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`,
|
|
959
|
-
'agents',
|
|
968
|
+
const executors = toPlace.filter((item) => item.name === EXECUTOR_VEHICLE).length;
|
|
969
|
+
add('agents', fillTemplate(WHATS.agents, { n: toPlace.length, ex: executors, ro: toPlace.length - executors }),
|
|
970
|
+
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`, 'agents',
|
|
960
971
|
`hidden-mode deployments only: after the --apply the preview prints, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
961
972
|
);
|
|
962
973
|
} catch (err) {
|
|
@@ -964,6 +975,60 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
964
975
|
}
|
|
965
976
|
};
|
|
966
977
|
|
|
978
|
+
// The subagent carrier's ONE instrument (contract: kit/carriers). Neither neighbour can report this
|
|
979
|
+
// state: `probeReviewRecipe` skips a slot configured `subagent` by construction, and the offer above
|
|
980
|
+
// judges only what is left to PLACE — a customized-but-broken vehicle leaves nothing to place. So a
|
|
981
|
+
// project could declare the carrier and have every such slot run solo, unseen.
|
|
982
|
+
const VEHICLE_BROKEN_STATES = Object.freeze(['missing', 'unusable']);
|
|
983
|
+
|
|
984
|
+
const writerBlock = (root, deps) => {
|
|
985
|
+
try {
|
|
986
|
+
preflightCheapAgents({ cwd: root }, deps);
|
|
987
|
+
return null;
|
|
988
|
+
} catch (err) {
|
|
989
|
+
return safeLine(err?.message ?? String(err));
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
994
|
+
try {
|
|
995
|
+
// The VALIDATED reader, as probeReviewRecipe uses it: a schema-invalid config is a stated skip,
|
|
996
|
+
// never an item computed over a shape nothing accepted.
|
|
997
|
+
const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
998
|
+
const configured = Object.entries(ACTIVITIES).flatMap(([activity, def]) =>
|
|
999
|
+
Object.keys(def.slots).filter((slot) => config?.[activity]?.[slot] === 'subagent'));
|
|
1000
|
+
if (configured.length === 0) return;
|
|
1001
|
+
const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(root, deps);
|
|
1002
|
+
if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return;
|
|
1003
|
+
const reason = safeLine(survey.reason ?? '');
|
|
1004
|
+
const stamp = readStamp(join(root, WORKFLOW_STAMP), readFsDeps(deps));
|
|
1005
|
+
const preconditions = [
|
|
1006
|
+
...(stamp === EXPECTED_WORKFLOW_VERSION ? [] : [`run /agent-workflow-kit upgrade first (deployment stamp ${stamp ?? 'none'}, expected ${EXPECTED_WORKFLOW_VERSION})`]),
|
|
1007
|
+
...(survey.state === 'unusable' ? [`${reason || 'the vehicle file is unusable'} — fix that`] : []),
|
|
1008
|
+
];
|
|
1009
|
+
// The writer refuses on ANY vehicle path it cannot touch (a symlinked read-only vehicle blocks a
|
|
1010
|
+
// missing executor's placement too), so its own preflight is the last precondition — named once.
|
|
1011
|
+
const blocked = writerBlock(root, deps);
|
|
1012
|
+
if (blocked && blocked !== reason) preconditions.push(`${blocked} — fix that`);
|
|
1013
|
+
const room = templateBudget(WHATS['executor-vehicle']) - String(configured.length).length - survey.state.length - 2;
|
|
1014
|
+
add(
|
|
1015
|
+
'executor-vehicle',
|
|
1016
|
+
fillTemplate(WHATS['executor-vehicle'], {
|
|
1017
|
+
n: configured.length,
|
|
1018
|
+
state: survey.state,
|
|
1019
|
+
reason: reason && room > 0 ? `: ${truncatedTo(reason, room)}` : '',
|
|
1020
|
+
}),
|
|
1021
|
+
// The writer places a MISSING vehicle; an unusable path (a symlink, a read-only customization)
|
|
1022
|
+
// is kept or refused, so that state's apply is a hand-apply precondition before the writer.
|
|
1023
|
+
`${preconditions.length ? `HAND-APPLY: ${preconditions.join('; ')}, then run: ` : ''}node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
|
|
1024
|
+
'executor-vehicle',
|
|
1025
|
+
`hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
1026
|
+
);
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
skip('executor-vehicle', err);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
967
1032
|
const probeFamilyFreshness = ({ deps, add, skip }) => {
|
|
968
1033
|
try {
|
|
969
1034
|
const survey = deps.surveyFamily ?? surveyFamily;
|
|
@@ -1485,6 +1550,7 @@ const PROBES = Object.freeze([
|
|
|
1485
1550
|
probeReadLane,
|
|
1486
1551
|
probeStateBlockHook,
|
|
1487
1552
|
probeCheapAgents,
|
|
1553
|
+
probeExecutorVehicle,
|
|
1488
1554
|
probeFamilyFreshness,
|
|
1489
1555
|
probeAdrStore,
|
|
1490
1556
|
probeMasksItem,
|
package/tools/renderers.mjs
CHANGED
|
@@ -20,6 +20,10 @@ const SETTINGS_COL = 14;
|
|
|
20
20
|
// checks, so a future token joins the render by joining this line.
|
|
21
21
|
const ACTIONABLE_ADR_LAYOUTS = Object.freeze(['old', 'old-unrotated']);
|
|
22
22
|
|
|
23
|
+
const DEGRADE_ARROW = '←';
|
|
24
|
+
const EMPTY_CELL = '—';
|
|
25
|
+
const UNKNOWN_EXECUTOR = 'unknown';
|
|
26
|
+
|
|
23
27
|
const SGR = Object.freeze({ bold: '\x1b[1m', reset: '\x1b[0m' });
|
|
24
28
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
25
29
|
export const visibleLength = (s) => s.replace(ANSI_RE, '').length;
|
|
@@ -116,13 +120,22 @@ const renderSettings = (vm, { color, glyph }) => {
|
|
|
116
120
|
const s = vm.project?.settings;
|
|
117
121
|
if (!s) return [];
|
|
118
122
|
const lines = ['', heading(BLOCK_TITLES.settings, color)];
|
|
119
|
-
// recipes —
|
|
123
|
+
// recipes — one line per slot: the effective recipe, where it came from, and the requested value
|
|
124
|
+
// a degrade replaced (a joined single line could not carry three activities and their slots), or a
|
|
125
|
+
// loud error; a detector floor adds a sub-line.
|
|
120
126
|
if (s.recipes?.error) lines.push(` ${pad(SETTINGS_LABELS.recipes, SETTINGS_COL)}error: ${s.recipes.error}`);
|
|
121
127
|
else if (s.recipes) {
|
|
122
|
-
const
|
|
123
|
-
|
|
128
|
+
const rows = s.recipes.pairs.map((p) => {
|
|
129
|
+
const source = p.source ? ` (${p.source})` : '';
|
|
130
|
+
const recovery = p.degradedFrom === 'subagent' ? ' — runs solo until the executor vehicle is usable (/agent-workflow-kit agents)' : '';
|
|
131
|
+
const degraded = p.degradedFrom ? ` ${DEGRADE_ARROW} degraded from ${p.degradedFrom}${recovery}` : '';
|
|
132
|
+
return `${p.key}: ${p.recipe}${source}${degraded}`;
|
|
133
|
+
});
|
|
134
|
+
(rows.length ? rows : [EMPTY_CELL]).forEach((row, i) => {
|
|
135
|
+
lines.push(` ${pad(i === 0 ? SETTINGS_LABELS.recipes : '', SETTINGS_COL)}${row}`);
|
|
136
|
+
});
|
|
124
137
|
if (s.recipes.detectError) {
|
|
125
|
-
lines.push(` ${pad('', SETTINGS_COL)}${glyph.note} couldn't check backends (${s.recipes.detectError}); recipes floored at solo`);
|
|
138
|
+
lines.push(` ${pad('', SETTINGS_COL)}${glyph.note} couldn't check backends (${s.recipes.detectError}); bridge-backed recipes floored at solo; the executor vehicle is unaffected`);
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
// attribution — effective includeCoAuthoredBy; a real local override is called out.
|
|
@@ -136,10 +149,17 @@ const renderSettings = (vm, { color, glyph }) => {
|
|
|
136
149
|
else if (s.velocity) {
|
|
137
150
|
lines.push(` ${pad(SETTINGS_LABELS.velocity, SETTINGS_COL)}defaultMode=${String(s.velocity.defaultMode)} · allow project/local=${s.velocity.allow.project}/${s.velocity.allow.local}`);
|
|
138
151
|
}
|
|
139
|
-
// cheap agents — the kit-placed .claude/agents/ vehicles: placed count vs the bundle
|
|
140
|
-
|
|
152
|
+
// cheap agents — the kit-placed .claude/agents/ vehicles: placed count vs the bundle, then the
|
|
153
|
+
// split the subagent carrier turns on — the read-only vehicles and the ONE executor, whose state
|
|
154
|
+
// decides whether a slot configured `subagent` can ride it (an unusable one carries its reason).
|
|
155
|
+
if (s.agents?.error) {
|
|
156
|
+
const partial = s.agents.executor ? ` — executor ${s.agents.executor}${s.agents.executorReason ? ` (${s.agents.executorReason})` : ''}` : '';
|
|
157
|
+
lines.push(` ${pad(SETTINGS_LABELS.agents, SETTINGS_COL)}error: ${s.agents.error}${partial}`);
|
|
158
|
+
}
|
|
141
159
|
else if (s.agents) {
|
|
142
|
-
|
|
160
|
+
const reason = s.agents.executorReason ? ` (${s.agents.executorReason})` : '';
|
|
161
|
+
const executor = s.agents.executor == null ? `${UNKNOWN_EXECUTOR} (the installed kit predates the field)` : `${s.agents.executor}${reason}`;
|
|
162
|
+
lines.push(` ${pad(SETTINGS_LABELS.agents, SETTINGS_COL)}${s.agents.placed}/${s.agents.bundled} placed — ${s.agents.readOnly} read-only, executor ${executor}`);
|
|
143
163
|
}
|
|
144
164
|
// gate hook — the opt-in PreToolUse gate-approval hook: wired / file placed / declaration present /
|
|
145
165
|
// declared gate count (null → '?' — unknown is shown as unknown, never as a number).
|
package/tools/review-state.mjs
CHANGED
|
@@ -93,9 +93,9 @@ import { join, dirname } from 'node:path';
|
|
|
93
93
|
import { fileURLToPath } from 'node:url';
|
|
94
94
|
import { spawnSync } from 'node:child_process';
|
|
95
95
|
import { createHash } from 'node:crypto';
|
|
96
|
-
import { detectBackends
|
|
96
|
+
import { detectBackends } from './detect-backends.mjs';
|
|
97
97
|
import { isDirectRun } from './direct-run.mjs';
|
|
98
|
-
import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe } from './recipes.mjs';
|
|
98
|
+
import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe, composeReadiness } from './recipes.mjs';
|
|
99
99
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
100
100
|
import { resolveFlowStorePath, readFlowStore, deriveFlowOwner, readPlanFrontmatterId } from './flow-store.mjs';
|
|
101
101
|
import { CHAIN_KIND, authoritativeFlowRecords } from './flow-record.mjs';
|
|
@@ -309,16 +309,16 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
|
|
|
309
309
|
// work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
|
|
310
310
|
// must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
|
|
311
311
|
// flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
|
|
312
|
-
export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
312
|
+
export const buildState = ({ cwd, env = process.env, detect = detectBackends, surveyVehicle, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
313
313
|
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
314
314
|
const { config, source: configSource } = loadConfig(root);
|
|
315
|
-
|
|
315
|
+
// A bridge-detector throw reaches the hook, never a catch that would also lose the surveyed
|
|
316
|
+
// executor vehicle: bridge readiness goes unknown (fail closed below), the carrier stays known.
|
|
316
317
|
let detectionWarning = null;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
}
|
|
318
|
+
const onDetectError = (err) => {
|
|
319
|
+
detectionWarning = `backend detection failed (${(err && err.message) || err}) — bridge readiness unknown.`;
|
|
320
|
+
};
|
|
321
|
+
const detection = composeReadiness(root, { detect, surveyVehicle, onDetectError });
|
|
322
322
|
// The resolver stays for DISPLAY/diagnostics only; the OBLIGATIONS come from the configured
|
|
323
323
|
// recipe (never the readiness-degraded effective one — no silent solo).
|
|
324
324
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
@@ -439,7 +439,6 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
|
|
|
439
439
|
degradedExempt,
|
|
440
440
|
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
|
|
441
441
|
detectionWarning,
|
|
442
|
-
anyReviewerReady: detection.some((b) => b.readiness === READY),
|
|
443
442
|
flowPresent,
|
|
444
443
|
flowArmed,
|
|
445
444
|
flowBrokenReason,
|
|
@@ -733,7 +732,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
733
732
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
734
733
|
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
735
734
|
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
736
|
-
const state = buildState({ cwd, env, detect, lstat: ctx.lstat, readFile: ctx.readFile });
|
|
735
|
+
const state = buildState({ cwd, env, detect, surveyVehicle: ctx.surveyVehicle, lstat: ctx.lstat, readFile: ctx.readFile });
|
|
737
736
|
const check = decideCheck(state);
|
|
738
737
|
// The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
|
|
739
738
|
const advisory = maskAdvisoryLine(state);
|
package/tools/set-recipe.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// set-recipe.mjs — the WRITER for docs/ai/orchestration.json. The division of labor (AD-025): the AGENT
|
|
3
|
-
// turns plain language into explicit `--set <activity>.<slot>=<
|
|
3
|
+
// turns plain language into explicit `--set <activity>.<slot>=<value>` / `--unset <activity>.<slot>`
|
|
4
4
|
// ops; the KIT does the deterministic validate → merge → preview → write. The kit ships NO NL parser
|
|
5
5
|
// (stays dependency-free + deterministic) and performs no `all`-magic — the agent expands "both review"
|
|
6
6
|
// into explicit per-activity ops (asking if scope is unclear).
|
|
@@ -17,15 +17,25 @@
|
|
|
17
17
|
// language when narrating. Exit codes: 0 success (an explicit recipe that gracefully degrades is still
|
|
18
18
|
// 0); 2 usage (bad/duplicate op, --write with zero ops); 1 config error (malformed/unreadable config)
|
|
19
19
|
// or a write STOP (no deployment / symlinked leaf). main(argv, ctx) → { code, stdout, stderr }; cwd /
|
|
20
|
-
// env / home / detect / fs are injectable for host-independent tests.
|
|
20
|
+
// env / home / detect / surveyVehicle / fs are injectable for host-independent tests.
|
|
21
|
+
//
|
|
22
|
+
// It writes every slot of every activity in the registry (carriers.mjs, via recipes.mjs) — the
|
|
23
|
+
// carrier slots and the `routine.parallel` switch included — and resolves each preview against the
|
|
24
|
+
// SAME readiness the recipes advisor composes: detected backends plus the executor-vehicle survey.
|
|
21
25
|
//
|
|
22
26
|
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
23
27
|
|
|
24
28
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
25
29
|
import { homedir } from 'node:os';
|
|
26
|
-
import { detectBackends } from './detect-backends.mjs';
|
|
27
30
|
import { isDirectRun } from './direct-run.mjs';
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
ACTIVITIES,
|
|
33
|
+
SLOT_RECIPES,
|
|
34
|
+
EXECUTOR_APPLY,
|
|
35
|
+
composeReadiness,
|
|
36
|
+
resolveActivityRecipe,
|
|
37
|
+
composeActiveRecipeLine,
|
|
38
|
+
} from './recipes.mjs';
|
|
29
39
|
import { loadAutonomy, resolveAutonomy } from './autonomy-config.mjs';
|
|
30
40
|
import {
|
|
31
41
|
CONFIG_REL,
|
|
@@ -35,6 +45,7 @@ import {
|
|
|
35
45
|
parseOp,
|
|
36
46
|
applySetOps,
|
|
37
47
|
serializeConfig,
|
|
48
|
+
refreshReadme,
|
|
38
49
|
CANON_README,
|
|
39
50
|
} from './orchestration-config.mjs';
|
|
40
51
|
import { writeConfig as writeConfigFs } from './orchestration-write.mjs';
|
|
@@ -50,7 +61,7 @@ const parseArgs = (argv) => {
|
|
|
50
61
|
let write = false;
|
|
51
62
|
let json = false;
|
|
52
63
|
const takeOp = (kind, tok) => {
|
|
53
|
-
if (tok === undefined || tok.startsWith('--')) throw fail(2, `--${kind} requires <activity>.<slot>${kind === 'set' ? '=<
|
|
64
|
+
if (tok === undefined || tok.startsWith('--')) throw fail(2, `--${kind} requires <activity>.<slot>${kind === 'set' ? '=<value>' : ''}`);
|
|
54
65
|
const op = parseOp(kind, tok);
|
|
55
66
|
const key = `${op.activity}.${op.slot}`;
|
|
56
67
|
if (seen.has(key)) throw fail(2, `duplicate op for "${key}" — name each activity.slot at most once`);
|
|
@@ -74,12 +85,24 @@ const parseArgs = (argv) => {
|
|
|
74
85
|
|
|
75
86
|
// ── effective-recipe resolution per op (degradation honesty) ────────────────────────
|
|
76
87
|
|
|
88
|
+
// The readiness EVERY resolution here runs against: the detected bridges plus the executor-vehicle
|
|
89
|
+
// survey, composed by the one helper the recipes CLI uses. Detection is a SECONDARY input — a bridge
|
|
90
|
+
// detector throw must NOT block the write (the config write is readiness-independent) and must not
|
|
91
|
+
// cost the CARRIER either: the hook warns, the bridge half floors at not-ready, the vehicle survives.
|
|
92
|
+
const composeReadinessOrWarn = (cwd, deps, warnings) =>
|
|
93
|
+
composeReadiness(cwd, {
|
|
94
|
+
...deps,
|
|
95
|
+
onDetectError: (err) => warnings.push(
|
|
96
|
+
`backend detection failed (${(err && err.message) || err}) — treating every bridge as not ready; recipes needing a bridge degrade to solo (the executor vehicle is unaffected).`,
|
|
97
|
+
),
|
|
98
|
+
});
|
|
99
|
+
|
|
77
100
|
// A single op's before/after value + the effective recipe it resolves to here (vs live readiness).
|
|
78
101
|
// `to` is null for an unset (falls to the computed default). degradedFrom/reason carry the honesty.
|
|
79
|
-
const resolveOp = (op, current, after,
|
|
102
|
+
const resolveOp = (op, current, after, readiness) => {
|
|
80
103
|
const from = current?.[op.activity]?.[op.slot] ?? null;
|
|
81
104
|
const to = after?.[op.activity]?.[op.slot] ?? null;
|
|
82
|
-
const r = resolveActivityRecipe({ config: after ?? {}, readiness
|
|
105
|
+
const r = resolveActivityRecipe({ config: after ?? {}, readiness, activity: op.activity, slot: op.slot });
|
|
83
106
|
return { activity: op.activity, slot: op.slot, from, to, effective: r.recipe, degradedFrom: r.degradedFrom, reason: r.reason };
|
|
84
107
|
};
|
|
85
108
|
|
|
@@ -127,19 +150,40 @@ const buildJson = ({ changed, unchanged, warnings, writtenPath, noop, activeLine
|
|
|
127
150
|
activeLine: activeLine ?? null,
|
|
128
151
|
});
|
|
129
152
|
|
|
153
|
+
// The writable surface, rendered FROM the registry — never re-typed as literals, so an activity, a
|
|
154
|
+
// slot or an accepted value added to the table shows up in the help (and in the doc that quotes it).
|
|
155
|
+
const ACTIVITY_LINES = Object.entries(ACTIVITIES)
|
|
156
|
+
.map(([activity, def]) => ` ${activity} → ${Object.keys(def.slots).join(', ')}`)
|
|
157
|
+
.join('\n');
|
|
158
|
+
|
|
159
|
+
const VALUE_LINES = Object.entries(SLOT_RECIPES)
|
|
160
|
+
.map(([slotType, values]) => ` ${slotType} slots accept ${values.join(' | ')}`)
|
|
161
|
+
.join('\n');
|
|
162
|
+
|
|
163
|
+
const QUALIFIED_SLOTS = Object.entries(ACTIVITIES)
|
|
164
|
+
.flatMap(([activity, def]) => Object.keys(def.slots).map((slot) => `${activity}.${slot}`))
|
|
165
|
+
.join(', ');
|
|
166
|
+
|
|
130
167
|
const HELP = `set-recipe — write the per-project orchestration config (docs/ai/orchestration.json).
|
|
131
168
|
|
|
132
169
|
Usage:
|
|
133
|
-
node set-recipe.mjs [--set <activity>.<slot>=<
|
|
170
|
+
node set-recipe.mjs [--set <activity>.<slot>=<value>]... [--unset <activity>.<slot>]... [--write] [--json]
|
|
134
171
|
|
|
135
|
-
--set <activity>.<slot>=<
|
|
172
|
+
--set <activity>.<slot>=<value> pin a value (fully-qualified; e.g. plan-authoring.review=council)
|
|
136
173
|
--unset <activity>.<slot> return a slot to its computed default
|
|
137
174
|
--write apply the change (default: preview only — writes nothing)
|
|
138
175
|
--json machine-readable output
|
|
139
176
|
--help, -h this help
|
|
140
177
|
|
|
141
|
-
Activities
|
|
142
|
-
|
|
178
|
+
Activities and their slots:
|
|
179
|
+
${ACTIVITY_LINES}
|
|
180
|
+
|
|
181
|
+
Accepted values per slot type:
|
|
182
|
+
${VALUE_LINES}
|
|
183
|
+
|
|
184
|
+
A carrier slot set to subagent needs the executor vehicle placed in this project — ${EXECUTOR_APPLY};
|
|
185
|
+
without it the slot resolves to solo with the reason stated. routine.parallel is a flag, not a
|
|
186
|
+
recipe: it never degrades.
|
|
143
187
|
|
|
144
188
|
Previews by default; --write applies via an atomic, symlink/TOCTOU-safe write behind a deployment gate.
|
|
145
189
|
Config writer only: it NEVER runs a backend and NEVER commits. Hand-editing the file stays fully supported.
|
|
@@ -152,7 +196,7 @@ Exit codes: 0 success (an explicit recipe that gracefully degrades is still 0);
|
|
|
152
196
|
|
|
153
197
|
export const main = (argv, ctx = {}) => {
|
|
154
198
|
const cwd = ctx.cwd ?? process.cwd();
|
|
155
|
-
const
|
|
199
|
+
const readinessDeps = { detect: ctx.detect, surveyVehicle: ctx.surveyVehicle };
|
|
156
200
|
const readFile = ctx.readFileSync ?? readFileSync;
|
|
157
201
|
const lstat = ctx.lstatSync ?? lstatSync;
|
|
158
202
|
const writeConfig = ctx.writeConfig ?? writeConfigFs;
|
|
@@ -170,23 +214,18 @@ export const main = (argv, ctx = {}) => {
|
|
|
170
214
|
return { code: 0, stdout: JSON.stringify(buildJson({ changed: [], unchanged: [], warnings: [], writtenPath: null, noop: true }), null, 2), stderr: '' };
|
|
171
215
|
}
|
|
172
216
|
const shown = current == null ? `(no ${CONFIG_REL} yet — computed defaults apply)` : serializeConfig(current).replace(/\n$/, '');
|
|
173
|
-
const hint = `\nPass --set <activity>.<slot>=<
|
|
217
|
+
const hint = `\nPass --set <activity>.<slot>=<value> (preview) then --write to apply. Activities/slots: ${QUALIFIED_SLOTS}.`;
|
|
174
218
|
return { code: 0, stdout: `${source === 'none' ? '' : `${CONFIG_REL}:\n`}${shown}${hint}`, stderr: '' };
|
|
175
219
|
}
|
|
176
220
|
|
|
177
|
-
|
|
221
|
+
// The merged config, then the _README refresh: a note that normalize-matches a KNOWN PRIOR canonical
|
|
222
|
+
// is replaced by the current one on a touched write, while a customized note stays untouched.
|
|
223
|
+
const after = refreshReadme(applySetOps(current, ops, { seedReadme: CANON_README })).config;
|
|
178
224
|
|
|
179
|
-
// Detection is a SECONDARY input — it only refines the EFFECTIVE recipe note. A throw must NOT block
|
|
180
|
-
// the write (the config write is readiness-independent): treat all backends as not-ready, warn, exit 0.
|
|
181
225
|
const warnings = [];
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
detection = detect();
|
|
185
|
-
} catch (err) {
|
|
186
|
-
warnings.push(`backend detection failed (${(err && err.message) || err}) — treating all backends as not ready; recipes needing a backend degrade to solo.`);
|
|
187
|
-
}
|
|
226
|
+
const readiness = composeReadinessOrWarn(cwd, readinessDeps, warnings);
|
|
188
227
|
|
|
189
|
-
const resolved = ops.map((op) => resolveOp(op, current, after,
|
|
228
|
+
const resolved = ops.map((op) => resolveOp(op, current, after, readiness));
|
|
190
229
|
const changed = resolved.filter((e) => e.from !== e.to);
|
|
191
230
|
const unchanged = resolved.filter((e) => e.from === e.to);
|
|
192
231
|
const noop = changed.length === 0;
|
|
@@ -220,7 +259,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
220
259
|
return { error: (err && err.message) || String(err) };
|
|
221
260
|
}
|
|
222
261
|
})();
|
|
223
|
-
const activeLine = composeActiveRecipeLine({ config: after, source: CONFIG_REL },
|
|
262
|
+
const activeLine = composeActiveRecipeLine({ config: after, source: CONFIG_REL }, readiness, autonomyFacts);
|
|
224
263
|
const stdout = json
|
|
225
264
|
? JSON.stringify(buildJson({ changed, unchanged, warnings, writtenPath, noop: false, activeLine }), null, 2)
|
|
226
265
|
: formatHuman({ changed, unchanged, warnings, wrote: true, fileBody, activeLine });
|
package/tools/view-model.mjs
CHANGED
|
@@ -54,7 +54,11 @@ const recipesVm = (r) => {
|
|
|
54
54
|
if (r.error) return { error: r.error };
|
|
55
55
|
const pairs = [];
|
|
56
56
|
for (const [activity, slots] of Object.entries(r.activities ?? {})) {
|
|
57
|
-
|
|
57
|
+
// source + degradedFrom ride along: an effective recipe alone cannot tell a chosen value from a
|
|
58
|
+
// computed default, nor a degrade from a configuration that really names the resolved recipe.
|
|
59
|
+
for (const [slot, v] of Object.entries(slots)) {
|
|
60
|
+
pairs.push({ key: `${activity}.${slot}`, recipe: v.recipe, source: v.source ?? null, degradedFrom: v.degradedFrom ?? null });
|
|
61
|
+
}
|
|
58
62
|
}
|
|
59
63
|
return { pairs, detectError: r.detectError ?? null };
|
|
60
64
|
};
|
|
@@ -76,8 +80,18 @@ const velocityVm = (v) => {
|
|
|
76
80
|
|
|
77
81
|
const agentsVm = (a) => {
|
|
78
82
|
if (!a) return null;
|
|
79
|
-
if (a.error) return { error: a.error };
|
|
80
|
-
|
|
83
|
+
if (a.error) return { error: a.error, executor: a.executor ?? null, executorReason: a.executorReason ?? null };
|
|
84
|
+
const bundled = a.bundled ?? 0;
|
|
85
|
+
return {
|
|
86
|
+
bundled,
|
|
87
|
+
placed: a.placed ?? 0,
|
|
88
|
+
// The envelope counts the read-only vehicles; an envelope predating the executor field bundled
|
|
89
|
+
// only read-only ones.
|
|
90
|
+
readOnly: a.readOnly ?? (a.executor == null ? bundled : Math.max(bundled - 1, 0)),
|
|
91
|
+
// null = an envelope predating the field (unknown), never a state.
|
|
92
|
+
executor: a.executor ?? null,
|
|
93
|
+
executorReason: a.executorReason ?? null,
|
|
94
|
+
};
|
|
81
95
|
};
|
|
82
96
|
|
|
83
97
|
const hookVm = (h) => {
|