@sabaiway/agent-workflow-kit 13.3.0 → 14.1.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 +86 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/agents.md +4 -4
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/dispatch.md +3 -3
- package/references/modes/recommendations.md +1 -1
- package/references/modes/review-state.md +1 -1
- package/references/modes/upgrade.md +6 -4
- package/references/templates/vehicles.json +1 -1
- package/tools/cheap-agents-read.mjs +37 -3
- package/tools/cheap-agents.mjs +44 -13
- package/tools/dispatch-record.mjs +8 -2
- package/tools/dispatch-store-read.mjs +35 -1
- package/tools/dispatch-store.mjs +8 -1
- package/tools/dispatch.mjs +25 -4
- package/tools/ensure-configs.mjs +15 -11
- package/tools/ensure-executor.mjs +87 -0
- package/tools/ensure-ops.mjs +2 -2
- package/tools/ensure-vocabulary.mjs +10 -3
- package/tools/held-session.mjs +57 -18
- package/tools/recommendations.mjs +15 -9
- package/tools/task-thread.mjs +70 -0
- package/tools/upgrade-runlist.mjs +2 -0
package/tools/cheap-agents.mjs
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
// only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it
|
|
8
8
|
// never commits. `surveyExecutorVehicle` is that vehicle's readiness, for the subagent carrier.
|
|
9
9
|
//
|
|
10
|
-
// The family's second `.claude/` writer, the velocity-profile.mjs
|
|
10
|
+
// The family's second `.claude/` writer, the velocity-profile.mjs discipline with an executor exception:
|
|
11
11
|
// • preview-then-mutate — `--dry-run` is the DEFAULT and writes nothing; `--apply` writes;
|
|
12
12
|
// • deployment-gated — `--apply` STOPs unless docs/ai/.workflow-version equals the lineage
|
|
13
13
|
// head (a dry-run stays usable whatever the stamp says; an unreadable orchestration config
|
|
14
14
|
// STOPs both, since the derived lenses it names cannot be known);
|
|
15
15
|
// • symlink-safe — a symlinked `.claude` / `.claude/agents` / target file is a STOP, never a
|
|
16
16
|
// write-through;
|
|
17
|
-
// •
|
|
18
|
-
//
|
|
17
|
+
// • preserves differing .claude/agents/ files (`customized — preserved`), except the executor:
|
|
18
|
+
// on apply its body is re-derived from docs/ai/vehicles.json, replacing a hand edit;
|
|
19
19
|
// an identical file is `already current` (idempotent re-run);
|
|
20
20
|
// • writes ONLY under .claude/agents/ — never settings.json / settings.local.json, never
|
|
21
21
|
// commits.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Exit codes: 0 done / dry-run (incl. preserved customizations — a user's file is a legitimate
|
|
29
29
|
// state, not an error); 1 precondition STOP (stamp, symlink, missing bundle, an unreadable
|
|
30
|
-
// orchestration config —
|
|
30
|
+
// orchestration config or docs/ai/vehicles.json — their derived bodies cannot be known); 2 usage.
|
|
31
31
|
// Dependency-free, Node >= 22. No side effects on import.
|
|
32
32
|
|
|
33
33
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
@@ -38,6 +38,9 @@ import { shellQuoteArg } from './repo-lex.mjs';
|
|
|
38
38
|
import { loadConfig } from './orchestration-config.mjs';
|
|
39
39
|
import { lensMembersOf } from './review-roster.mjs';
|
|
40
40
|
import { deriveLensTemplate, lensVehicleSpec } from './review-roster-resolve.mjs';
|
|
41
|
+
import { loadVehicles, resolveExecutor } from './vehicle-settings.mjs';
|
|
42
|
+
import { safeLine } from './carriers.mjs';
|
|
43
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
41
44
|
// The READ core, never a second copy: the bundle, the placement plan and the executor survey live
|
|
42
45
|
// there so the read-only advisor graph can reach them without reaching this writer.
|
|
43
46
|
import {
|
|
@@ -47,9 +50,12 @@ import {
|
|
|
47
50
|
EXPECTED_WORKFLOW_VERSION,
|
|
48
51
|
UTF8,
|
|
49
52
|
CHEAP_AGENTS_STAMP,
|
|
53
|
+
CHEAP_AGENTS_SYMLINK,
|
|
50
54
|
CHEAP_AGENTS_BUNDLE,
|
|
51
55
|
CHEAP_AGENTS_CONFIG,
|
|
56
|
+
CHEAP_AGENTS_VEHICLES,
|
|
52
57
|
EXECUTOR_VEHICLE,
|
|
58
|
+
executorTemplate,
|
|
53
59
|
makeCheapAgentsError,
|
|
54
60
|
readFsDeps,
|
|
55
61
|
readBundledAgents,
|
|
@@ -68,6 +74,10 @@ export {
|
|
|
68
74
|
CHEAP_AGENTS_SYMLINK,
|
|
69
75
|
CHEAP_AGENTS_BUNDLE,
|
|
70
76
|
CHEAP_AGENTS_CONFIG,
|
|
77
|
+
CHEAP_AGENTS_VEHICLES,
|
|
78
|
+
readExecutorPosture,
|
|
79
|
+
executorTemplate,
|
|
80
|
+
deriveExecutorBody,
|
|
71
81
|
makeCheapAgentsError,
|
|
72
82
|
readBundledAgents,
|
|
73
83
|
planPlacement,
|
|
@@ -96,7 +106,9 @@ authoring, or write-capable routine slice the orchestrator verifies, never for r
|
|
|
96
106
|
it never commits.
|
|
97
107
|
Default is --dry-run (a preview; writes nothing). --apply writes.
|
|
98
108
|
Configured derived review lenses are planned and placed beside the bundled vehicles.
|
|
99
|
-
An existing file with DIFFERENT content is preserved and reported, never overwritten
|
|
109
|
+
An existing file with DIFFERENT content is preserved and reported, never overwritten, except the executor.
|
|
110
|
+
The executor's model and effort come from docs/ai/vehicles.json, and a placed executor whose bytes differ
|
|
111
|
+
from the derived body is re-derived on --apply (a hand edit is replaced, never preserved).`;
|
|
100
112
|
|
|
101
113
|
export const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
102
114
|
|
|
@@ -127,6 +139,14 @@ const loadConfigOrStop = (cwd, deps) => {
|
|
|
127
139
|
}
|
|
128
140
|
};
|
|
129
141
|
|
|
142
|
+
const loadExecutorPostureOrStop = (cwd, deps) => {
|
|
143
|
+
try {
|
|
144
|
+
return resolveExecutor(loadVehicles(cwd, deps.readFile, deps.lstat)).posture;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw makeCheapAgentsError(CHEAP_AGENTS_VEHICLES, `${err?.message ?? err} — the agents writer cannot derive the executor vehicle — nothing is placed`);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
130
150
|
const configuredDerivedTemplates = (cwd, deps) => {
|
|
131
151
|
const config = loadConfigOrStop(cwd, deps);
|
|
132
152
|
const bundle = readBundledAgents(deps);
|
|
@@ -149,9 +169,10 @@ export const applyCheapAgentsCommand = (root) =>
|
|
|
149
169
|
export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
150
170
|
const fs = writeFsDeps(deps);
|
|
151
171
|
const projectDir = cwd ?? process.cwd();
|
|
152
|
-
const
|
|
172
|
+
const posture = loadExecutorPostureOrStop(projectDir, deps);
|
|
173
|
+
const derived = [executorTemplate(posture, deps), ...configuredDerivedTemplates(projectDir, deps)];
|
|
153
174
|
const preflight = preflightCheapAgents({ cwd: projectDir, derived }, deps);
|
|
154
|
-
if (dryRun) return { wrote: false, dryRun: true, ...preflight };
|
|
175
|
+
if (dryRun) return { wrote: false, dryRun: true, posture, ...preflight };
|
|
155
176
|
|
|
156
177
|
if (!preflight.stampOk) {
|
|
157
178
|
throw makeCheapAgentsError(
|
|
@@ -159,10 +180,16 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
159
180
|
`not a deployed agent-workflow project at lineage ${EXPECTED_WORKFLOW_VERSION} (found ${preflight.stamp ?? 'none'}) — run init/upgrade first`,
|
|
160
181
|
);
|
|
161
182
|
}
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
for (const item of
|
|
165
|
-
|
|
183
|
+
const toWrite = preflight.plan.filter((item) => item.action === 'place' || item.action === 're-derive');
|
|
184
|
+
if (toWrite.length > 0) fs.mkdir(join(preflight.projectDir, AGENTS_DIR), { recursive: true });
|
|
185
|
+
for (const item of toWrite) {
|
|
186
|
+
if (item.action === 're-derive') {
|
|
187
|
+
writeContainedFileAtomic(preflight.projectDir, item.abs, item.content, deps, {
|
|
188
|
+
label: item.rel, stop: (message) => makeCheapAgentsError(CHEAP_AGENTS_SYMLINK, message),
|
|
189
|
+
});
|
|
190
|
+
} else fs.writeFile(item.abs, item.content, UTF8);
|
|
191
|
+
}
|
|
192
|
+
return { wrote: toWrite.length > 0, dryRun: false, posture, ...preflight };
|
|
166
193
|
};
|
|
167
194
|
|
|
168
195
|
// ── report ────────────────────────────────────────────────────────────────────────────
|
|
@@ -170,17 +197,20 @@ export const writeCheapAgents = ({ cwd, dryRun = true } = {}, deps = {}) => {
|
|
|
170
197
|
const ACTION_LABEL = {
|
|
171
198
|
place: 'place',
|
|
172
199
|
'already-current': 'already current',
|
|
200
|
+
're-derive': 're-derived — a hand edit was replaced; set the model in docs/ai/vehicles.json',
|
|
173
201
|
'customized-preserved': 'customized — preserved (delete the file to reseed from the bundle)',
|
|
174
202
|
};
|
|
175
203
|
|
|
176
204
|
export const formatResult = (result) => {
|
|
205
|
+
const { posture } = result;
|
|
177
206
|
const lines = [
|
|
178
207
|
result.dryRun
|
|
179
208
|
? 'agent-workflow subagent vehicles — DRY RUN (no changes)'
|
|
180
209
|
: 'agent-workflow subagent vehicles — APPLY',
|
|
181
210
|
];
|
|
182
211
|
for (const item of result.plan) {
|
|
183
|
-
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
212
|
+
const verb = result.dryRun && item.action === 'place' ? 'would place'
|
|
213
|
+
: result.dryRun && item.action === 're-derive' ? 'would re-derive' : ACTION_LABEL[item.action];
|
|
184
214
|
lines.push(` - ${item.rel}: ${verb}`);
|
|
185
215
|
}
|
|
186
216
|
if (!result.stampOk) {
|
|
@@ -191,11 +221,12 @@ export const formatResult = (result) => {
|
|
|
191
221
|
`${readOnlyCount} vehicles are Claude Code subagents with READ-ONLY tools and NO shell as bundled or derived (a customized file keeps whatever it grants) — so a fan-out on the shipped templates can never turn into a wave of approval prompts.`,
|
|
192
222
|
`three of those ride the cheap lane (model: haiku, effort: low) for mechanical work; ${FALLBACK_LENS_ADDITIONAL_ONLY}`,
|
|
193
223
|
'executor is the one FULL-TOOL vehicle: dispatched only for a bounded execution, authoring, or write-capable routine slice the orchestrator verifies, never for read-only work, and it never commits.',
|
|
224
|
+
`executor posture: model=${safeLine(posture.model)} effort=${safeLine(posture.effort)} fallback=${safeLine(posture.fallback)} (source: ${safeLine(posture.source)}) — set in docs/ai/vehicles.json`,
|
|
194
225
|
);
|
|
195
226
|
// A preview must print the EXACT command that applies it. The advisor renders this dry-run as an
|
|
196
227
|
// item's one-liner, and that flow's contract is "run the printed command, no improvisation" — a
|
|
197
228
|
// bare "re-run with --apply" would leave the caller to reconstruct --cwd and its quoting.
|
|
198
|
-
if (result.dryRun && result.plan.some((item) => item.action === 'place')) {
|
|
229
|
+
if (result.dryRun && result.plan.some((item) => item.action === 'place' || item.action === 're-derive')) {
|
|
199
230
|
lines.push(`to apply, run exactly: ${applyCheapAgentsCommand(result.projectDir)}`);
|
|
200
231
|
}
|
|
201
232
|
if (!result.dryRun && result.wrote) {
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
|
|
77
77
|
import { createHash } from 'node:crypto';
|
|
78
78
|
import { SAFE_NONCE_RE, flowCanonicalSerialization } from './flow-record.mjs';
|
|
79
|
+
import { validateTask } from './task-thread.mjs';
|
|
79
80
|
|
|
80
81
|
const deepFreeze = (value) => {
|
|
81
82
|
if (value !== null && typeof value === 'object') {
|
|
@@ -133,7 +134,7 @@ const short = (v) => {
|
|
|
133
134
|
export const DELEGATION_SCHEMA_VERSION = 1;
|
|
134
135
|
export const BASELINE_KINDS = deepFreeze(['head', 'checkpoint']);
|
|
135
136
|
export const HEAD_BASELINE = deepFreeze({ kind: 'head', treeOid: null });
|
|
136
|
-
export const OPTIONAL_FIELDS = deepFreeze({ dispatch: ['baseline'] });
|
|
137
|
+
export const OPTIONAL_FIELDS = deepFreeze({ dispatch: ['baseline', 'task'] });
|
|
137
138
|
const TREE_OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
138
139
|
export const isTreeOid = (v) => typeof v === 'string' && TREE_OID_RE.test(v);
|
|
139
140
|
export const readsBaseline = (dispatch) => Object.hasOwn(dispatch, 'baseline')
|
|
@@ -145,7 +146,7 @@ export const DELEGATION_KINDS = deepFreeze(['pre-registration', 'dispatch', 'ret
|
|
|
145
146
|
// The closed key set per kind (schema + kind are implicit on every record).
|
|
146
147
|
export const DELEGATION_KEY_SETS = deepFreeze({
|
|
147
148
|
'pre-registration': ['waveId', 'stepClasses', 'pairingKey', 'minPerClass', 'meanLThreshold', 'firstPassNum', 'firstPassDen', 'timestamp'],
|
|
148
|
-
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp', 'baseline'],
|
|
149
|
+
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp', 'baseline', 'task'],
|
|
149
150
|
return: ['role', 'backend', 'nonce', 'contractDigest', 'preTreeDigest', 'postTreeDigest', 'diffDigest', 'diffLength', 'reportDigest', 'reportLength', 'bundleDigest', 'bundleLength', 'metric', 'outcome', 'exitStatus', 'sessionId', 'wrapperVersion', 'posture', 'timestamp'],
|
|
150
151
|
fold: ['nonce', 'returnDigest', 'treeDigestAtFold', 'verdict', 'timestamp'],
|
|
151
152
|
observation: ['waveId', 'stepClass', 'scope', 'metric', 'planId', 'phase', 'timestamp'],
|
|
@@ -346,6 +347,7 @@ const FIELD_CHECKS = {
|
|
|
346
347
|
firstPassDen: { ok: (v) => Number.isSafeInteger(v) && v >= 1, want: 'a positive integer first-pass denominator' },
|
|
347
348
|
vehicle: { ok: isPlainObject, want: 'the closed vehicle pair {requested, selected}' },
|
|
348
349
|
baseline: { ok: isPlainObject, want: 'the closed baseline pair {kind, treeOid}' },
|
|
350
|
+
task: { ok: isPlainObject, want: 'the closed task object {brief, files}' },
|
|
349
351
|
// The safe token grammar, not merely non-empty: the backend NAMES an artifact beside the store
|
|
350
352
|
// (the exec receipt and its report), so a name outside this grammar records a dispatch whose own
|
|
351
353
|
// receipt could never be written. One grammar on both sides, or the ledger accepts what the
|
|
@@ -519,6 +521,10 @@ export const validateDelegationRecord = (record) => {
|
|
|
519
521
|
const baseline = validateBaseline('dispatch: baseline', record.baseline);
|
|
520
522
|
if (!baseline.ok) return baseline;
|
|
521
523
|
}
|
|
524
|
+
if (Object.hasOwn(record, 'task')) {
|
|
525
|
+
const closed = checkClosedKeys('dispatch: task', record.task, ['brief', 'files']); if (!closed.ok) return closed;
|
|
526
|
+
const task = validateTask(record.task, readsBaseline(record).kind); if (!task.ok) return refuse(`dispatch: task: ${task.reason}`);
|
|
527
|
+
}
|
|
522
528
|
return vehicle.ok ? validateDispatchCrossFields(record) : vehicle;
|
|
523
529
|
}
|
|
524
530
|
if (record.kind === 'return') return validateReturnCrossFields(record);
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
} from './dispatch-record.mjs';
|
|
8
8
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
9
9
|
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
10
|
+
import { readsTask, readsInsideEpoch, epochClaims } from './task-thread.mjs';
|
|
11
|
+
import { hasPathOverlap } from './claim-relation.mjs';
|
|
12
|
+
import { HELD_RECEIPT_BACKEND } from './held-session.mjs';
|
|
10
13
|
|
|
11
14
|
export const DELEGATION_STORE_STOP = 'DELEGATION_STORE_STOP';
|
|
12
15
|
export const delegationStoreStop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'DelegationStoreStop', code: DELEGATION_STORE_STOP });
|
|
@@ -127,7 +130,34 @@ const describeClosure = (last) =>
|
|
|
127
130
|
|
|
128
131
|
const WAVE_SCOPED_KINDS = ['dispatch', 'observation', 'degrade'];
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
const checkTaskClaims = (records, snapshot, head) => {
|
|
134
|
+
if (head.state === 'error') throw stop(`${head.reason}; nothing was written`);
|
|
135
|
+
const task = readsTask(snapshot);
|
|
136
|
+
const claims = epochClaims(records, head, isThreadTerminalRecord);
|
|
137
|
+
if (task === null) {
|
|
138
|
+
const collision = claims.find((claim) => claim.state === 'open'
|
|
139
|
+
|| (claim.state === 'folded' && snapshot.stepClass === 'code' && snapshot.backend === HELD_RECEIPT_BACKEND));
|
|
140
|
+
if (collision !== undefined) {
|
|
141
|
+
throw stop(`mixed-open: untasked dispatch collides with task thread "${collision.nonce}" (${collision.state}); nothing was written`);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const untasked = records.find((record) => record.kind === 'dispatch' && readsTask(record) === null
|
|
146
|
+
&& readsInsideEpoch(record, head) && delegationThreadState(records, record.nonce).open);
|
|
147
|
+
if (untasked !== undefined) {
|
|
148
|
+
throw stop(`mixed-open: task dispatch collides with open untasked thread "${untasked.nonce}"; nothing was written`);
|
|
149
|
+
}
|
|
150
|
+
for (const claim of claims) {
|
|
151
|
+
if (claim.state === 'folded' && claim.brief === task.brief) continue;
|
|
152
|
+
const shared = [...new Set(task.files.flatMap((path) => claim.files.flatMap((other) =>
|
|
153
|
+
hasPathOverlap(path, other) ? [path, other] : [])))].sort();
|
|
154
|
+
if (shared.length > 0) {
|
|
155
|
+
throw stop(`files-overlap: task dispatch overlaps thread "${claim.nonce}" on ${shared.join(', ')}; nothing was written`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const delegationSemanticPreflight = ({ records, snapshot, storePath, head = null }) => {
|
|
131
161
|
const digest = canonicalDelegationDigest(snapshot);
|
|
132
162
|
if (records.some((record) => canonicalDelegationDigest(record) === digest)) {
|
|
133
163
|
throw stop(`refusing a canonical duplicate: ${storePath} already carries this exact record (${digest.slice(0, 12)}…), however its keys are ordered — a genuine new record carries new content or a new timestamp; nothing was written`);
|
|
@@ -180,6 +210,9 @@ export const delegationSemanticPreflight = ({ records, snapshot, storePath }) =>
|
|
|
180
210
|
if (base.kind !== priorBase.kind || base.treeOid !== priorBase.treeOid) {
|
|
181
211
|
throw stop(`refusing a retry: its base {kind "${base.kind}", treeOid ${base.treeOid}} differs from its retry origin "${snapshot.retryOf}"'s {kind "${priorBase.kind}", treeOid ${priorBase.treeOid}} — a retry chain is measured against ONE base; nothing was written`);
|
|
182
212
|
}
|
|
213
|
+
if (JSON.stringify(readsTask(snapshot)) !== JSON.stringify(readsTask(prior.dispatch))) {
|
|
214
|
+
throw stop(`refusing a retry: its task differs from its retry origin "${snapshot.retryOf}" in brief or files — a retry keeps the same task; nothing was written`);
|
|
215
|
+
}
|
|
183
216
|
const origin = findRetryChainOrigin(records, prior.dispatch);
|
|
184
217
|
if (snapshot.retryIndex > origin.retryCap) {
|
|
185
218
|
throw stop(`refusing a retry: retryIndex ${snapshot.retryIndex} exceeds the retryCap ${origin.retryCap} recorded on the thread's ORIGIN dispatch ("${origin.nonce}") — a fresh contract never manufactures a fresh retry budget; nothing was written`);
|
|
@@ -188,6 +221,7 @@ export const delegationSemanticPreflight = ({ records, snapshot, storePath }) =>
|
|
|
188
221
|
throw stop(`refusing a retry: it retries a contract-refusal thread and must carry a DIFFERENT contractDigest — an unchanged contract would only be refused again, and a retry loop on one contract is exactly what the cap exists to prevent; nothing was written`);
|
|
189
222
|
}
|
|
190
223
|
}
|
|
224
|
+
if (head !== null) checkTaskClaims(records, snapshot, head);
|
|
191
225
|
return;
|
|
192
226
|
}
|
|
193
227
|
if (state.dispatch === null) {
|
package/tools/dispatch-store.mjs
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
resolveDelegationStorePath,
|
|
10
10
|
parseDelegationStoreText,
|
|
11
11
|
delegationSemanticPreflight,
|
|
12
|
+
readHeadInstant,
|
|
12
13
|
} from './dispatch-store-read.mjs';
|
|
13
14
|
import { createStoreAppendLane } from './store-append.mjs';
|
|
15
|
+
import { readsTask } from './task-thread.mjs';
|
|
14
16
|
|
|
15
17
|
export {
|
|
16
18
|
DELEGATION_STORE_STOP,
|
|
@@ -52,6 +54,11 @@ const delegationAppendLane = createStoreAppendLane({
|
|
|
52
54
|
export const appendDelegationRecord = ({ cwd = process.cwd(), record, env = process.env, deps = {} } = {}) => {
|
|
53
55
|
const { line, snapshot } = delegationAppendLane.captureRecordSnapshot(record);
|
|
54
56
|
return delegationAppendLane.appendResolvedRecord({
|
|
55
|
-
cwd, env, deps,
|
|
57
|
+
cwd, env, deps, makeRecord: () => ({ line, snapshot }),
|
|
58
|
+
preflight: ({ records, snapshot, line, storePath }) => delegationSemanticPreflight({
|
|
59
|
+
records, snapshot, line, storePath, cwd,
|
|
60
|
+
head: snapshot.kind === 'dispatch' && (readsTask(snapshot) !== null || records.some((record) => readsTask(record) !== null))
|
|
61
|
+
? readHeadInstant(cwd) : null,
|
|
62
|
+
}),
|
|
56
63
|
});
|
|
57
64
|
};
|
package/tools/dispatch.mjs
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// • a receipt is FORGEABLE, exactly like every record in this family. What the absorb door defends
|
|
55
55
|
// against is a buggy or interrupted producer, not a hostile one.
|
|
56
56
|
// • D10 stands as a bar, not a mechanism: at most ONE in-tree exec dispatch runs at a time, and
|
|
57
|
-
// nothing here refuses a second one.
|
|
57
|
+
// nothing here refuses a second one, task threads included; files-overlap and mixed-open additionally refuse conflicting task threads at append time.
|
|
58
58
|
// • `await` observes ARRIVAL and nothing else: an expiry never authorizes the next writer, and the
|
|
59
59
|
// verb releases no slot it never held. Whether the run may be ABSORBED stays `return`'s question. A parallel-write story is not this plan's.
|
|
60
60
|
//
|
|
@@ -99,6 +99,8 @@ import {
|
|
|
99
99
|
resolveRepoRoot, measureScope, ratio, formatRatio, buildObservationRecord,
|
|
100
100
|
} from './observation-builder.mjs';
|
|
101
101
|
import { handoffReturn, HANDOFF_SLUG_RE } from './worktree-handoff-return.mjs';
|
|
102
|
+
import { parseBrief } from './task-brief.mjs';
|
|
103
|
+
import { byteOrder, validateTask } from './task-thread.mjs';
|
|
102
104
|
|
|
103
105
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
104
106
|
|
|
@@ -145,6 +147,7 @@ export const OPEN_FLAG_FIELDS = Object.freeze({
|
|
|
145
147
|
'--rationale': 'rationale',
|
|
146
148
|
'--retry-of': 'retryOf',
|
|
147
149
|
'--checkpoint': 'baseline.treeOid',
|
|
150
|
+
'--task': 'task',
|
|
148
151
|
});
|
|
149
152
|
// COPIED from the contract header at mint and bound by contractDigest (D3): a dispatch that
|
|
150
153
|
// disagreed with the header it claims to carry is refused by checkDispatchMintConsistency.
|
|
@@ -476,7 +479,7 @@ const runObserve = ({ baseCwd, env, argv, now }) => {
|
|
|
476
479
|
// some other producer wrote — and a verb computing over such a ledger inherits its lie. So every
|
|
477
480
|
// verb that DERIVES anything from the store (`return`, `fold`, `degrade`, `aggregate`) reads it
|
|
478
481
|
// through this one door, which replays the store's own preflight in file order and stops at the
|
|
479
|
-
// first record the append path would have refused, naming its physical line.
|
|
482
|
+
// first record the append path would have refused, except files-overlap and mixed-open, which are judged at append time only, naming its physical line.
|
|
480
483
|
//
|
|
481
484
|
// `open` deliberately has no read-side audit: it derives nothing from the ledger. Its retry and wave
|
|
482
485
|
// rules are evaluated by the store, on the snapshot under the lock, which is the only place they can
|
|
@@ -539,6 +542,22 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
539
542
|
if (!resolved.ok) return refusal('open', resolved.reason);
|
|
540
543
|
}
|
|
541
544
|
const base = oid === undefined ? HEAD_BASELINE : { kind: 'checkpoint', treeOid: oid };
|
|
545
|
+
const brief = values['--task'];
|
|
546
|
+
let task = null;
|
|
547
|
+
if (brief !== undefined) {
|
|
548
|
+
if (oid === undefined) return refusal('open', 'task-base: --task requires --checkpoint; nothing was written');
|
|
549
|
+
const root = resolveRepoRoot(cwd);
|
|
550
|
+
if (root === null) return refusal('open', 'not inside a git work tree — a dispatch is opened against a repository (fail closed); nothing was written');
|
|
551
|
+
const read = readRegularFileNoFollow(resolve(root, brief));
|
|
552
|
+
if (read.outcome !== 'ok') {
|
|
553
|
+
return refusal('open', `cannot read ${brief} (${read.code ?? read.className ?? read.outcome}); nothing was written`);
|
|
554
|
+
}
|
|
555
|
+
const parsed = parseBrief(read.content);
|
|
556
|
+
if (!parsed.ok) return refusal('open', parsed.reason);
|
|
557
|
+
task = { brief, files: [...new Set(parsed.files.map(({ path }) => path))].sort(byteOrder) };
|
|
558
|
+
const checked = validateTask(task, base.kind);
|
|
559
|
+
if (!checked.ok) return refusal('open', checked.reason);
|
|
560
|
+
}
|
|
542
561
|
const baselineClean = isCleanAgainstBase(cwd, base);
|
|
543
562
|
if (baselineClean === null) {
|
|
544
563
|
return refusal('open', 'the working state could not be probed, so the baseline is undecidable — a dispatch never records a guessed baseline, and outside a git work tree there is no tree to fingerprint either (fail closed); nothing was written');
|
|
@@ -571,6 +590,7 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
571
590
|
preTreeDigest,
|
|
572
591
|
baselineClean,
|
|
573
592
|
baseline: { kind: base.kind, treeOid: base.treeOid },
|
|
593
|
+
...(task === null ? {} : { task }),
|
|
574
594
|
deadlineS: contract.deadlineS,
|
|
575
595
|
retryOf: values['--retry-of'] ?? null,
|
|
576
596
|
retryIndex: contract.retry.index,
|
|
@@ -1600,7 +1620,7 @@ Usage:
|
|
|
1600
1620
|
[--denominator-bytes <n>] [--cwd <dir>]
|
|
1601
1621
|
node dispatch.mjs open --contract <dispatch-file> --wave <id> --backend <name>
|
|
1602
1622
|
--rationale <text> --wrapper-cap-s <n> --kill-grace-s <n>
|
|
1603
|
-
[--checkpoint <oid>] [--retry-of <nonce>] [--cwd <dir>]
|
|
1623
|
+
[--checkpoint <oid>] [--task <brief>] [--retry-of <nonce>] [--cwd <dir>]
|
|
1604
1624
|
node dispatch.mjs await --nonce <n> [--timeout <s>] [--cwd <dir>]
|
|
1605
1625
|
node dispatch.mjs return --nonce <n> [--outcome <o>]
|
|
1606
1626
|
[--no-receipt --exit-status <n>] [--cwd <dir>]
|
|
@@ -1652,6 +1672,7 @@ refusal from the store verbatim.
|
|
|
1652
1672
|
--checkpoint <oid> records a resolvable TREE object of exactly 40 or 64 lowercase hex characters
|
|
1653
1673
|
(a commit, blob, tag or unknown id refuses by name); the base rides the record and return/fold read
|
|
1654
1674
|
it there, while a checkpoint CLEAN line names the base.
|
|
1675
|
+
--task records the brief's path and its byte-ordered Files, requires --checkpoint (task-base), refuses a brief the parser refuses by the parser's name, and the store refuses files-overlap and mixed-open at append time.
|
|
1655
1676
|
|
|
1656
1677
|
THE LEDGER a tree-binding verb uses is the CANONICAL one, exactly: <git common dir>/
|
|
1657
1678
|
agent-workflow-delegation.jsonl. A store inside the work tree is measured as part of the change set
|
|
@@ -1780,7 +1801,7 @@ arrives as a deletion plus a creation, over-counting a plain rename; under a che
|
|
|
1780
1801
|
rename detection applies inside the one base-relative layer and a detected rename is counted once.
|
|
1781
1802
|
A receipt is forgeable exactly like every record here;
|
|
1782
1803
|
and D10 stands as a BAR, not a mechanism — at most one in-tree exec dispatch at a time, and nothing
|
|
1783
|
-
refuses a second.
|
|
1804
|
+
refuses a second, task threads included; files-overlap and mixed-open additionally refuse conflicting task threads at append time.
|
|
1784
1805
|
|
|
1785
1806
|
Never commits, never runs a subscription CLI, spawns nothing but git READS — with TWO stated exceptions: handoff-return,
|
|
1786
1807
|
which attests MAIN's index with git write-tree: that may write a tree OBJECT into the odb and moves
|
package/tools/ensure-configs.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// ensure-configs.mjs — ONE runnable command for the
|
|
2
|
+
// ensure-configs.mjs — ONE runnable command for the stamp-independent upgrade ensures:
|
|
3
3
|
//
|
|
4
4
|
// orchestration docs/ai/orchestration.json seed, or refresh a still-canonical onboarding note
|
|
5
5
|
// gates docs/ai/gates.json seed-if-missing (an existing declaration is authored content)
|
|
6
6
|
// autonomy docs/ai/autonomy.json seed-if-missing (same)
|
|
7
7
|
// vehicles docs/ai/vehicles.json seed-if-missing (same)
|
|
8
|
+
// executor .claude/agents/executor.md re-derived from docs/ai/vehicles.json when its bytes differ; absent stays absent
|
|
8
9
|
// scripts scripts/<ADR enforcement> seed-if-missing, ADR-layout detect FIRST
|
|
9
10
|
// specs scripts/<spec layer> reader + checker pairs seeded when absent, refreshed only on
|
|
10
11
|
// + docs/ai/specs/index.md a shipped prior body, store root seeded behind a current checker
|
|
11
12
|
// index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
|
|
12
13
|
//
|
|
13
14
|
// Each was prose in references/modes/upgrade.md that an agent performed by hand. One command instead
|
|
14
|
-
// of
|
|
15
|
-
// has a single invocation point whose
|
|
15
|
+
// of one per ensure is deliberate: independent runs would be as many chances to skip one, and the mode
|
|
16
|
+
// doc now has a single invocation point whose outcome lines it relays.
|
|
16
17
|
//
|
|
17
18
|
// The contract (pinned by this module's tests):
|
|
18
19
|
// • --reconcile is REQUIRED. A bare run is a usage error, so nothing writes by accident.
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
// • The ops run in a FIXED order and one op's failure NEVER skips the rest: every op reports its own
|
|
21
22
|
// token, and the exit is non-zero when any of them failed.
|
|
22
23
|
// • The deployment gate runs ONCE, before any op: an absent/symlinked docs/ai stops the whole run
|
|
23
|
-
// with the gate's own message rather than
|
|
24
|
+
// with the gate's own message rather than one copy of it per op.
|
|
24
25
|
//
|
|
25
26
|
// Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
|
|
26
27
|
// Exit codes: 0 every op fine · 1 an op failed, or the deployment gate stopped the run · 2 usage.
|
|
@@ -32,14 +33,15 @@ import { dirname, resolve } from 'node:path';
|
|
|
32
33
|
import { fileURLToPath } from 'node:url';
|
|
33
34
|
import { assertDocsAiDeployment } from './atomic-write.mjs';
|
|
34
35
|
import { isDirectRun } from './direct-run.mjs';
|
|
36
|
+
import { ensureExecutor } from './ensure-executor.mjs';
|
|
35
37
|
import { ENSURE_OPS, OWN_IMPLEMENTATIONS, failedOutcome } from './ensure-ops.mjs';
|
|
36
38
|
import { ensureSpecs } from './ensure-specs.mjs';
|
|
37
39
|
|
|
38
40
|
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
41
|
|
|
40
42
|
// The op table this CLI walks — name → implementation, in ENSURE_OPS order. Composed HERE (the one
|
|
41
|
-
// consumer) because the spec-layer
|
|
42
|
-
export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, specs: ensureSpecs });
|
|
43
|
+
// consumer) because the executor and spec-layer ensures import ensure-ops.mjs for their outcome door and probes.
|
|
44
|
+
export const ENSURE_IMPLEMENTATIONS = Object.freeze({ ...OWN_IMPLEMENTATIONS, executor: ensureExecutor, specs: ensureSpecs });
|
|
43
45
|
|
|
44
46
|
const EXIT_OK = 0;
|
|
45
47
|
const EXIT_FAILED = 1;
|
|
@@ -52,23 +54,25 @@ const EMPTY_ONLY = `--only needs one operation name (${ENSURE_OPS.join(' | ')})
|
|
|
52
54
|
const REPEATED_ONLY = '--only was passed more than once — this selector names exactly ONE operation';
|
|
53
55
|
const unknownOp = (value) => `--only ${value}: no such operation (${ENSURE_OPS.join(' | ')}) — nothing was run`;
|
|
54
56
|
|
|
55
|
-
const HELP = `ensure-configs — the
|
|
57
|
+
const HELP = `ensure-configs — the eight stamp-independent upgrade ensures, as ONE command.
|
|
56
58
|
|
|
57
59
|
Usage:
|
|
58
60
|
node ensure-configs.mjs --reconcile [--dry-run] [--only <op>] [--cwd <project>]
|
|
59
61
|
|
|
60
|
-
--reconcile required — run the
|
|
62
|
+
--reconcile required — run the eight ensures (${ENSURE_OPS.join(', ')})
|
|
61
63
|
--dry-run report what each ensure WOULD do; write nothing
|
|
62
64
|
--only <op> run EXACTLY ONE of them (an unknown, missing or repeated value is a usage error)
|
|
63
65
|
--cwd <dir> the target project (default: the current directory)
|
|
64
66
|
--help, -h this help
|
|
65
67
|
|
|
66
68
|
Every SEED is CREATE-ONLY: an existing file is preserved byte-for-byte, never clobbered and never
|
|
67
|
-
refreshed in place.
|
|
69
|
+
refreshed in place. Four ops refresh instead: the orchestration onboarding note, only while it still
|
|
68
70
|
matches a canonical the kit shipped (your own wording is preserved verbatim); the spec-layer reader
|
|
69
71
|
and checker pairs, only while a file's bytes are a body a release shipped (an edited file is
|
|
70
72
|
preserved, and the writes that depend on it — the spec store root included — are withheld behind
|
|
71
|
-
it);
|
|
73
|
+
it); the placed executor vehicle (.claude/agents/executor.md), re-derived from docs/ai/vehicles.json
|
|
74
|
+
whenever its bytes differ from the derived body (a hand edit is replaced, never preserved; an absent
|
|
75
|
+
file is never placed here); and the navigator index — a GENERATED artifact, regenerated
|
|
72
76
|
whenever it is missing or stale. The enforcement-script ensure detects an older ADR-store layout
|
|
73
77
|
FIRST and instructs the opt-in migration instead of seeding.
|
|
74
78
|
|
|
@@ -149,7 +153,7 @@ export const main = (argv = [], ctx = {}) => {
|
|
|
149
153
|
const cwd = resolve(args.cwd ?? ctx.cwd ?? process.cwd());
|
|
150
154
|
const deps = ctx.deps ?? {};
|
|
151
155
|
// ONE deployment gate for the whole run (see the header): with no docs/ai there is nothing to
|
|
152
|
-
// reconcile, and
|
|
156
|
+
// reconcile, and one identical STOP per op would read as that many separate problems.
|
|
153
157
|
assertDocsAiDeployment(cwd, deps, { noun: 'the project configuration', rel: 'under docs/ai' });
|
|
154
158
|
const outcomes = runEnsures({ cwd, kitRoot: ctx.kitRoot ?? KIT_ROOT, dryRun: args.dryRun, deps, only: args.only });
|
|
155
159
|
return {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ensure-executor.mjs — the EIGHTH upgrade ensure, fifth in the fixed order right after `vehicles`:
|
|
2
|
+
// the placed executor vehicle (.claude/agents/executor.md) re-derived from docs/ai/vehicles.json.
|
|
3
|
+
// The agents writer PLACES the vehicle on consent; this ensure never does — it only keeps a placed
|
|
4
|
+
// body equal to the one derived from the settings, and a hand edit is replaced, never preserved.
|
|
5
|
+
//
|
|
6
|
+
// The state table, in the order the cells are proven (the write is the LAST step):
|
|
7
|
+
// | cell | .claude, .claude/agents | placed path | docs/ai/vehicles.json | answer | read before answering |
|
|
8
|
+
// |------|-------------------------|-------------|-----------------------|--------|-----------------------|
|
|
9
|
+
// | 0 | a symlink or not a directory | any | any | failed / wrong-node-kind | nothing |
|
|
10
|
+
// | 1 | absent or directories | a symlink, a directory, another non-regular node | any | failed / wrong-node-kind | nothing |
|
|
11
|
+
// | 2 | absent or directories | no entry | any, malformed included | not-placed | nothing |
|
|
12
|
+
// | 3 | directories | regular file | unreadable (malformed, read or lstat error) | failed / vehicle-settings-unreadable | the settings only |
|
|
13
|
+
// | 4 | directories | regular file | present or absent | failed / bundle-unreadable when the bundled template cannot be read | settings, bundle |
|
|
14
|
+
// | 5 | directories | regular file, bytes equal to the derived body | present or absent | already-current | settings, bundle, placed file |
|
|
15
|
+
// | 6 | directories | regular file, bytes differ | present or absent | dry-run would-re-derive; apply re-derived, or failed / write-refused | settings, bundle, placed file |
|
|
16
|
+
// | 7 | an lstat of `.claude`, `.claude/agents` or the placed path throwing other than ENOENT, or the read of the placed file throwing | | | NOT caught: the error propagates to the CLI's catch-all (`unexpected-error`) | |
|
|
17
|
+
//
|
|
18
|
+
// Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*). No side effects on import.
|
|
19
|
+
|
|
20
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
23
|
+
import { composeFailure, composeOutcome, probeSeedTarget, tmpNote } from './ensure-ops.mjs';
|
|
24
|
+
import { assertDirSafe, AGENTS_DIR, CLAUDE_DIR, CHEAP_AGENTS_SYMLINK, EXECUTOR_VEHICLE_REL, executorTemplate, readExecutorPosture } from './cheap-agents-read.mjs';
|
|
25
|
+
import { VEHICLES_REL } from './vehicle-settings.mjs';
|
|
26
|
+
|
|
27
|
+
export const EXECUTOR_OP = 'executor';
|
|
28
|
+
|
|
29
|
+
const BUNDLE_DIR = ['references', 'agents'];
|
|
30
|
+
const ENCODING = 'utf8';
|
|
31
|
+
|
|
32
|
+
const ok = (token, lines) => composeOutcome(EXECUTOR_OP, token, lines, false);
|
|
33
|
+
const loud = (cause, ...lines) => composeFailure(EXECUTOR_OP, cause, ...lines);
|
|
34
|
+
const causeOf = (err) => String((err && err.message) || err);
|
|
35
|
+
|
|
36
|
+
export const ensureExecutor = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
37
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
38
|
+
const read = deps.readFile ?? readFileSync;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
assertDirSafe(join(cwd, CLAUDE_DIR), CLAUDE_DIR, { lstat });
|
|
42
|
+
assertDirSafe(join(cwd, AGENTS_DIR), AGENTS_DIR, { lstat });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err?.code !== CHEAP_AGENTS_SYMLINK) throw err;
|
|
45
|
+
return loud('wrong-node-kind', causeOf(err));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = join(cwd, EXECUTOR_VEHICLE_REL);
|
|
49
|
+
const probe = probeSeedTarget(target, lstat);
|
|
50
|
+
if (probe.wrongKind) {
|
|
51
|
+
return loud('wrong-node-kind', `${EXECUTOR_VEHICLE_REL}: exists but is ${probe.wrongKind} — nothing was read or written; resolve it by hand, then re-run`);
|
|
52
|
+
}
|
|
53
|
+
if (!probe.present) {
|
|
54
|
+
return ok('not-placed', [`${EXECUTOR_VEHICLE_REL}: not placed — the agents mode places it on consent; nothing written`]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { posture, reason } = readExecutorPosture(cwd, deps);
|
|
58
|
+
if (posture === null) {
|
|
59
|
+
return loud('vehicle-settings-unreadable', `${EXECUTOR_VEHICLE_REL}: ${reason} — nothing written; fix ${VEHICLES_REL} by hand, then re-run`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = executorTemplate(posture, { ...deps, bundleDir: join(kitRoot, ...BUNDLE_DIR) }).content;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return loud('bundle-unreadable', `${EXECUTOR_VEHICLE_REL}: the bundled executor template could not be read, so nothing was written — reinstall the kit. ${causeOf(err)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existing = read(target, ENCODING);
|
|
70
|
+
if (existing === body) {
|
|
71
|
+
return ok('already-current', [`${EXECUTOR_VEHICLE_REL}: the body derived from ${VEHICLES_REL} (model=${posture.model}, effort=${posture.effort}, source ${posture.source}) — nothing written`]);
|
|
72
|
+
}
|
|
73
|
+
if (dryRun) {
|
|
74
|
+
return ok('would-re-derive', [`${EXECUTOR_VEHICLE_REL}: differs from the body derived from ${VEHICLES_REL} — would be rewritten (a hand edit is replaced, never preserved)`]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = writeContainedFileAtomic(cwd, target, body, deps, { label: EXECUTOR_VEHICLE_REL });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return loud('write-refused', `${EXECUTOR_VEHICLE_REL}: ${causeOf(err)}`);
|
|
82
|
+
}
|
|
83
|
+
return ok('re-derived', [
|
|
84
|
+
`${EXECUTOR_VEHICLE_REL}: rewritten to the body derived from ${VEHICLES_REL} (model=${posture.model}, effort=${posture.effort}) — a hand edit was replaced; set the model in ${VEHICLES_REL}`,
|
|
85
|
+
...tmpNote(EXECUTOR_VEHICLE_REL, result.tmpLeftBehind),
|
|
86
|
+
]);
|
|
87
|
+
};
|
package/tools/ensure-ops.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ensure-ops.mjs — the upgrade ensure operations this module owns, one function each, behind one shared
|
|
2
|
-
// outcome shape; the
|
|
2
|
+
// outcome shape; the leaves that import this module compose their outcomes through
|
|
3
3
|
// the same door and probes exported below. The CLI that orders and runs them is ensure-configs.mjs —
|
|
4
4
|
// it owns the op table, so the import graph stays acyclic; this module owns what each ensure DOES
|
|
5
5
|
// and, more importantly, what it is allowed to CLAIM.
|
|
@@ -371,7 +371,7 @@ export const ensureIndex = ({ cwd, kitRoot, dryRun = false, deps = {} }) => {
|
|
|
371
371
|
};
|
|
372
372
|
|
|
373
373
|
// The ops this module owns, by name. The CLI composes the full ENSURE_OPS table from these plus
|
|
374
|
-
// the
|
|
374
|
+
// the leaves that import THIS module, which join the table in ensure-configs.mjs (so it cannot live here).
|
|
375
375
|
export const OWN_IMPLEMENTATIONS = Object.freeze({
|
|
376
376
|
orchestration: ensureOrchestration,
|
|
377
377
|
gates: ensureGates,
|
|
@@ -10,14 +10,16 @@
|
|
|
10
10
|
// The FIXED order the CLI runs them in — the order references/modes/upgrade.md already prescribed.
|
|
11
11
|
// `specs` sits after `scripts` (its reader pair is a scripts/ seed too) and BEFORE `index`: the store
|
|
12
12
|
// root it seeds is a docs/ai file the navigator must count.
|
|
13
|
-
|
|
13
|
+
// `executor` follows `vehicles`: its body is derived from the settings file that op seeds.
|
|
14
|
+
export const ENSURE_OPS = Object.freeze(['orchestration', 'gates', 'autonomy', 'vehicles', 'executor', 'scripts', 'specs', 'index']);
|
|
14
15
|
|
|
15
16
|
// Tokens that assert a WRITE happened. --dry-run may never emit one of these (the CLI's contract test
|
|
16
17
|
// walks this set), and each has exactly one `would-` counterpart below. `refreshed` is the spec-layer
|
|
17
18
|
// ensure's pair refresh (reader or checker) — a deployed script on a body a release shipped,
|
|
18
19
|
// rewritten to the bundled one (a custom body is never refreshed).
|
|
19
|
-
|
|
20
|
-
export const
|
|
20
|
+
// `re-derived` is the executor ensure rewriting a placed executor whose bytes differ from the body derived from docs/ai/vehicles.json.
|
|
21
|
+
export const WRITE_TOKENS = Object.freeze(['seeded', 'note-refreshed', 'refreshed', 'regenerated', 're-derived']);
|
|
22
|
+
export const DRY_RUN_TOKENS = Object.freeze(['would-seed', 'would-refresh-note', 'would-refresh', 'would-regenerate', 'would-re-derive']);
|
|
21
23
|
|
|
22
24
|
// The CLOSED outcome vocabulary. Closed at RUNTIME, not by convention: composing an outcome with a
|
|
23
25
|
// token outside this list throws, so an op cannot quietly invent a word the mode doc has never heard
|
|
@@ -29,6 +31,7 @@ export const ENSURE_TOKENS = Object.freeze([
|
|
|
29
31
|
...DRY_RUN_TOKENS,
|
|
30
32
|
'already-current',
|
|
31
33
|
'already-present',
|
|
34
|
+
'not-placed',
|
|
32
35
|
'customized-preserved',
|
|
33
36
|
'malformed-preserved',
|
|
34
37
|
'skipped-no-node-evidence',
|
|
@@ -48,6 +51,8 @@ export const FAILURE_CAUSES = Object.freeze([
|
|
|
48
51
|
// here is unproven, so the ensures that place Node scripts write nothing (contract: kit/node-evidence).
|
|
49
52
|
'node-evidence-unverifiable',
|
|
50
53
|
'wrong-node-kind',
|
|
54
|
+
// The settings file could not be read, so no body can be derived and nothing is written.
|
|
55
|
+
'vehicle-settings-unreadable',
|
|
51
56
|
'write-refused',
|
|
52
57
|
'unexpected-error',
|
|
53
58
|
// The navigator ensure drives a SEPARATE PROCESS (the bundled generator), so its failures split by
|
|
@@ -74,10 +79,12 @@ export const RELAYED_ENSURE_TOKENS = Object.freeze([
|
|
|
74
79
|
'note-refreshed',
|
|
75
80
|
'refreshed',
|
|
76
81
|
'regenerated',
|
|
82
|
+
're-derived',
|
|
77
83
|
'already-current',
|
|
78
84
|
'customized-preserved',
|
|
79
85
|
'malformed-preserved',
|
|
80
86
|
'already-present',
|
|
87
|
+
'not-placed',
|
|
81
88
|
'skipped-no-node-evidence',
|
|
82
89
|
'old-adr-layout-migration-instructed',
|
|
83
90
|
'failed',
|