@sabaiway/agent-workflow-kit 5.9.0 → 5.11.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 +139 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +32 -11
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.mjs +160 -0
- package/bridges/antigravity-cli-bridge/bin/agy-envelope.test.mjs +235 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +23 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +242 -38
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +482 -38
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/antigravity-cli-bridge/references/models-and-flags.md +45 -12
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +6 -3
- package/bridges/antigravity-cli-bridge/setup/README.md +18 -5
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/state-block-guard.mjs +107 -45
- package/references/modes/commit-guard.md +11 -8
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +32 -10
- package/references/modes/set-recipe.md +8 -5
- package/references/modes/state-block-guard.md +39 -31
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/check-docs-size-cli.test.mjs +2 -2
- package/references/shared/report-footer.md +2 -2
- package/references/templates/agent_rules.md +1 -0
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/commands.mjs +2 -2
- package/tools/commit-guard.mjs +74 -17
- package/tools/core-evidence.mjs +10 -0
- package/tools/detect-backends.mjs +1 -0
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +68 -14
- package/tools/ensure-configs.mjs +4 -4
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +20 -5
- package/tools/lens-region.mjs +13 -1
- package/tools/observation-builder.mjs +123 -0
- package/tools/satellite-locator.mjs +179 -0
- package/tools/source-size-scope.mjs +3 -1
- package/tools/worktree-handoff-return.mjs +369 -0
- package/tools/worktree-prompt.mjs +190 -0
- package/tools/worktrees-record.mjs +171 -0
- package/tools/worktrees.mjs +308 -297
package/tools/dispatch.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// dispatch.mjs — the delegation ENGINE (delegation Plan 1, Phase 3; the writer verbs are Plan 2,
|
|
3
3
|
// Phase 2 and the waiter Plan 2, Phase 4): the ONE human-facing surface over the record vocabulary
|
|
4
|
-
// (dispatch-record.mjs) and the ledger (dispatch-store.mjs).
|
|
4
|
+
// (dispatch-record.mjs) and the ledger (dispatch-store.mjs). Eleven verbs:
|
|
5
5
|
//
|
|
6
|
-
// check <dispatch-file> the D8 sub-task contract header, FORM-only, exit 0/1
|
|
6
|
+
// check <dispatch-file> the D8 sub-task contract header, FORM-only, exit 0/1 (+ the advisory footer)
|
|
7
|
+
// advise --step-class which vehicle carries this step class on THIS host — advice, never a gate
|
|
7
8
|
// register the wave's PRE-REGISTRATION record (classes, pairing key, thresholds)
|
|
8
9
|
// observe one OBSERVATION record (the solo baseline / a self-reported datum)
|
|
9
10
|
// open the DISPATCH record — every mint-time field COPIED from the header
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
// return the RETURN record — the wrapper's exec receipt ABSORBED through this door
|
|
12
13
|
// fold the FOLD record — the integration re-confirmation
|
|
13
14
|
// degrade the DEGRADE record — the recorded no-fold closure
|
|
15
|
+
// handoff-return the worktree-stream return rung — deliver, prove, then count
|
|
14
16
|
// aggregate [--wave] the L0 deterministic report over ONE wave
|
|
15
17
|
//
|
|
16
18
|
// Why an engine at all: the funded metric — how much leverage a delegated sub-task actually buys —
|
|
@@ -58,31 +60,39 @@
|
|
|
58
60
|
//
|
|
59
61
|
// Writer: appends to the delegation ledger through the store's lock-serialized append (the store's
|
|
60
62
|
// preflight is the single legality door — this module adds NO second validator). Never commits,
|
|
61
|
-
// never runs a subscription CLI, spawns nothing but git READS
|
|
62
|
-
//
|
|
63
|
+
// never runs a subscription CLI, spawns nothing but git READS — with ONE stated exception:
|
|
64
|
+
// `handoff-return` attests MAIN's index with `git write-tree`, which may write a tree OBJECT into
|
|
65
|
+
// the odb and moves no ref (the same probe `land --prepare` itself uses). Dependency-free,
|
|
66
|
+
// Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
63
67
|
|
|
64
|
-
import { readFileSync,
|
|
65
|
-
import { resolve, isAbsolute,
|
|
68
|
+
import { readFileSync, realpathSync, readlinkSync } from 'node:fs';
|
|
69
|
+
import { resolve, isAbsolute, dirname, basename, join } from 'node:path';
|
|
66
70
|
import { fileURLToPath } from 'node:url';
|
|
67
71
|
import { createHash } from 'node:crypto';
|
|
68
72
|
import {
|
|
69
73
|
DELEGATION_SCHEMA_VERSION, STEP_CLASSES, OBSERVATION_PROVENANCE, RETURN_OUTCOMES,
|
|
70
74
|
SESSION_ID_NULLABLE_OUTCOMES, checkDispatchContractForm, checkDispatchMintConsistency,
|
|
71
75
|
contractDigest, canonicalDelegationDigest, computeNumerator, evaluateMetricEligibility,
|
|
72
|
-
evaluateObservationEligibility,
|
|
73
76
|
} from './dispatch-record.mjs';
|
|
74
77
|
import {
|
|
75
78
|
appendDelegationRecord, readDelegationStore, resolveDelegationStorePath, delegationThreadState,
|
|
76
79
|
auditDelegationStoreSemantics, uncommittedStateFingerprint, DELEGATION_STORE_BASENAME,
|
|
77
80
|
} from './dispatch-store.mjs';
|
|
81
|
+
import {
|
|
82
|
+
renderAdvisorBlock, renderSelectionNote, advisorDeps, advisorRow, ADVISOR_STEP_CLASSES,
|
|
83
|
+
ADVISOR_PROBE_POSTURE,
|
|
84
|
+
} from './dispatch-advisor.mjs';
|
|
78
85
|
import { execReceiptBasename, execReportBasename, parseExecReceipt } from './exec-receipt.mjs';
|
|
79
86
|
import {
|
|
80
87
|
enumerateReturnedObjects, computeReturnedDiff, assembleIntegrationBundle,
|
|
81
88
|
} from './exec-producer.mjs';
|
|
82
89
|
import { gitBuf, isTreeClean } from './core-evidence.mjs';
|
|
83
|
-
import { lstatNoFollowRead,
|
|
90
|
+
import { lstatNoFollowRead, readRegularFileNoFollow, readFileBytesNoFollow } from './fs-read-nofollow.mjs';
|
|
84
91
|
import { gitLine } from './flow-store-read.mjs';
|
|
85
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
resolveRepoRoot, measureScope, ratio, formatRatio, buildObservationRecord,
|
|
94
|
+
} from './observation-builder.mjs';
|
|
95
|
+
import { handoffReturn, HANDOFF_SLUG_RE } from './worktree-handoff-return.mjs';
|
|
86
96
|
|
|
87
97
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
88
98
|
|
|
@@ -195,11 +205,6 @@ const OBSERVE_REPEATABLE = new Set(['--scope']);
|
|
|
195
205
|
// would record a contract the computation does not follow — refused at both ends.
|
|
196
206
|
export const IMPLEMENTED_PAIRING_KEYS = Object.freeze(['stepClass']);
|
|
197
207
|
|
|
198
|
-
// The solo baseline counts each scope object's POST-IMAGE — the bytes on disk after the construction
|
|
199
|
-
// — which is exactly the `new` numerator rule (D6). No exec diff kind enumerates ranges, so a solo
|
|
200
|
-
// observation can never claim a partial object.
|
|
201
|
-
const SOLO_COMPONENT_KIND = 'new';
|
|
202
|
-
|
|
203
208
|
// Acceptance aggregates the git-provable domain only (D6/R2).
|
|
204
209
|
const ACCEPTANCE_PROVENANCE = 'wrapper-git';
|
|
205
210
|
|
|
@@ -285,9 +290,65 @@ const asNumber = (flag, raw) => {
|
|
|
285
290
|
|
|
286
291
|
const refusal = (verb, reason) => ({ code: 1, stdout: '', stderr: `dispatch ${verb}: ${reason}` });
|
|
287
292
|
|
|
293
|
+
// ── advise: the vehicle-routing advisory, at its two points of use ────────────────────────────────
|
|
294
|
+
// It refuses nothing and decides nothing (D1). The ledger is read through the SAME single door every
|
|
295
|
+
// deriving verb uses, and its OUTCOME is handed to the advisor — an unreadable store degrades the
|
|
296
|
+
// history line rather than suppressing the advice or moving an exit code.
|
|
297
|
+
|
|
298
|
+
export const ADVISE_FLAG_FIELDS = Object.freeze({ '--step-class': 'stepClass' });
|
|
299
|
+
|
|
300
|
+
// The advisory probe may never own an exit code, so neither of its two throwing inputs escapes it.
|
|
301
|
+
// The store path resolution REFUSES a relative override and one ending in a separator by throwing
|
|
302
|
+
// (dispatch-store.mjs:61-71), and the top-level resolution spawns git: an exception from either would
|
|
303
|
+
// delete a form-valid `check`'s verdict line and turn its exit 0 into a refusal — which is exactly
|
|
304
|
+
// the gate D1 says this surface never becomes. Caught here, the store's own words still travel, as
|
|
305
|
+
// the history line, which is already where an unreadable ledger speaks.
|
|
306
|
+
const ledgerOutcome = (cwd, env) => {
|
|
307
|
+
try {
|
|
308
|
+
return readLegalLedger(cwd, env);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return { ok: false, reason: err?.message ?? String(err) };
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// The cheap vehicles live at the REPOSITORY top-level (.claude/agents/), never at the caller's cwd —
|
|
315
|
+
// a run from a subdirectory would otherwise report a placed vehicle as absent. Only the VEHICLE probe
|
|
316
|
+
// is re-anchored: the ledger keeps the original cwd, because its own resolution is git-common-dir
|
|
317
|
+
// based and already answers the same from anywhere inside the tree.
|
|
318
|
+
//
|
|
319
|
+
// The result is a PAIR, not a path. `resolveRepoRoot` answers null both for "not a work tree" and for
|
|
320
|
+
// "the git probe did not answer", and a throw is a third way to learn nothing — collapsing all three
|
|
321
|
+
// into the caller's cwd made an unlocatable vehicle print as "not placed", which is a fact this tool
|
|
322
|
+
// does not have. Unanchored, the agent lane says `unknown` instead.
|
|
323
|
+
// The resolver is a SEAM (ctx.repoRoot) rather than a hard call, because "the probe threw" is a lane
|
|
324
|
+
// with its own printed answer and a lane nothing can reach by arranging a directory: the throw comes
|
|
325
|
+
// from a git spawn or a realpath the caller cannot make fail on demand. A seam makes the branch
|
|
326
|
+
// exercisable in-process, which is the only place coverage can see it (D14).
|
|
327
|
+
const vehicleAnchor = (cwd, repoRoot) => {
|
|
328
|
+
try {
|
|
329
|
+
const root = repoRoot(cwd);
|
|
330
|
+
return root === null ? { cwd, anchored: false } : { cwd: root, anchored: true };
|
|
331
|
+
} catch {
|
|
332
|
+
return { cwd, anchored: false };
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const advisoryBlock = ({ cwd, env, stepClass, repoRoot }) =>
|
|
337
|
+
renderAdvisorBlock({ stepClass, ledger: ledgerOutcome(cwd, env), deps: advisorDeps(vehicleAnchor(cwd, repoRoot)) });
|
|
338
|
+
|
|
339
|
+
const runAdvise = ({ argv, baseCwd, env, repoRoot }) => {
|
|
340
|
+
const { values, operands, cwd } = scan(argv, ADVISE_FLAG_FIELDS, baseCwd);
|
|
341
|
+
refuseOperands('advise', operands);
|
|
342
|
+
const stepClass = need(values, '--step-class');
|
|
343
|
+
if (advisorRow(stepClass) === undefined) {
|
|
344
|
+
throw usageFail(`--step-class must be one of the D9 step classes ${ADVISOR_STEP_CLASSES.join(' | ')} (got "${stepClass}")`);
|
|
345
|
+
}
|
|
346
|
+
return { code: 0, stdout: advisoryBlock({ cwd, env, stepClass, repoRoot }), stderr: '' };
|
|
347
|
+
};
|
|
348
|
+
|
|
288
349
|
// ── check: the D8 contract header, FORM only ──────────────────────────────────────────────────────
|
|
289
350
|
|
|
290
|
-
const runCheck = ({ argv, baseCwd }) => {
|
|
351
|
+
const runCheck = ({ argv, baseCwd, env, repoRoot }) => {
|
|
291
352
|
const { operands, cwd } = scan(argv, {}, baseCwd);
|
|
292
353
|
if (operands.length > 1) throw usageFail(`unknown argument: ${operands[1]}`);
|
|
293
354
|
const file = operands[0];
|
|
@@ -302,9 +363,17 @@ const runCheck = ({ argv, baseCwd }) => {
|
|
|
302
363
|
const form = checkDispatchContractForm(text);
|
|
303
364
|
if (!form.ok) return { code: 1, stdout: `dispatch check: FORM VIOLATION — ${form.reason}`, stderr: '' };
|
|
304
365
|
const c = form.contract;
|
|
366
|
+
// The advisory footer prints ONLY here, under a form-valid contract, so it can never mask a
|
|
367
|
+
// refusal: the exit code and the FIRST line above are identical whatever the advisor concludes.
|
|
368
|
+
const advisory = [advisoryBlock({ cwd, env, stepClass: c.stepClass, repoRoot }), renderSelectionNote(c)]
|
|
369
|
+
.filter((line) => line !== null && line !== undefined);
|
|
305
370
|
return {
|
|
306
371
|
code: 0,
|
|
307
|
-
stdout:
|
|
372
|
+
stdout: [
|
|
373
|
+
`dispatch check: FORM OK — nonce "${c.nonce}", step class "${c.stepClass}", vehicle ${c.vehicle.requested} → ${c.vehicle.selected}, deadline ${c.deadlineS}s, retry ${c.retry.index}/${c.retry.cap}`,
|
|
374
|
+
` (${DISPATCH_CONTRACT})`,
|
|
375
|
+
...advisory,
|
|
376
|
+
].join('\n'),
|
|
308
377
|
stderr: '',
|
|
309
378
|
};
|
|
310
379
|
};
|
|
@@ -342,84 +411,13 @@ const runRegister = ({ baseCwd, env, argv, now }) => {
|
|
|
342
411
|
};
|
|
343
412
|
|
|
344
413
|
// ── observe: one hand-recorded observation ────────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
// ratio at all, and printing a number beside the name is how a silent zero gets read as a measurement.
|
|
350
|
-
const formatRatio = (metric) => (metric.eligible
|
|
351
|
-
? `L = ${ratio(metric.numeratorBytes / metric.denominatorBytes)} (${metric.numeratorBytes} B / ${metric.denominatorBytes} B)`
|
|
352
|
-
: `L = n/a — INELIGIBLE (${metric.ineligibleReason})`);
|
|
414
|
+
// The scope measurement and the record construction live in observation-builder.mjs, so this verb
|
|
415
|
+
// and the handoff-return rung build the IDENTICAL record through ONE path (the rung cannot import
|
|
416
|
+
// this module back — dispatch.mjs imports the rung for its verb, and the tools graph is pinned
|
|
417
|
+
// acyclic).
|
|
353
418
|
|
|
354
419
|
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
355
420
|
|
|
356
|
-
// The scope's anchor is the git TOP-LEVEL, never the caller's cwd: a recorded scope must name the
|
|
357
|
-
// same objects whoever runs the tool from wherever. `null` outside a work tree — a repo-relative
|
|
358
|
-
// domain with no repository has nothing to be relative TO, and falling back to cwd would be a
|
|
359
|
-
// second, incompatible semantics for the same field.
|
|
360
|
-
const resolveRepoRoot = (cwd) => {
|
|
361
|
-
if (gitLine(['rev-parse', '--is-inside-work-tree'], cwd) !== 'true') return null;
|
|
362
|
-
const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
363
|
-
return top === null ? null : realpathSync(top);
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// The read is no-follow on the LEAF (a symlinked leaf is already refused by name above; O_NOFOLLOW
|
|
367
|
-
// makes a swap between the classification and the read fail loudly rather than counting another
|
|
368
|
-
// object's bytes). Honest limit: classify-then-read is not race-free, and it is not meant to be —
|
|
369
|
-
// the scope is the orchestrator's OWN work tree and the result is a MAGNITUDE, never a store
|
|
370
|
-
// identity, so a pathname race costs a wrong byte count, not a forged record.
|
|
371
|
-
const readObjectBytes = (path) => {
|
|
372
|
-
const fd = openSync(path, (fsConstants.O_RDONLY ?? 0) | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
|
|
373
|
-
try {
|
|
374
|
-
return readFileSync(fd);
|
|
375
|
-
} finally {
|
|
376
|
-
closeSync(fd);
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
// One scope object → one numerator entry. Refuses by NAME on anything it cannot count honestly: a
|
|
381
|
-
// path escaping the repo LEXICALLY, an absent path, a non-regular path (a symlinked leaf included —
|
|
382
|
-
// following one would count another object's bytes under this name), and a path whose REAL location
|
|
383
|
-
// is outside the repository. The last one is the case the lexical rule alone cannot see: it rejects
|
|
384
|
-
// `../x` while accepting `link/x`, where `link` is an ancestor symlink pointing out of the tree.
|
|
385
|
-
// The identity is the CANONICAL repo-relative path taken from the verified real path — not a content
|
|
386
|
-
// hash. The solo domain has no rename lineage for a content id to protect, and a content id would
|
|
387
|
-
// let one object read between two measurements look like TWO objects instead of refusing as the
|
|
388
|
-
// producer contradiction it is ("one identity, one size"). Two equal-byte files at different paths
|
|
389
|
-
// are two objects and count twice; one path reached twice (a second listing, an in-repo ancestor
|
|
390
|
-
// symlink) is one object and counts once.
|
|
391
|
-
const measureObject = (root, rel) => {
|
|
392
|
-
const lexical = lexicalRepoRelative(rel);
|
|
393
|
-
if (!lexical.ok) return { ok: false, reason: `scope path "${rel}": ${lexical.reason}` };
|
|
394
|
-
const path = resolve(root, rel);
|
|
395
|
-
const stat = lstatNoFollowRead(path);
|
|
396
|
-
if (stat === null) return { ok: false, reason: `scope path "${rel}" does not exist — an observation counts objects that are actually there (fail closed)` };
|
|
397
|
-
if (!stat.isFile()) return { ok: false, reason: `scope path "${rel}" is a ${describeNonRegular(stat)}, not a regular file — the scope names repository objects (fail closed)` };
|
|
398
|
-
const real = realpathSync(path);
|
|
399
|
-
if (!real.startsWith(`${root}${sep}`)) {
|
|
400
|
-
return { ok: false, reason: `scope path "${rel}" resolves to ${real}, which leaves the repository at ${root} — an ancestor symlink is not a way out of the scope domain (fail closed)` };
|
|
401
|
-
}
|
|
402
|
-
const canonical = real.slice(root.length + 1);
|
|
403
|
-
const bytes = readObjectBytes(path);
|
|
404
|
-
return { ok: true, entry: { kind: SOLO_COMPONENT_KIND, path: canonical, objectId: canonical, postImageBytes: bytes.length } };
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
// One repo-relative path per `--scope` occurrence, in the order given. The measured CANONICAL paths
|
|
408
|
-
// become the record's `scope` as a canonical JSON array, so what was measured and what is written
|
|
409
|
-
// down are the same statement — and a path carrying a space says so unambiguously.
|
|
410
|
-
const measureScope = (root, paths) => {
|
|
411
|
-
const entries = [];
|
|
412
|
-
for (const rel of paths) {
|
|
413
|
-
const measured = measureObject(root, rel);
|
|
414
|
-
if (!measured.ok) return measured;
|
|
415
|
-
entries.push(measured.entry);
|
|
416
|
-
}
|
|
417
|
-
const numerator = computeNumerator(entries);
|
|
418
|
-
return numerator.ok
|
|
419
|
-
? { ...numerator, scope: JSON.stringify(entries.map((e) => e.path)) }
|
|
420
|
-
: { ok: false, reason: numerator.reason };
|
|
421
|
-
};
|
|
422
|
-
|
|
423
421
|
const runObserve = ({ baseCwd, env, argv, now }) => {
|
|
424
422
|
const { values, operands, cwd } = scan(argv, OBSERVE_FLAG_FIELDS, baseCwd, { repeatable: OBSERVE_REPEATABLE });
|
|
425
423
|
refuseOperands('observe', operands);
|
|
@@ -444,25 +442,16 @@ const runObserve = ({ baseCwd, env, argv, now }) => {
|
|
|
444
442
|
const denominatorBytes = solo
|
|
445
443
|
? measured.numeratorBytes
|
|
446
444
|
: asInteger('--denominator-bytes', need(values, '--denominator-bytes'));
|
|
447
|
-
const
|
|
448
|
-
const record = {
|
|
449
|
-
schema: DELEGATION_SCHEMA_VERSION,
|
|
450
|
-
kind: 'observation',
|
|
445
|
+
const record = buildObservationRecord({
|
|
451
446
|
waveId: need(values, '--wave'),
|
|
452
447
|
stepClass: need(values, '--step-class'),
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
denominatorBytes,
|
|
457
|
-
components: measured.components,
|
|
458
|
-
provenance,
|
|
459
|
-
eligible: eligibility.eligible,
|
|
460
|
-
ineligibleReason: eligibility.ineligibleReason,
|
|
461
|
-
},
|
|
448
|
+
measured,
|
|
449
|
+
provenance,
|
|
450
|
+
denominatorBytes,
|
|
462
451
|
planId: need(values, '--plan'),
|
|
463
452
|
phase: asInteger('--phase', need(values, '--phase')),
|
|
464
453
|
timestamp: now(),
|
|
465
|
-
};
|
|
454
|
+
});
|
|
466
455
|
const { writtenPath } = appendDelegationRecord({ cwd, record, env });
|
|
467
456
|
// DISTINCT objects, not scope entries: the numerator dedups on the canonical path, so counting
|
|
468
457
|
// entries would report two objects where one was counted — the echo must agree with the number.
|
|
@@ -1348,6 +1337,40 @@ const runDegrade = ({ baseCwd, env, argv, now }) => {
|
|
|
1348
1337
|
};
|
|
1349
1338
|
};
|
|
1350
1339
|
|
|
1340
|
+
// ── handoff-return: the worktree-stream return rung ───────────────────────────────────────────────
|
|
1341
|
+
// The rung itself lives in worktree-handoff-return.mjs (it locates the satellite through the shared
|
|
1342
|
+
// locator leaf, so the worktrees tool never enters this CLI's import closure); this wrapper owns
|
|
1343
|
+
// only the flag surface, exactly like every other verb.
|
|
1344
|
+
|
|
1345
|
+
export const HANDOFF_RETURN_FLAG_FIELDS = Object.freeze({
|
|
1346
|
+
'--wave': 'waveId',
|
|
1347
|
+
'--plan': 'planId',
|
|
1348
|
+
'--phase': 'phase',
|
|
1349
|
+
});
|
|
1350
|
+
export const HANDOFF_RETURN_INPUT_FLAGS = Object.freeze({
|
|
1351
|
+
'--slug': 'the satellite slug the handoff identity is resolved from',
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
const runHandoffReturn = ({ baseCwd, env, argv, now }) => {
|
|
1355
|
+
const { values, operands, cwd } = scan(argv, { ...HANDOFF_RETURN_FLAG_FIELDS, ...HANDOFF_RETURN_INPUT_FLAGS }, baseCwd);
|
|
1356
|
+
refuseOperands('handoff-return', operands);
|
|
1357
|
+
const slug = need(values, '--slug');
|
|
1358
|
+
// Refused as USAGE before any probe: the locator's own refusals interpolate the slug into a
|
|
1359
|
+
// terminal message, and the worktrees grammar is what keeps that echo safe.
|
|
1360
|
+
if (!HANDOFF_SLUG_RE.test(slug)) {
|
|
1361
|
+
throw usageFail(`--slug must match the worktrees slug grammar (lowercase letters, digits, hyphens, max 64 chars, letter/digit first; got ${JSON.stringify(slug)})`);
|
|
1362
|
+
}
|
|
1363
|
+
return handoffReturn({
|
|
1364
|
+
cwd,
|
|
1365
|
+
env,
|
|
1366
|
+
now,
|
|
1367
|
+
slug,
|
|
1368
|
+
waveId: need(values, '--wave'),
|
|
1369
|
+
planId: need(values, '--plan'),
|
|
1370
|
+
phase: asInteger('--phase', need(values, '--phase')),
|
|
1371
|
+
});
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1351
1374
|
// ── aggregate: the L0 report over ONE wave ────────────────────────────────────────────────────────
|
|
1352
1375
|
|
|
1353
1376
|
const WAVE_BEARING_KINDS = ['pre-registration', 'dispatch', 'observation', 'degrade'];
|
|
@@ -1542,6 +1565,7 @@ record, the arrival waiter, and the L0 acceptance report.
|
|
|
1542
1565
|
|
|
1543
1566
|
Usage:
|
|
1544
1567
|
node dispatch.mjs check <dispatch-file> [--cwd <dir>]
|
|
1568
|
+
node dispatch.mjs advise --step-class <c> [--cwd <dir>]
|
|
1545
1569
|
node dispatch.mjs register --wave <id> --step-classes <c[,c...]>
|
|
1546
1570
|
--pairing-key ${IMPLEMENTED_PAIRING_KEYS.join('|')}
|
|
1547
1571
|
--min-per-class <n> --mean-l-threshold <x>
|
|
@@ -1558,10 +1582,25 @@ Usage:
|
|
|
1558
1582
|
node dispatch.mjs fold --nonce <n> --verdict <text> [--cwd <dir>]
|
|
1559
1583
|
node dispatch.mjs degrade --wave <id> --step-class <c> --rationale <text>
|
|
1560
1584
|
[--nonce <n>] [--cwd <dir>]
|
|
1585
|
+
node dispatch.mjs handoff-return --slug <s> --wave <id> --plan <id> --phase <n> [--cwd <dir>]
|
|
1561
1586
|
node dispatch.mjs aggregate [--wave <id>] [--cwd <dir>]
|
|
1562
1587
|
|
|
1563
1588
|
check reads the ONE \`\`\`aw-dispatch-contract fenced block in the dispatch file and validates its
|
|
1564
|
-
FORM: ${DISPATCH_CONTRACT}. Exit 0 form-valid; 1 names the FIRST violated field.
|
|
1589
|
+
FORM: ${DISPATCH_CONTRACT}. Exit 0 form-valid; 1 names the FIRST violated field. On a form-VALID
|
|
1590
|
+
contract it then prints the advisory block below, plus a divergence NOTE when the contract's SELECTED
|
|
1591
|
+
vehicle is not the advised one — the footer prints only over a valid form, so it can never mask a
|
|
1592
|
+
refusal, and the exit code and the FIRST line never move with it.
|
|
1593
|
+
|
|
1594
|
+
advise answers "which vehicle carries this step class on THIS host, and what has the ledger recorded
|
|
1595
|
+
for it" — and DECIDES nothing: it refuses no dispatch and gates no verb. Posture: ${ADVISOR_PROBE_POSTURE}.
|
|
1596
|
+
Host capability is read from the filesystem (the execute backend from the bridge install, the cheap
|
|
1597
|
+
vehicles from the presence of .claude/agents/<name>.md at the REPOSITORY top-level — where that root
|
|
1598
|
+
is not resolved the lane answers "unknown" rather than claiming a vehicle is unplaced);
|
|
1599
|
+
doc-research is HOST-LOCAL and never claimed portable, and the
|
|
1600
|
+
harness's own subagent lane carries no availability verdict at all. The recorded history is the
|
|
1601
|
+
ledger's own thread walk over the four states folded | failure-terminal | degrade-closed | open, with
|
|
1602
|
+
open counted SEPARATELY; an absent ledger prints "no recorded history" and an unreadable one prints
|
|
1603
|
+
the store's own words while the advice still prints. Exit 0 for every legal step class; 2 on usage.
|
|
1565
1604
|
|
|
1566
1605
|
register appends the wave's PRE-REGISTRATION record (immutable per wave: a second one refuses).
|
|
1567
1606
|
observe appends ONE observation — provenance ${OBSERVATION_PROVENANCE.join(' or ')} only, since
|
|
@@ -1653,6 +1692,24 @@ degrade appends the recorded no-fold closure, threaded (with --nonce) or PRE-DIS
|
|
|
1653
1692
|
pre-dispatch form opens no nonce thread, so aggregate REFUSES the whole wave by name once one is
|
|
1654
1693
|
recorded — stated here because it is a live cost of writing that record.
|
|
1655
1694
|
|
|
1695
|
+
handoff-return is the worktree-stream return rung: run FROM MAIN after land --prepare, it locates
|
|
1696
|
+
the satellite through the handoff identity, DELIVERS every user-owned fragment of the handoff byte
|
|
1697
|
+
verbatim (with its boundaries and byte lengths, naming the MAIN-owned destinations), requires BOTH
|
|
1698
|
+
prepared-tree and prepared-head from the record and re-attests them against MAIN's staged write-tree
|
|
1699
|
+
and HEAD (a record with no prepared-head was written by an earlier kit — re-run land --prepare), and
|
|
1700
|
+
appends ONE self-reported worktree-stream observation (numerator: the prepared change set's blob
|
|
1701
|
+
bytes, read from the ATTESTED tree itself via git cat-file — never from disk, which an unstaged
|
|
1702
|
+
edit after the prepare moves silently; denominator: the handoff byte count) ONLY when the prepared
|
|
1703
|
+
change set lies wholly inside the observation domain, re-checking the staged write-tree and HEAD
|
|
1704
|
+
once more immediately before EITHER answer (the pre-append idiom: it narrows the race window rather
|
|
1705
|
+
than closing it). A deletion, a rename's absent old side, a symlink, a submodule, a mode-only
|
|
1706
|
+
change, a path whose name is not valid UTF-8, and every other unrepresentable form end instead with
|
|
1707
|
+
"observation: NOT RECORDED — <form> at <path> is outside the observation domain" at exit 0,
|
|
1708
|
+
delivery and proof still printed — no partial scope is ever recorded. The handoff digest and the two OIDs are the rung's printed PROOF,
|
|
1709
|
+
not ledger fields (the closed observation key set carries no artifact digest — an accepted
|
|
1710
|
+
limitation). The fold stays orchestrator judgment, and a fold landed after the gates leaves them
|
|
1711
|
+
stale: the printed next-step order says so.
|
|
1712
|
+
|
|
1656
1713
|
aggregate reports ONE wave: the registered thresholds, every observation (context, never acceptance),
|
|
1657
1714
|
and per registered step class the delegated threads with the D7 inclusion table applied — a folded
|
|
1658
1715
|
success with an eligible wrapper-git metric contributes its L; a folded success whose metric is
|
|
@@ -1687,7 +1744,9 @@ against the payload, not claimed away here. A receipt is forgeable exactly like
|
|
|
1687
1744
|
and D10 stands as a BAR, not a mechanism — at most one in-tree exec dispatch at a time, and nothing
|
|
1688
1745
|
refuses a second.
|
|
1689
1746
|
|
|
1690
|
-
Never commits, never runs a subscription CLI, spawns nothing but git READS
|
|
1747
|
+
Never commits, never runs a subscription CLI, spawns nothing but git READS — except handoff-return,
|
|
1748
|
+
which attests MAIN's index with git write-tree: that may write a tree OBJECT into the odb and moves
|
|
1749
|
+
no ref (the same probe land --prepare itself uses). Exit codes: 0 success;
|
|
1691
1750
|
1 a refusal (store STOP verbatim, a form violation, an unreadable file, a supervision question); 2
|
|
1692
1751
|
usage; ${AWAIT_UNANSWERED_STATUS} an await that ended with no terminal receipt (the absolute deadline or the --timeout
|
|
1693
1752
|
bound) — its own status so a caller that BRANCHES on the code can tell it from a refusal; a caller
|
|
@@ -1696,6 +1755,10 @@ that discards failure wholesale discards this one too.`;
|
|
|
1696
1755
|
export const main = (argv, ctx = {}) => {
|
|
1697
1756
|
const env = ctx.env ?? process.env;
|
|
1698
1757
|
const now = ctx.now ?? (() => new Date().toISOString());
|
|
1758
|
+
// The ADVISORY lane's repository-root resolver, injectable for exactly one reason: the "the probe
|
|
1759
|
+
// threw" branch cannot be reached by arranging a directory, and an unexercised branch in a lane
|
|
1760
|
+
// whose whole claim is "it never owns an exit code" is the branch worth exercising.
|
|
1761
|
+
const repoRoot = ctx.repoRoot ?? resolveRepoRoot;
|
|
1699
1762
|
try {
|
|
1700
1763
|
// Help is the FIRST argument or nothing: past the verb, `--help` is an ordinary operand or an
|
|
1701
1764
|
// already-claimed flag value, so `check --help` reads a file by that name rather than turning a
|
|
@@ -1703,18 +1766,20 @@ export const main = (argv, ctx = {}) => {
|
|
|
1703
1766
|
if (HELP_FLAGS.has(argv[0])) return { code: 0, stdout: HELP, stderr: '' };
|
|
1704
1767
|
const [verb, ...rest] = argv;
|
|
1705
1768
|
const baseCwd = ctx.cwd ?? process.cwd();
|
|
1706
|
-
if (verb === 'check') return runCheck({ argv: rest, baseCwd });
|
|
1769
|
+
if (verb === 'check') return runCheck({ argv: rest, baseCwd, env, repoRoot });
|
|
1770
|
+
if (verb === 'advise') return runAdvise({ argv: rest, baseCwd, env, repoRoot });
|
|
1707
1771
|
if (verb === 'register') return runRegister({ baseCwd, env, argv: rest, now });
|
|
1708
1772
|
if (verb === 'observe') return runObserve({ baseCwd, env, argv: rest, now });
|
|
1709
1773
|
if (verb === 'open') return runOpen({ baseCwd, env, argv: rest, now });
|
|
1710
1774
|
if (verb === 'return') return runReturn({ baseCwd, env, argv: rest, now });
|
|
1711
1775
|
if (verb === 'fold') return runFold({ baseCwd, env, argv: rest, now });
|
|
1712
1776
|
if (verb === 'degrade') return runDegrade({ baseCwd, env, argv: rest, now });
|
|
1777
|
+
if (verb === 'handoff-return') return runHandoffReturn({ baseCwd, env, argv: rest, now });
|
|
1713
1778
|
if (verb === 'aggregate') return runAggregate({ baseCwd, env, argv: rest });
|
|
1714
1779
|
if (verb === 'await') {
|
|
1715
1780
|
throw usageFail('await is the one verb that WAITS, so it answers through mainAwait (the CLI routes it there) — main() returns the answer a verb has already computed, and a promise handed back here would read as a result object with no code at all');
|
|
1716
1781
|
}
|
|
1717
|
-
throw usageFail(`unknown verb: ${verb ?? '(none)'} — expected check | register | observe | open | await | return | fold | degrade | aggregate (see --help)`);
|
|
1782
|
+
throw usageFail(`unknown verb: ${verb ?? '(none)'} — expected check | advise | register | observe | open | await | return | fold | degrade | handoff-return | aggregate (see --help)`);
|
|
1718
1783
|
} catch (err) {
|
|
1719
1784
|
return { code: err.exitCode ?? 1, stdout: '', stderr: `dispatch: ${err.message}` };
|
|
1720
1785
|
}
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -19,9 +19,7 @@
|
|
|
19
19
|
// Read-only: never writes, never commits, never runs a subscription CLI, spawns nothing. Dependency-
|
|
20
20
|
// free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
import { dirname, resolve } from 'node:path';
|
|
24
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
|
+
import { pathToFileURL } from 'node:url';
|
|
25
23
|
import { EXIT as DOCTOR_EXIT, STATUS as DOCTOR_STATUS, TRUSTED_DIRS as DOCTOR_TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
26
24
|
import {
|
|
27
25
|
RECOMMENDATIONS_SECTION_HEADER,
|
|
@@ -39,6 +37,12 @@ import { PARITY } from './refresh-parity.mjs';
|
|
|
39
37
|
import { HOST_HONORS_QUALIFIER } from './velocity-profile.mjs';
|
|
40
38
|
import { LATENT_ARM_NOTICE } from './review-state.mjs';
|
|
41
39
|
import { QUEUE_SHARED_RULE, LANDING_FROM_MAIN, NO_DEPENDENCIES_POSTURE, CLEANUP_OWNERSHIP_RULE, INCLUDE_IDENTITY_RULE, RESUME_VERIFY_RULE } from './worktrees.mjs';
|
|
40
|
+
// The one-writer BAR the satellite cold-start prompt states at both of its print sites — read from
|
|
41
|
+
// the composer that emits it, not from the worktrees tool, so the lint keeps the leaf's own words.
|
|
42
|
+
import { ONE_WRITER_BAR } from './worktree-prompt.mjs';
|
|
43
|
+
// The after-the-fold order the handoff-return rung prints (D9) — bound from the rung that owns it,
|
|
44
|
+
// so the Landing-flow doc can never drift from what the tool actually tells an operator to do.
|
|
45
|
+
import { AFTER_FOLD_ORDER } from './worktree-handoff-return.mjs';
|
|
42
46
|
// The flow contract constants: the accepted schema version + the honest lagging-kit sentence
|
|
43
47
|
// (owned by the config validator), and the set-flow bookkeeping-floor residual (owned by the
|
|
44
48
|
// arming writer) — each pinned byte-exact into its mode doc(s).
|
|
@@ -47,6 +51,14 @@ import { FLOW_BOOKKEEPING_FLOOR_RESIDUAL } from './set-flow.mjs';
|
|
|
47
51
|
import { FLOW_ARMED_HALVES_HEADER } from './procedures.mjs';
|
|
48
52
|
import { RECEIPT_DEADLINE_CONTRACT } from './receipt-deadline.mjs';
|
|
49
53
|
import { DISPATCH_CONTRACT } from './dispatch.mjs';
|
|
54
|
+
// The routing advisor's two bound sentences, plus the matrix STRUCTURE check that holds the mode
|
|
55
|
+
// doc's routing table to the advisor's frozen registry cell for cell — a correspondence no per-row
|
|
56
|
+
// token check could establish, since every token survives a reorder.
|
|
57
|
+
import { ADVISOR_NO_GATE, HARNESS_SUBAGENT_LANE, ADVISOR_PROBE_POSTURE } from './dispatch-advisor.mjs';
|
|
58
|
+
import {
|
|
59
|
+
ADVISOR_MATRIX_DOC, ADVISOR_MATRIX_BEGIN, ADVISOR_MATRIX_END,
|
|
60
|
+
parseAdvisorMatrix, checkMatrixStructure, readKitDoc,
|
|
61
|
+
} from './advisor-matrix.mjs';
|
|
50
62
|
// The coverage vocabulary leaf: a CLOSED value set the gates contract doc must enumerate.
|
|
51
63
|
import { COVERAGE } from './coverage-state.mjs';
|
|
52
64
|
// The canonical producer body: gates.md prints the whole command byte for byte, so the doc is a
|
|
@@ -58,8 +70,6 @@ import { COVERAGE_PRODUCER_BODY } from './coverage-producer.mjs';
|
|
|
58
70
|
// implementation — and through it the orchestration writer — into its import graph.
|
|
59
71
|
import { RELAYED_ENSURE_TOKENS, RELAYED_FAILURE_CAUSES } from './ensure-vocabulary.mjs';
|
|
60
72
|
|
|
61
|
-
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
62
|
-
|
|
63
73
|
const AUTONOMY_DOCTOR_DOC = 'references/modes/autonomy-doctor.md';
|
|
64
74
|
const RECOMMENDATIONS_DOC = 'references/modes/recommendations.md';
|
|
65
75
|
const UPGRADE_DOC = 'references/modes/upgrade.md';
|
|
@@ -71,7 +81,9 @@ const PROCEDURES_DOC = 'references/modes/procedures.md';
|
|
|
71
81
|
const SET_FLOW_DOC = 'references/modes/set-flow.md';
|
|
72
82
|
const RECEIPT_DEADLINE_DOC = 'references/modes/receipt-deadline.md';
|
|
73
83
|
const GATES_DOC = 'references/modes/gates.md';
|
|
74
|
-
|
|
84
|
+
// One literal for the dispatch mode doc: the structure leaf already names it as the file it anchors
|
|
85
|
+
// its table in, and a second copy here is exactly the drift this lint exists to catch.
|
|
86
|
+
const DISPATCH_DOC = ADVISOR_MATRIX_DOC;
|
|
75
87
|
|
|
76
88
|
// A typed usage failure (exit 2) for the CLI parser — the codebase's typed-error idiom (no classes).
|
|
77
89
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
@@ -151,6 +163,11 @@ export const BINDINGS = Object.freeze([
|
|
|
151
163
|
// prose-only bars a doc could silently drop, so all are pinned to the live strings the record
|
|
152
164
|
// actually carries.
|
|
153
165
|
valueBinding('queue-shared-rule', QUEUE_SHARED_RULE, QUEUE_SHARED_RULE, [WORKTREES_DOC]),
|
|
166
|
+
// D7, the same class: one writer per worktree is a BAR and nothing refuses a second writer. The
|
|
167
|
+
// prompt states it at every print site, so the mode doc must carry the exact words the satellite
|
|
168
|
+
// reads — a doc that softened it would promise a guard the tool does not have.
|
|
169
|
+
valueBinding('one-writer-bar', ONE_WRITER_BAR, ONE_WRITER_BAR, [WORKTREES_DOC]),
|
|
170
|
+
valueBinding('after-fold-order', AFTER_FOLD_ORDER, AFTER_FOLD_ORDER, [WORKTREES_DOC]),
|
|
154
171
|
valueBinding('landing-from-main', LANDING_FROM_MAIN, LANDING_FROM_MAIN, [WORKTREES_DOC]),
|
|
155
172
|
valueBinding('no-dependencies-posture', NO_DEPENDENCIES_POSTURE, NO_DEPENDENCIES_POSTURE, [WORKTREES_DOC]),
|
|
156
173
|
// The cleanup-ownership contract (AD-069): the exact live sentence every ownership STOP emits —
|
|
@@ -185,6 +202,15 @@ export const BINDINGS = Object.freeze([
|
|
|
185
202
|
// doc — a doc that softened either would promise a judgment the checker never makes, or a number
|
|
186
203
|
// the aggregator refuses to compute.
|
|
187
204
|
valueBinding('dispatch-contract', DISPATCH_CONTRACT, DISPATCH_CONTRACT, [DISPATCH_DOC]),
|
|
205
|
+
// The routing advisor's honesty pair (delegation Plan 3): what the advice is NOT (it never gates),
|
|
206
|
+
// and the one lane that carries no availability verdict at all. Both were prose bars a doc edit
|
|
207
|
+
// could soften into a promise the module does not make.
|
|
208
|
+
valueBinding('advisor-no-gate', ADVISOR_NO_GATE, ADVISOR_NO_GATE, [DISPATCH_DOC]),
|
|
209
|
+
valueBinding('harness-subagent-lane', HARNESS_SUBAGENT_LANE, HARNESS_SUBAGENT_LANE, [DISPATCH_DOC]),
|
|
210
|
+
// And the verb's honest PROBE posture: "spawns nothing" was true of the module and false of the
|
|
211
|
+
// verb, which resolves the store path and the vehicle anchor through git. Stated once, live, so
|
|
212
|
+
// the mode doc, the HELP and the module header can never drift into three different claims.
|
|
213
|
+
valueBinding('advisor-probe-posture', ADVISOR_PROBE_POSTURE, ADVISOR_PROBE_POSTURE, [DISPATCH_DOC]),
|
|
188
214
|
// The runner's `coverage=` summary vocabulary (Decision 8): the gates contract doc enumerates the
|
|
189
215
|
// CLOSED value set, so a renamed or added value fails here instead of leaving the doc describing
|
|
190
216
|
// a vocabulary the runner no longer speaks. One binding per value — the set is small and closed.
|
|
@@ -215,22 +241,35 @@ export const checkBinding = (binding, readText) => {
|
|
|
215
241
|
return { constant: binding.constant, token: binding.token, files, ok: files.every((f) => f.ok) };
|
|
216
242
|
};
|
|
217
243
|
|
|
218
|
-
const defaultReadText =
|
|
244
|
+
const defaultReadText = readKitDoc;
|
|
245
|
+
|
|
246
|
+
// The matrix STRUCTURE check rides beside the bindings, from its own leaf: a binding proves a token
|
|
247
|
+
// is somewhere in a file, while correspondence — one row per registry row, in order, every CELL
|
|
248
|
+
// equal — is a different claim with its own parser and its own refusal vocabulary. Re-exported here
|
|
249
|
+
// so the lint stays the ONE surface a caller reaches either check through.
|
|
250
|
+
export { ADVISOR_MATRIX_DOC, ADVISOR_MATRIX_BEGIN, ADVISOR_MATRIX_END, parseAdvisorMatrix, checkMatrixStructure, readKitDoc };
|
|
219
251
|
|
|
220
252
|
// checkParity(bindings, readText) → [ per-binding result ]. Default reads the real modes/*.md files
|
|
221
253
|
// relative to the kit root.
|
|
222
254
|
export const checkParity = (bindings = BINDINGS, readText = defaultReadText) => bindings.map((b) => checkBinding(b, readText));
|
|
223
255
|
|
|
224
256
|
// ── rendering ───────────────────────────────────────────────────────────────────────
|
|
225
|
-
const
|
|
257
|
+
const structureLine = (structure) =>
|
|
258
|
+
` ${structure.ok ? '✓' : '✗'} ${structure.constant} → ${structure.files[0].rel}${structure.ok ? '' : ` — ${structure.files[0].reason}`}`;
|
|
259
|
+
|
|
260
|
+
const formatHuman = (results, structure) => {
|
|
226
261
|
const lines = ['doc-parity — code constants ⟷ references/modes/*.md contract (read-only, BUGFREE-3)'];
|
|
227
262
|
for (const r of results) {
|
|
228
263
|
for (const f of r.files) {
|
|
229
264
|
lines.push(` ${f.ok ? '✓' : '✗'} ${r.constant} → ${f.rel}${f.ok ? '' : ` — ${f.reason}`}`);
|
|
230
265
|
}
|
|
231
266
|
}
|
|
267
|
+
lines.push(structureLine(structure));
|
|
232
268
|
const failed = results.flatMap((r) => r.files.filter((f) => !f.ok).map((f) => `${r.constant} @ ${f.rel}`));
|
|
233
|
-
|
|
269
|
+
// The summary verdict derives from BOTH halves. A PASS token computed from the bindings alone read
|
|
270
|
+
// as a green report while the structure check below it said FAIL — the one surface that lied.
|
|
271
|
+
const green = failed.length === 0 && structure.ok;
|
|
272
|
+
lines.push(` check: ${green ? 'PASS' : 'FAIL'} — ${failed.length === 0 ? `${results.length} binding(s) consistent` : `${failed.length} drifted binding(s): ${failed.join('; ')}`}, structure ${structure.ok ? 'PASS' : 'FAIL'}`);
|
|
234
273
|
return lines.join('\n');
|
|
235
274
|
};
|
|
236
275
|
|
|
@@ -248,13 +287,25 @@ orientation contract (shared-queue rule, landing-from-main, no-dependencies inst
|
|
|
248
287
|
worktrees cleanup-ownership rule, the worktrees include-identity rule, the worktrees
|
|
249
288
|
resume-verify rule, the flow tolerate contract (the accepted flow schema version + the
|
|
250
289
|
lagging-kit sentence, procedures.md), the receipt-deadline arrival contract, the dispatch engine's
|
|
251
|
-
FORM-only + aggregate-refusal contract
|
|
290
|
+
FORM-only + aggregate-refusal contract and the routing advisor's two honesty sentences — the
|
|
291
|
+
advice never gates, and the harness-subagent lane carries no availability verdict (dispatch.md), the
|
|
292
|
+
runner's closed coverage= summary
|
|
252
293
|
vocabulary (gates.md), and the canonical coverage-producer-body command the same doc prints in full
|
|
253
294
|
(gates.md) — to
|
|
254
295
|
the exact token its references/modes/*.md contract must carry, and
|
|
255
296
|
asserts the CURRENT value renders into every bound file. A drifted doc, an unreadable bound file,
|
|
256
297
|
or an absent token FAILS CLOSED.
|
|
257
298
|
|
|
299
|
+
Beside the bindings runs ONE structure check: the dispatch mode doc's routing matrix must CORRESPOND
|
|
300
|
+
to the frozen advisor registry — one row per step class, in registry order, no duplicates, and every
|
|
301
|
+
CELL of every row equal to the registry's (vehicle, availability and returns alike), with the column
|
|
302
|
+
arity pinned so a fifth cell cannot sit there unread. A reorder, a duplicate, a dropped row, a
|
|
303
|
+
mis-bound vehicle and a drifted availability or returns cell each leave every token present, so a
|
|
304
|
+
token check would pass every one of them; this one names the first CELL that disagrees. The table is
|
|
305
|
+
read only from the surface anchored between the doc's advisor-matrix begin/end markers, so a copy
|
|
306
|
+
elsewhere in the doc can neither stand in for it nor mask its drift, and a drifted header inside the
|
|
307
|
+
anchor fails closed rather than falling through to another table.
|
|
308
|
+
|
|
258
309
|
--check exits 0/1 as a gate (declare it in docs/ai/gates.json by hand). --json prints the structured
|
|
259
310
|
result. Default prints the per-binding report.
|
|
260
311
|
|
|
@@ -270,15 +321,18 @@ export const main = (argv, ctx = {}) => {
|
|
|
270
321
|
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
271
322
|
if (unknown !== undefined) throw usageFail(`unknown argument: ${unknown}`);
|
|
272
323
|
const results = checkParity(BINDINGS, readText);
|
|
324
|
+
const structure = checkMatrixStructure(readText);
|
|
273
325
|
const failed = results.filter((r) => !r.ok);
|
|
326
|
+
const green = failed.length === 0 && structure.ok;
|
|
274
327
|
if (argv.includes('--json')) {
|
|
275
|
-
return { code: argv.includes('--check') &&
|
|
328
|
+
return { code: argv.includes('--check') && !green ? 1 : 0, stdout: JSON.stringify({ results, structure, ok: green }, null, 2), stderr: '' };
|
|
276
329
|
}
|
|
277
330
|
if (argv.includes('--check')) {
|
|
278
|
-
const
|
|
279
|
-
|
|
331
|
+
const bindingReason = failed.length === 0 ? `${results.length} binding(s) consistent` : `${failed.length} drifted binding(s): ${failed.map((r) => r.constant).join(', ')} — update the contract doc(s) in the SAME edit as the code`;
|
|
332
|
+
const structureReason = structure.ok ? 'the advisor matrix structure corresponds' : `the advisor matrix structure DRIFTED — ${structure.files[0].reason}`;
|
|
333
|
+
return { code: green ? 0 : 1, stdout: `doc-parity check: ${green ? 'PASS' : 'FAIL'} — ${bindingReason}; ${structureReason}`, stderr: '' };
|
|
280
334
|
}
|
|
281
|
-
return { code: 0, stdout: formatHuman(results), stderr: '' };
|
|
335
|
+
return { code: 0, stdout: formatHuman(results, structure), stderr: '' };
|
|
282
336
|
} catch (err) {
|
|
283
337
|
return { code: err.exitCode ?? 1, stdout: '', stderr: `doc-parity: ${err.message}` };
|
|
284
338
|
}
|
package/tools/ensure-configs.mjs
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// index docs/ai/index.md regenerate-if-missing-or-stale (a GENERATED artifact)
|
|
9
9
|
//
|
|
10
10
|
// Each was prose in references/modes/upgrade.md that an agent performed by hand. One command instead
|
|
11
|
-
// of
|
|
12
|
-
// has a single invocation point whose
|
|
11
|
+
// of five is deliberate: five independent runs would be five chances to skip one, and the mode doc now
|
|
12
|
+
// has a single invocation point whose five outcome lines it relays.
|
|
13
13
|
//
|
|
14
14
|
// The contract (pinned by this module's tests):
|
|
15
15
|
// • --reconcile is REQUIRED. A bare run is a usage error, so nothing writes by accident.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// • The ops run in a FIXED order and one op's failure NEVER skips the rest: every op reports its own
|
|
18
18
|
// token, and the exit is non-zero when any of them failed.
|
|
19
19
|
// • The deployment gate runs ONCE, before any op: an absent/symlinked docs/ai stops the whole run
|
|
20
|
-
// with the gate's own message rather than
|
|
20
|
+
// with the gate's own message rather than five copies of it.
|
|
21
21
|
//
|
|
22
22
|
// Output is ENGLISH/structured (repo-artifact Hard Constraint); the agent localizes when narrating.
|
|
23
23
|
// Exit codes: 0 every op fine · 1 an op failed, or the deployment gate stopped the run · 2 usage.
|
|
@@ -138,7 +138,7 @@ export const main = (argv = [], ctx = {}) => {
|
|
|
138
138
|
const cwd = resolve(args.cwd ?? ctx.cwd ?? process.cwd());
|
|
139
139
|
const deps = ctx.deps ?? {};
|
|
140
140
|
// ONE deployment gate for the whole run (see the header): with no docs/ai there is nothing to
|
|
141
|
-
// reconcile, and
|
|
141
|
+
// reconcile, and five identical STOPs would read as five separate problems.
|
|
142
142
|
assertDocsAiDeployment(cwd, deps, { noun: 'the project configuration', rel: 'under docs/ai' });
|
|
143
143
|
const outcomes = runEnsures({ cwd, kitRoot: ctx.kitRoot ?? KIT_ROOT, dryRun: args.dryRun, deps, only: args.only });
|
|
144
144
|
return {
|