@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
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
// idiom).
|
|
33
33
|
|
|
34
34
|
import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
|
|
35
|
-
import { createHash } from 'node:crypto';
|
|
36
35
|
import { homedir } from 'node:os';
|
|
37
36
|
import { dirname, join, resolve } from 'node:path';
|
|
38
37
|
import { fileURLToPath } from 'node:url';
|
|
@@ -52,7 +51,7 @@ import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-confi
|
|
|
52
51
|
import { deriveDoctorPlan } from './autonomy-doctor.mjs';
|
|
53
52
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
54
53
|
import { isDirectRun } from './direct-run.mjs';
|
|
55
|
-
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
54
|
+
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
56
55
|
import { surveyFamily, surveyGateHook, surveyAdrLayoutStrict } from './family-registry.mjs';
|
|
57
56
|
import { probeSandboxMasks, needsMasksApply } from './sandbox-masks.mjs';
|
|
58
57
|
import { shellQuoteArg } from './review-state.mjs';
|
|
@@ -83,7 +82,28 @@ import { loadConfig } from './orchestration-config.mjs';
|
|
|
83
82
|
import { DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
84
83
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
85
84
|
import { loadWorktreesConfig, resolveProbeDir } from './worktrees.mjs';
|
|
86
|
-
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';
|
|
88
|
+
// The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
|
|
89
|
+
// (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
|
|
90
|
+
import {
|
|
91
|
+
ACKS_FILE,
|
|
92
|
+
ACKS_LANE_KEY,
|
|
93
|
+
ACKS_WORKTREES_DIR_KEY,
|
|
94
|
+
ACKS_COVERAGE_DOMAIN_KEY,
|
|
95
|
+
ACKS_SOURCE_SIZE_COPY_KEY,
|
|
96
|
+
ACK_LANES,
|
|
97
|
+
factFingerprint,
|
|
98
|
+
readAckValue,
|
|
99
|
+
} from './ack-store.mjs';
|
|
100
|
+
import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
|
|
101
|
+
import { ENSURE_OPS } from './ensure-vocabulary.mjs';
|
|
102
|
+
|
|
103
|
+
// The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
|
|
104
|
+
const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
|
|
105
|
+
|
|
106
|
+
export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_KEY, ACKS_SOURCE_SIZE_COPY_KEY, ACK_LANES, factFingerprint };
|
|
87
107
|
|
|
88
108
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
89
109
|
const toolPath = (rel) => join(HERE, rel);
|
|
@@ -158,11 +178,20 @@ export const SEVERITIES = Object.freeze({
|
|
|
158
178
|
'mcp-channel.masked': SEVERITY_OPTIONAL,
|
|
159
179
|
'mcp-channel.differing': SEVERITY_ATTENTION,
|
|
160
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,
|
|
161
185
|
'family-freshness': SEVERITY_ATTENTION,
|
|
162
186
|
'adr-store-migration': SEVERITY_ATTENTION,
|
|
163
187
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
164
188
|
'sandbox-lane': SEVERITY_OPTIONAL,
|
|
165
189
|
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
190
|
+
// The layer is opt-in, so both arms are OFFERS under the frozen registry (attention is a CONFIGURED
|
|
191
|
+
// declaration that is broken): an absent store offers the seed, a store with no live contract offers
|
|
192
|
+
// the decline. Neither arm can leave the flow-optimal line standing — an offer is still an item.
|
|
193
|
+
'spec-adoption': SEVERITY_OPTIONAL,
|
|
194
|
+
'spec-adoption.adopting': SEVERITY_OPTIONAL,
|
|
166
195
|
});
|
|
167
196
|
// The per-item render tags (frozen presentation data, same language contract as the templates).
|
|
168
197
|
export const SEVERITY_LABELS = Object.freeze({
|
|
@@ -227,13 +256,16 @@ export const WHATS = Object.freeze({
|
|
|
227
256
|
'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
|
|
228
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',
|
|
229
258
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
230
|
-
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',
|
|
231
261
|
'family-freshness': '{parts}',
|
|
232
262
|
'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
|
|
233
263
|
'sandbox-masks': '{n} sandbox device mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
234
264
|
'sandbox-masks.stale-real': '{n} sandbox device mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
|
|
235
265
|
'sandbox-lane': 'the wired review wrappers declare a session-sandbox recipe (egress hosts + writable state dirs) not yet acknowledged for this project',
|
|
236
266
|
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
267
|
+
'spec-adoption': 'feature-spec store absent (docs/ai/specs) — no feature contract can govern a plan here yet; seed the store, or record the decline',
|
|
268
|
+
'spec-adoption.adopting': 'feature-spec store: {n} draft spec(s), no live contract — nothing governs a plan through it yet; land a live contract, or record the decline',
|
|
237
269
|
});
|
|
238
270
|
|
|
239
271
|
// ── the shape contract (D2): registry strings AND composed items stay one line under the cap ────
|
|
@@ -284,12 +316,14 @@ export const BENEFITS = Object.freeze({
|
|
|
284
316
|
'read-lane': 'velocity — pipes/chains of your seeded read-only commands auto-approve instead of prompting (opt-in, conservatively classified)',
|
|
285
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',
|
|
286
318
|
'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
|
|
287
|
-
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',
|
|
288
321
|
'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
|
|
289
322
|
'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
|
|
290
323
|
'sandbox-masks': 'zero clutter — git status shows only your changes (the review domain already ignores the masks by construction)',
|
|
291
324
|
'sandbox-lane': 'discoverability — the manifest-declared observed sandbox recipe for bridge runs surfaces itself instead of waiting to be asked',
|
|
292
325
|
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
326
|
+
'spec-adoption': 'contracts — a plan names the contract it builds to, and a change to a governed slice is visible at review instead of after it',
|
|
293
327
|
});
|
|
294
328
|
|
|
295
329
|
// ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
|
|
@@ -332,12 +366,19 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
332
366
|
{ id: 'mcp-channel', mode: 'mcp', advisorKey: 'mcp-channel' },
|
|
333
367
|
{ id: 'worktrees-dir', mode: 'worktrees', advisorKey: 'worktrees-dir' },
|
|
334
368
|
{ id: 'family-freshness', mode: 'upgrade', advisorKey: 'family-freshness' },
|
|
369
|
+
// The feature-spec layer is delivered by upgrade's spec-layer ensure (there is no specs mode), so
|
|
370
|
+
// its adoption state is declared where the store is seeded.
|
|
371
|
+
{ id: 'spec-adoption', mode: 'upgrade', advisorKey: 'spec-adoption' },
|
|
335
372
|
{ id: 'adr-store-migration', mode: 'migrate-adr-store', advisorKey: 'adr-store-migration' },
|
|
336
373
|
{ id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
337
374
|
// The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
|
|
338
375
|
// which is why the review-recipe benefit is worded for either slot rather than for review alone.
|
|
339
376
|
{ id: 'delegated-execution', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
340
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' },
|
|
341
382
|
// Exempt, not un-audited. `acceptEdits` auto-applies Edit/Write and auto-runs mkdir/touch/mv/cp:
|
|
342
383
|
// a TRUST-POSTURE change. The kit never nudges a user toward weakening their approval posture (the
|
|
343
384
|
// same doctrine that keeps sandbox network/filesystem allowances HAND-APPLY); velocity presents the
|
|
@@ -448,11 +489,12 @@ const probeReviewRecipe = ({ root, deps, add, skip }) => {
|
|
|
448
489
|
// The VALIDATED reader (Segment B): a schema-invalid config (unknown activity/slot,
|
|
449
490
|
// bad recipe) throws here and becomes a stated skip — raw JSON.parse would silently ignore it.
|
|
450
491
|
const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
451
|
-
const
|
|
492
|
+
const readiness = composeReadiness(root, { ...deps, detect: deps.detect ?? (() => detectBackends(deps)), onDetectError: (err) => { throw err; } });
|
|
452
493
|
const degraded = [];
|
|
453
494
|
for (const [activity, def] of Object.entries(ACTIVITIES)) {
|
|
454
495
|
for (const slot of Object.keys(def.slots)) {
|
|
455
|
-
|
|
496
|
+
if (config?.[activity]?.[slot] === 'subagent') continue; // the executor vehicle has its own probe
|
|
497
|
+
const r = resolveActivityRecipe({ config, readiness, activity, slot });
|
|
456
498
|
if (r.degradedFrom) degraded.push(`${activity}.${slot}: configured ${r.degradedFrom} degrades to ${r.recipe} (${r.reason})`);
|
|
457
499
|
}
|
|
458
500
|
}
|
|
@@ -923,11 +965,9 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
923
965
|
// skipped the per-vehicle plan the user is supposed to see before consenting.
|
|
924
966
|
// The hidden-mode reconcile rides the detail, never the apply line: it is wrong to run on a
|
|
925
967
|
// VISIBLE deployment, and the apply slot must stay one pure executable command.
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
`node ${q(toolPath('cheap-agents.mjs'))} --cwd ${q(root)}`,
|
|
930
|
-
'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',
|
|
931
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`,
|
|
932
972
|
);
|
|
933
973
|
} catch (err) {
|
|
@@ -935,6 +975,60 @@ const probeCheapAgents = ({ root, deps, add, skip }) => {
|
|
|
935
975
|
}
|
|
936
976
|
};
|
|
937
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
|
+
|
|
938
1032
|
const probeFamilyFreshness = ({ deps, add, skip }) => {
|
|
939
1033
|
try {
|
|
940
1034
|
const survey = deps.surveyFamily ?? surveyFamily;
|
|
@@ -1042,41 +1136,13 @@ export const recipeFingerprint = ({ hosts, dirs, home }) => {
|
|
|
1042
1136
|
if (abs === homeAbs) return '~';
|
|
1043
1137
|
return abs.startsWith(`${homeAbs}/`) ? `~/${abs.slice(homeAbs.length + 1)}` : abs;
|
|
1044
1138
|
};
|
|
1045
|
-
|
|
1046
|
-
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
1139
|
+
return factFingerprint(JSON.stringify({ hosts: [...hosts].sort(), dirs: [...new Set(dirs.map(norm))].sort() }));
|
|
1047
1140
|
};
|
|
1048
1141
|
|
|
1049
|
-
// The
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
// is churn (the census binds the verdict + extension set, never per-file counts).
|
|
1054
|
-
export const factFingerprint = (fact) => createHash('sha256').update(fact).digest('hex').slice(0, 16);
|
|
1055
|
-
|
|
1056
|
-
// The kit-owned neutral ack store (D4; AD-055 Part I): a FAMILY-OWNED strict-JSON file no host
|
|
1057
|
-
// validator guards — top-level key `sandboxLaneAck` (+ optional `_README`), unknown keys tolerated
|
|
1058
|
-
// on read (future acks are siblings). This is the PRIMARY ack channel; the legacy settings-scope
|
|
1059
|
-
// keys below are read for one deprecation window. The sandbox/permissions security keys are NEVER
|
|
1060
|
-
// consulted as an ack.
|
|
1061
|
-
export const ACKS_FILE = 'docs/ai/acks.json';
|
|
1062
|
-
export const ACKS_LANE_KEY = 'sandboxLaneAck';
|
|
1063
|
-
export const ACKS_WORKTREES_DIR_KEY = 'worktreesDirAck';
|
|
1064
|
-
export const ACKS_COVERAGE_DOMAIN_KEY = 'coverageDomainAck';
|
|
1065
|
-
export const ACKS_SOURCE_SIZE_COPY_KEY = 'sourceSizeCopyAck';
|
|
1066
|
-
// The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's
|
|
1067
|
-
// command line → the store key that writer sets. A lane the registry does not name is a usage
|
|
1068
|
-
// refusal at the writer, never a newly-invented key in the shared store.
|
|
1069
|
-
//
|
|
1070
|
-
// An ack lane exists for a state the maintainer can only ANSWER, never converge: a tracked tree the
|
|
1071
|
-
// coverage domain cannot reach, a checker deliberately vendored elsewhere. It is deliberately NOT
|
|
1072
|
-
// available to a state that is simply BROKEN — a dead checker/producer pair is fixed, not
|
|
1073
|
-
// acknowledged, so no lane names it.
|
|
1074
|
-
export const ACK_LANES = Object.freeze({
|
|
1075
|
-
'sandbox-lane': ACKS_LANE_KEY,
|
|
1076
|
-
'worktrees-dir': ACKS_WORKTREES_DIR_KEY,
|
|
1077
|
-
'coverage-domain': ACKS_COVERAGE_DOMAIN_KEY,
|
|
1078
|
-
'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
|
|
1079
|
-
});
|
|
1142
|
+
// The ack store (D4; AD-055 Part I) is the kit-owned PRIMARY ack channel; the legacy settings-scope
|
|
1143
|
+
// keys below are read for one deprecation window. An ack lane exists for a state the maintainer can
|
|
1144
|
+
// only ANSWER, never converge — a dead checker/producer pair is fixed, not acknowledged, so no lane
|
|
1145
|
+
// names it. The store's path, keys, lane registry and reader are ack-store.mjs (re-exported above).
|
|
1080
1146
|
|
|
1081
1147
|
// The opt-in read-lane toggle file (AD-055 Part II) — the SAME kit-owned docs/ai/lanes.json the
|
|
1082
1148
|
// placed hook reads live. The read-lane item offers to enable it once the hook is placed+wired.
|
|
@@ -1146,38 +1212,6 @@ const declarationCarriesMarker = (root, deps) => {
|
|
|
1146
1212
|
export const SANDBOX_LANE_ACK_PARENT = 'agentWorkflow';
|
|
1147
1213
|
export const SANDBOX_LANE_ACK_KEY = 'sandboxLaneAck';
|
|
1148
1214
|
|
|
1149
|
-
// Read the family-owned ack store. An ABSENT file (or absent docs/ai) is the NORMAL not-yet-acked
|
|
1150
|
-
// state → null (plain fall-through, never a skip). A parse/IO error on an EXISTING file THROWS — the
|
|
1151
|
-
// probe's catch turns it into a stated skip line (Decisions 2). A non-object root is a malformed
|
|
1152
|
-
// store (fail-closed skip); a non-string value at the key is tolerated → null (the item re-fires).
|
|
1153
|
-
// The WHOLE path chain (root / docs / ai / acks.json) is guarded WITHOUT following symlinks
|
|
1154
|
-
// BEFORE any read: a symlinked ANCESTOR could otherwise read an ack from OUTSIDE the project (the
|
|
1155
|
-
// writer refuses such a deployment — the reader must too), a symlinked/dangling LEAF must not read as
|
|
1156
|
-
// not-yet-acked, and a non-regular target (FIFO/dir/device) is a fail-closed SKIP — never read it (a
|
|
1157
|
-
// FIFO would BLOCK the advisor). ENOENT-safe: an absent file/dir is the NORMAL not-yet-acked null.
|
|
1158
|
-
const readAckValue = (root, deps, ackKey) => {
|
|
1159
|
-
const readFile = deps.readFile ?? readFileSync;
|
|
1160
|
-
const lstat = deps.lstat ?? lstatSync;
|
|
1161
|
-
const absPath = join(root, ACKS_FILE);
|
|
1162
|
-
let st;
|
|
1163
|
-
try {
|
|
1164
|
-
assertContainedRealPath(root, absPath, { lstat }); // symlinked root/ancestor/leaf or escape → throws
|
|
1165
|
-
st = lstat(absPath);
|
|
1166
|
-
} catch (err) {
|
|
1167
|
-
if (err?.code === 'ENOENT') return null; // genuinely absent (file or docs/ai) — normal not-yet-acked
|
|
1168
|
-
throw err; // a symlinked ancestor/leaf, an escape, or a real IO error — stated skip
|
|
1169
|
-
}
|
|
1170
|
-
if (!st.isFile()) {
|
|
1171
|
-
throw new Error(`${ACKS_FILE} is not a regular file — refusing to read it`);
|
|
1172
|
-
}
|
|
1173
|
-
const parsed = JSON.parse(readFile(absPath, 'utf8'));
|
|
1174
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1175
|
-
throw new Error(`${ACKS_FILE}: expected a JSON object`);
|
|
1176
|
-
}
|
|
1177
|
-
const value = parsed[ackKey];
|
|
1178
|
-
return typeof value === 'string' ? value : null;
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
1215
|
// Read the opt-in read-lane toggle for the read-lane item. An ABSENT file (or absent docs/ai) →
|
|
1182
1216
|
// false (the lane is off — offer it). `readLane === true` → enabled (converged). A parse/IO error on
|
|
1183
1217
|
// an EXISTING file, a symlinked ancestor/leaf, an escape, or a non-object root THROWS — the probe
|
|
@@ -1208,7 +1242,38 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
1208
1242
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
1209
1243
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
1210
1244
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
1211
|
-
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel']);
|
|
1245
|
+
export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption']);
|
|
1246
|
+
|
|
1247
|
+
// The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
|
|
1248
|
+
// zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
|
|
1249
|
+
// started — an owner found the store absent only by asking. The survey reads the store through
|
|
1250
|
+
// spec-check's own census; a recorded decline (the `spec-adoption` ack lane) is the fact that
|
|
1251
|
+
// silences the item, and an unreadable store is a stated skip so the flow-optimal line never renders
|
|
1252
|
+
// over it. The not-adopted apply is the spec-layer ensure; the decline preview rides the recipe line.
|
|
1253
|
+
export const probeSpecAdoption = ({ root, deps, add, skip }) => {
|
|
1254
|
+
try {
|
|
1255
|
+
const survey = surveySpecAdoption(root, deps);
|
|
1256
|
+
if (survey.state === ADOPTION.UNREADABLE) {
|
|
1257
|
+
skip('spec-adoption', new Error(`${survey.reason} — the adoption state under ${SPEC_STORE_DIR_REL} cannot be judged`));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (survey.state === ADOPTION.ADOPTED || readDeclineAck(root, deps)) return;
|
|
1261
|
+
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane ${SPEC_ADOPTION_LANE} --fingerprint ${declineFingerprint()} --cwd ${q(root)}`;
|
|
1262
|
+
if (survey.state === ADOPTION.NOT_ADOPTED) {
|
|
1263
|
+
add(
|
|
1264
|
+
'spec-adoption',
|
|
1265
|
+
fillTemplate(WHATS['spec-adoption'], {}),
|
|
1266
|
+
`node ${q(toolPath('ensure-configs.mjs'))} --reconcile --only ${SPEC_LAYER_ENSURE} --cwd ${q(root)}`,
|
|
1267
|
+
'spec-adoption',
|
|
1268
|
+
`HAND-APPLY alternative (instead of the apply, never after it): decline the layer by recording it — ${decline}`,
|
|
1269
|
+
);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
add('spec-adoption', fillTemplate(WHATS['spec-adoption.adopting'], { n: survey.draft }), decline, 'spec-adoption.adopting');
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
skip('spec-adoption', err);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1212
1277
|
|
|
1213
1278
|
const probeSandboxLane = ({ root, deps, add, skip }) => {
|
|
1214
1279
|
try {
|
|
@@ -1485,12 +1550,14 @@ const PROBES = Object.freeze([
|
|
|
1485
1550
|
probeReadLane,
|
|
1486
1551
|
probeStateBlockHook,
|
|
1487
1552
|
probeCheapAgents,
|
|
1553
|
+
probeExecutorVehicle,
|
|
1488
1554
|
probeFamilyFreshness,
|
|
1489
1555
|
probeAdrStore,
|
|
1490
1556
|
probeMasksItem,
|
|
1491
1557
|
probeSandboxLane,
|
|
1492
1558
|
probeWorktreesDir,
|
|
1493
1559
|
probeMcpChannel,
|
|
1560
|
+
probeSpecAdoption,
|
|
1494
1561
|
]);
|
|
1495
1562
|
|
|
1496
1563
|
export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
package/tools/renderers.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Pure, no side effects, Node >= 22.
|
|
9
9
|
|
|
10
10
|
import { BLOCK_TITLES, SETTINGS_LABELS, glyphsFor, NO_DEPLOYMENT } from './presentation.mjs';
|
|
11
|
+
import { describeAdoption } from './spec-adoption.mjs';
|
|
11
12
|
|
|
12
13
|
const MEMBER_COL = 20;
|
|
13
14
|
const VERSION_COL = 12;
|
|
@@ -19,6 +20,10 @@ const SETTINGS_COL = 14;
|
|
|
19
20
|
// checks, so a future token joins the render by joining this line.
|
|
20
21
|
const ACTIONABLE_ADR_LAYOUTS = Object.freeze(['old', 'old-unrotated']);
|
|
21
22
|
|
|
23
|
+
const DEGRADE_ARROW = '←';
|
|
24
|
+
const EMPTY_CELL = '—';
|
|
25
|
+
const UNKNOWN_EXECUTOR = 'unknown';
|
|
26
|
+
|
|
22
27
|
const SGR = Object.freeze({ bold: '\x1b[1m', reset: '\x1b[0m' });
|
|
23
28
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
24
29
|
export const visibleLength = (s) => s.replace(ANSI_RE, '').length;
|
|
@@ -96,6 +101,14 @@ const renderProject = (vm, { color }) => {
|
|
|
96
101
|
if (ACTIONABLE_ADR_LAYOUTS.includes(p.adrLayout)) {
|
|
97
102
|
lines.push(` ${pad('ADR store', STAMP_COL)}old layout — run /agent-workflow-kit migrate-adr-store`);
|
|
98
103
|
}
|
|
104
|
+
// Every state renders — an owner opens this surface deliberately, so "not adopted" is the one line
|
|
105
|
+
// that must never be missing; an envelope without the field says so rather than inventing a state.
|
|
106
|
+
if (p.specs) {
|
|
107
|
+
const declineNote = p.specs.declineError ? ` (decline ack unreadable: ${p.specs.declineError})` : '';
|
|
108
|
+
lines.push(` ${pad('specs', STAMP_COL)}${describeAdoption(p.specs, { declined: p.specs.declined })}${declineNote}`);
|
|
109
|
+
} else {
|
|
110
|
+
lines.push(` ${pad('specs', STAMP_COL)}unknown — the installed kit predates the adoption state`);
|
|
111
|
+
}
|
|
99
112
|
if (p.visibility) {
|
|
100
113
|
const v = p.visibility.error ? `error: ${p.visibility.error}` : p.visibility.phrase;
|
|
101
114
|
lines.push(` ${pad('visibility', STAMP_COL)}${v}`);
|
|
@@ -107,13 +120,22 @@ const renderSettings = (vm, { color, glyph }) => {
|
|
|
107
120
|
const s = vm.project?.settings;
|
|
108
121
|
if (!s) return [];
|
|
109
122
|
const lines = ['', heading(BLOCK_TITLES.settings, color)];
|
|
110
|
-
// 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.
|
|
111
126
|
if (s.recipes?.error) lines.push(` ${pad(SETTINGS_LABELS.recipes, SETTINGS_COL)}error: ${s.recipes.error}`);
|
|
112
127
|
else if (s.recipes) {
|
|
113
|
-
const
|
|
114
|
-
|
|
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
|
+
});
|
|
115
137
|
if (s.recipes.detectError) {
|
|
116
|
-
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`);
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
141
|
// attribution — effective includeCoAuthoredBy; a real local override is called out.
|
|
@@ -127,10 +149,17 @@ const renderSettings = (vm, { color, glyph }) => {
|
|
|
127
149
|
else if (s.velocity) {
|
|
128
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}`);
|
|
129
151
|
}
|
|
130
|
-
// cheap agents — the kit-placed .claude/agents/ vehicles: placed count vs the bundle
|
|
131
|
-
|
|
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
|
+
}
|
|
132
159
|
else if (s.agents) {
|
|
133
|
-
|
|
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}`);
|
|
134
163
|
}
|
|
135
164
|
// gate hook — the opt-in PreToolUse gate-approval hook: wired / file placed / declaration present /
|
|
136
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);
|