@sabaiway/agent-workflow-kit 5.4.0 → 5.6.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 +130 -0
- package/README.md +1 -0
- package/SKILL.md +5 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +51 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +616 -24
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +700 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +15 -10
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/dispatch.md +29 -0
- package/references/modes/gates.md +6 -3
- package/references/modes/procedures.md +2 -0
- package/references/modes/receipt-deadline.md +3 -3
- package/references/modes/recommendations.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/tools/commands.mjs +7 -0
- package/tools/core-evidence.mjs +37 -3
- package/tools/detect-backends.mjs +5 -4
- package/tools/dispatch-record.mjs +10 -3
- package/tools/dispatch-store.mjs +392 -0
- package/tools/dispatch.mjs +1779 -0
- package/tools/doc-parity.mjs +10 -2
- package/tools/exec-producer.mjs +483 -0
- package/tools/exec-receipt.mjs +263 -0
- package/tools/flow-check-cores.mjs +253 -0
- package/tools/flow-check-git-lane.mjs +56 -0
- package/tools/flow-check-rungs.mjs +330 -0
- package/tools/flow-check.mjs +23 -611
- package/tools/flow-store.mjs +111 -462
- package/tools/gates-declaration.mjs +13 -1
- package/tools/gates-init.mjs +134 -22
- package/tools/procedures.mjs +64 -5
- package/tools/receipt-deadline.mjs +25 -3
- package/tools/recommendations.mjs +108 -7
- package/tools/release-scan.mjs +33 -0
- package/tools/source-size-check.mjs +320 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +53 -0
- package/tools/source-size-gate-cmd.mjs +55 -0
- package/tools/source-size-judge.mjs +114 -0
- package/tools/source-size-refusal.mjs +70 -0
- package/tools/source-size-report.mjs +254 -0
- package/tools/source-size-scope.mjs +145 -0
- package/tools/store-append.mjs +444 -0
- package/tools/velocity-profile.mjs +24 -3
|
@@ -11,6 +11,7 @@ import { join, isAbsolute } from 'node:path';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { fail, loadConfig, CONFIG_REL } from './orchestration-config.mjs';
|
|
13
13
|
import { matchesCoverageProducer } from './coverage-producer.mjs';
|
|
14
|
+
import { matchesSourceSizeGate } from './source-size-core.mjs';
|
|
14
15
|
|
|
15
16
|
// The per-project declaration (strict JSON, hand-editable). cwd-relative — errors show a path the
|
|
16
17
|
// user can open (the orchestration-config CONFIG_REL idiom).
|
|
@@ -174,7 +175,8 @@ export const coverageDeclarationDefects = (gates, projectDir) => {
|
|
|
174
175
|
message:
|
|
175
176
|
`${GATES_REL}: the canonical coverage checker (${checkers[0].id}) must be the LAST declared gate — ` +
|
|
176
177
|
`${after.map((g) => g.id).join(', ')} would run after it consumed the lcov. REORDER the declaration by hand ` +
|
|
177
|
-
'(the gate itself is fine — this is an ORDERING refusal
|
|
178
|
+
'(the gate itself is fine — this is an ORDERING refusal about entries that are ALREADY declared: the fill ' +
|
|
179
|
+
'places a new entry before a trailing checker, but it never reorders what it did not write)',
|
|
178
180
|
}];
|
|
179
181
|
}
|
|
180
182
|
if (!coverageProducerPrecedes(gates, index)) {
|
|
@@ -202,6 +204,16 @@ const REVIEW_DEPENDENT_CHECKS = ['review-state', 'commit-guard', 'coverage-check
|
|
|
202
204
|
export const isReviewDependentGate = (gate, projectDir) =>
|
|
203
205
|
REVIEW_DEPENDENT_CHECKS.some((check) => matchesCanonicalCheck(check, gate.cmd, projectDir));
|
|
204
206
|
|
|
207
|
+
// isKitOwnedCheckerGate — is this gate one of the KIT's own checkers, rather than something the
|
|
208
|
+
// project declared to verify itself? A separate question from review-dependence, and the two stopped
|
|
209
|
+
// coinciding the moment a kit checker arrived that needs no receipt: the source-size gate is
|
|
210
|
+
// deliberately in neither FINAL_CORE_CHECKS nor REVIEW_DEPENDENT_CHECKS, so every surface asking
|
|
211
|
+
// "does this declaration verify the PROJECT?" through the review-dependent predicate alone read a
|
|
212
|
+
// matrix of nothing but that gate as project verification. Three surfaces asked it, each knowing a
|
|
213
|
+
// different half; this is the one home, so a future kit checker is added once.
|
|
214
|
+
export const isKitOwnedCheckerGate = (gate, projectDir) =>
|
|
215
|
+
isReviewDependentGate(gate, projectDir) || matchesSourceSizeGate(gate.cmd, projectDir);
|
|
216
|
+
|
|
205
217
|
// ── the pregate subset derivation (#66 / Decision 7 — ONE home for producer and factory) ─────
|
|
206
218
|
|
|
207
219
|
export const unknownPregateExcludeIds = (gates, exclude) => {
|
package/tools/gates-init.mjs
CHANGED
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
// tool path (spaces survive; executes from the project root).
|
|
46
46
|
//
|
|
47
47
|
// Write discipline: preview (dry-run) is the DEFAULT and writes NOTHING — a declined offer leaves
|
|
48
|
-
// the file byte-identical. `--apply`
|
|
48
|
+
// the file byte-identical. `--apply` writes EXACTLY the consented entries (`--only <id>`
|
|
49
49
|
// repeatable) through the shared atomic-write core (tools/atomic-write.mjs — exclusive-create
|
|
50
|
-
// tmp+rename, TOCTOU re-check, symlink STOPs):
|
|
51
|
-
// entry, refuses id collisions, refuses a malformed declaration (never writes over what it
|
|
52
|
-
// parse).
|
|
50
|
+
// tmp+rename, TOCTOU re-check, symlink STOPs): ADD-ONLY (it never modifies, removes or reorders an
|
|
51
|
+
// existing entry), refuses id collisions, refuses a malformed declaration (never writes over what it
|
|
52
|
+
// cannot parse). WHERE a consented entry lands is a PLACEMENT rule, not a blind append — see
|
|
53
|
+
// placeEntries below. Deployment-gated: docs/ai presence (lstat, no-follow) on EVERY run; the
|
|
53
54
|
// .workflow-version == lineage-head stamp gate on --apply only (the velocity/gate-hook precedent).
|
|
54
55
|
//
|
|
55
56
|
// Exit codes: 0 done / dry-run; 1 precondition STOP (no deployment, stamp, symlink, malformed
|
|
@@ -60,10 +61,20 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
60
61
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
61
62
|
import { discoverGateCandidates, EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
62
63
|
import { GATES_REL, validateDeclaration } from './run-gates.mjs';
|
|
63
|
-
import { coverageDeclarationDefects,
|
|
64
|
+
import { canonicalCheckerGates, coverageDeclarationDefects, isKitOwnedCheckerGate } from './gates-declaration.mjs';
|
|
64
65
|
import { COVERAGE_PRODUCER_BODY, matchesCoverageProducer } from './coverage-producer.mjs';
|
|
65
66
|
import { loadConfig } from './orchestration-config.mjs';
|
|
66
67
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
|
|
68
|
+
// The source-size practice, through its PURE READ core only (D-18): this module asks whether the
|
|
69
|
+
// practice is minted, never how to mint it — the checker's writer half stays outside the read graph.
|
|
70
|
+
import {
|
|
71
|
+
INITIAL_ADOPTION_REASON,
|
|
72
|
+
SOURCE_SIZE_GATE_ID,
|
|
73
|
+
SOURCE_SIZE_TOOL_PATH,
|
|
74
|
+
escapeForLine,
|
|
75
|
+
isLineUnsafe,
|
|
76
|
+
loadSourceSizeConfig,
|
|
77
|
+
} from './source-size-core.mjs';
|
|
67
78
|
|
|
68
79
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
69
80
|
const KIT_ROOT = resolve(HERE, '..');
|
|
@@ -95,9 +106,11 @@ export const TRUST_CHAIN_DISCLOSURE =
|
|
|
95
106
|
const USAGE = `usage: gates-init [--dry-run | --apply] [--only <id>]... [--cwd <dir>] [--help]
|
|
96
107
|
|
|
97
108
|
The consented FILL preview for the project's own docs/ai/gates.json (D9). Default is --dry-run:
|
|
98
|
-
prints the derived { id, title, cmd } entries and writes NOTHING. --apply
|
|
99
|
-
consented entries (--only <id> selects a subset;
|
|
100
|
-
|
|
109
|
+
prints the derived { id, title, cmd } entries and writes NOTHING. --apply WRITES exactly the
|
|
110
|
+
consented entries (--only <id> selects a subset; add-only — existing entries are never modified,
|
|
111
|
+
removed or reordered, an id collision is refused). A consented entry lands at the END, except that
|
|
112
|
+
a non-checker entry goes BEFORE a declaration's trailing canonical coverage checker, which must
|
|
113
|
+
stay last. The offer is CLOSED-WORLD: only a
|
|
101
114
|
terminating-class script name (test / lint / type-check / build — never release/publish/deploy,
|
|
102
115
|
never watch/serve, never a write-mode variant) whose BODY is a member of the literal runner
|
|
103
116
|
allowlist is offered, as the hook-free \`COREPACK_ENABLE_NETWORK=0 <pm> exec -- <body>\` form for
|
|
@@ -410,6 +423,77 @@ export const flowCheckCandidate = (cwd, deps = {}) => {
|
|
|
410
423
|
}
|
|
411
424
|
};
|
|
412
425
|
|
|
426
|
+
// The conditional SOURCE-SIZE candidate — keyed on the practice's own declaration rather than on a
|
|
427
|
+
// recipe: the gate is offered ONLY over a MINTED docs/ai/source-size.json, because on any other
|
|
428
|
+
// state the checker REFUSES by design (an absent config has no declared scope; an authored or
|
|
429
|
+
// half-written one records nothing yet), and a declared gate that refuses is a matrix this preview
|
|
430
|
+
// would have reddened on the user's behalf. The two states are told apart out loud: a project that
|
|
431
|
+
// never declared the practice hears nothing (there is no offer to make), while one that authored it
|
|
432
|
+
// and stopped short hears WHY the gate is not offered yet — the mint is the step it is missing.
|
|
433
|
+
// A path is unrenderable when EITHER guard fires, and both are asked ONCE, before anything about
|
|
434
|
+
// this candidate is printed. They are the same question wearing two coats: double-quoting cannot
|
|
435
|
+
// survive a shell-active byte, and a line-unsafe byte cannot be printed at all — U+2028 quotes
|
|
436
|
+
// perfectly and still breaks the line, so a surface screening only shell metacharacters lets it walk
|
|
437
|
+
// into a declared gate cmd while reading as fully guarded.
|
|
438
|
+
const unrenderableToolPath = (text) => isLineUnsafe(text) || DQ_UNSAFE_PATH_PATTERN.test(text);
|
|
439
|
+
|
|
440
|
+
export const sourceSizeCandidate = (cwd, deps = {}) => {
|
|
441
|
+
const toolPath = deps.sourceSizeTool ?? SOURCE_SIZE_TOOL_PATH;
|
|
442
|
+
try {
|
|
443
|
+
const { state } = loadSourceSizeConfig(resolve(cwd), deps);
|
|
444
|
+
if (state === 'absent') return { candidate: null, note: null };
|
|
445
|
+
// Asked BEFORE the state branches: every branch below prints this path, so a check that only one
|
|
446
|
+
// of them performs is a guard the other silently lacks.
|
|
447
|
+
if (unrenderableToolPath(toolPath)) {
|
|
448
|
+
// The withheld path is NAMED, so it crosses the line-safety boundary before it is rendered —
|
|
449
|
+
// escaped rather than dropped, because two different paths must never print as the same one.
|
|
450
|
+
// The advice still follows the STATE: over an unminted record a hand-declared gate is red on
|
|
451
|
+
// every run, so pointing there would hand the reader their next failure as the way out.
|
|
452
|
+
const lane = state === 'minted'
|
|
453
|
+
? 'declare the gate by hand'
|
|
454
|
+
: `mint the record first — run source-size-check.mjs --adopt --reason "${INITIAL_ADOPTION_REASON}" with the working directory set to this project, then declare the gate by hand`;
|
|
455
|
+
return {
|
|
456
|
+
candidate: null,
|
|
457
|
+
note:
|
|
458
|
+
`the source-size candidate was withheld: the resolved kit path cannot be rendered into a ` +
|
|
459
|
+
`command — it carries bytes that do not survive double-quoting, or a byte that cannot appear ` +
|
|
460
|
+
`in a printed line at all (${escapeForLine(toolPath)}) — ${lane}`,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (state !== 'minted') {
|
|
464
|
+
// The named recovery is the reader's next keystroke, so it is a command they can actually run:
|
|
465
|
+
// the resolved absolute path and an explicit --cwd, carrying the reason a FIRST mint requires
|
|
466
|
+
// (recording a value for the first time is a raise, and the bare verb would refuse). The
|
|
467
|
+
// PROJECT path is screened by the same predicate — a rendered command naming a path the shell
|
|
468
|
+
// would read differently could run somewhere other than the project it names.
|
|
469
|
+
const project = resolve(cwd);
|
|
470
|
+
const recovery = unrenderableToolPath(project)
|
|
471
|
+
? `run source-size-check.mjs --adopt --reason "${INITIAL_ADOPTION_REASON}" yourself, with the working directory set to this project (no command is printed: this project's path cannot be rendered into one)`
|
|
472
|
+
: `run node "${toolPath}" --adopt --reason "${INITIAL_ADOPTION_REASON}" --cwd "${project}"`;
|
|
473
|
+
return {
|
|
474
|
+
candidate: null,
|
|
475
|
+
note:
|
|
476
|
+
`the source-size candidate was withheld: the declared practice is not yet MINTED (${state}) — ` +
|
|
477
|
+
`the checker refuses until it records this tree, so declaring the gate now would red the matrix; ` +
|
|
478
|
+
`${recovery}, then re-run this preview`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
candidate: {
|
|
483
|
+
id: SOURCE_SIZE_GATE_ID,
|
|
484
|
+
title: 'Source files within the declared size caps (the recorded ratchet)',
|
|
485
|
+
cmd: `node "${toolPath}" --check`,
|
|
486
|
+
},
|
|
487
|
+
note: null,
|
|
488
|
+
};
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return {
|
|
491
|
+
candidate: null,
|
|
492
|
+
note: `the source-size practice config is unreadable (${err.message}) — the source-size candidate was not evaluated`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
413
497
|
// Every --only id must name an OFFERED entry — enforced in BOTH paths (dry-run and apply), before
|
|
414
498
|
// any empty-offer shortcut, so a typo is a loud usage error, never a silent filter or a silent
|
|
415
499
|
// "nothing to offer" success.
|
|
@@ -424,15 +508,16 @@ const assertOnlyIdsOffered = (offer, onlyIds = []) => {
|
|
|
424
508
|
}
|
|
425
509
|
};
|
|
426
510
|
|
|
427
|
-
// The full offer: script entries + the conditional review-state / flow-check /
|
|
428
|
-
// candidates (coverage-check LAST — the `run-gates --final` declaration-shape rule
|
|
429
|
-
// checker as the last declared gate, so a whole-offer apply is final-ready by
|
|
430
|
-
// pair keys on plan-execution.review reviewed/council OR a flow block (the P21
|
|
431
|
-
// itself appears only under a flow block.
|
|
511
|
+
// The full offer: script entries + the conditional review-state / flow-check / source-size /
|
|
512
|
+
// coverage-check candidates (coverage-check LAST — the `run-gates --final` declaration-shape rule
|
|
513
|
+
// requires the checker as the last declared gate, so a whole-offer apply is final-ready by
|
|
514
|
+
// construction). The pair keys on plan-execution.review reviewed/council OR a flow block (the P21
|
|
515
|
+
// trio); flow-check itself appears only under a flow block; source-size only over a minted practice.
|
|
432
516
|
export const buildOffer = (cwd, deps = {}) => {
|
|
433
517
|
const scripts = deriveScripts(cwd, deps);
|
|
434
518
|
const rs = reviewStateCandidate(cwd, deps);
|
|
435
519
|
const fc = flowCheckCandidate(cwd, deps);
|
|
520
|
+
const ss = sourceSizeCandidate(cwd, deps);
|
|
436
521
|
const cc = coverageCheckCandidate(cwd, deps);
|
|
437
522
|
// Decision 3 — the checker is never offered DEAD. The producer may come from this offer or from
|
|
438
523
|
// a declaration the user already wrote by hand, so the rule reads the merged picture; a
|
|
@@ -462,7 +547,7 @@ export const buildOffer = (cwd, deps = {}) => {
|
|
|
462
547
|
// readable, no gate → the claim and the advice both hold;
|
|
463
548
|
// readable, has gate → a green matrix proves plenty and the user already declared their own.
|
|
464
549
|
const declarationState =
|
|
465
|
-
existing.unreadable !== null ? 'unreadable' : existing.gates.some((gate) => !
|
|
550
|
+
existing.unreadable !== null ? 'unreadable' : existing.gates.some((gate) => !isKitOwnedCheckerGate(gate, cwd)) ? 'has-gate' : 'no-gate';
|
|
466
551
|
const noVerificationNote =
|
|
467
552
|
scripts.entries.length > 0
|
|
468
553
|
? null
|
|
@@ -471,10 +556,10 @@ export const buildOffer = (cwd, deps = {}) => {
|
|
|
471
556
|
? `, and none is declared either, so a green matrix would prove only that the kit's own checkers ran; declare your own in ${GATES_REL}`
|
|
472
557
|
: ''
|
|
473
558
|
}`;
|
|
474
|
-
const candidates = [rs.candidate, fc.candidate, withholdCoverage ? null : cc.candidate].filter(Boolean);
|
|
559
|
+
const candidates = [rs.candidate, fc.candidate, ss.candidate, withholdCoverage ? null : cc.candidate].filter(Boolean);
|
|
475
560
|
return {
|
|
476
561
|
entries: [...scripts.entries, ...candidates],
|
|
477
|
-
notes: [...scripts.notes, noVerificationNote, unreadableNote, rs.note, fc.note, coverageNote].filter(Boolean),
|
|
562
|
+
notes: [...scripts.notes, noVerificationNote, unreadableNote, rs.note, fc.note, ss.note, coverageNote].filter(Boolean),
|
|
478
563
|
};
|
|
479
564
|
};
|
|
480
565
|
|
|
@@ -510,7 +595,7 @@ export const formatPreview = (offer, applyInvocation = null, { explicitOnly = fa
|
|
|
510
595
|
return lines.join('\n');
|
|
511
596
|
};
|
|
512
597
|
|
|
513
|
-
// ── the existing declaration (
|
|
598
|
+
// ── the existing declaration (the base every placement is computed against) ────────────
|
|
514
599
|
const loadExistingDeclaration = (cwd, deps = {}) => {
|
|
515
600
|
const read = deps.readFile ?? readFileSync;
|
|
516
601
|
const lstat = deps.lstat ?? lstatSync;
|
|
@@ -572,7 +657,30 @@ const readStampValue = (cwd, deps = {}) => {
|
|
|
572
657
|
}
|
|
573
658
|
};
|
|
574
659
|
|
|
575
|
-
// ──
|
|
660
|
+
// ── the PLACEMENT rule (D-8) ───────────────────────────────────────────────────────────
|
|
661
|
+
// WHERE a consented entry lands. A blind append made the fill unusable on exactly the declarations
|
|
662
|
+
// it should serve best: the canonical coverage checker must be the LAST gate, so appending any
|
|
663
|
+
// other entry after it produced a declaration the written-declaration validator correctly reds —
|
|
664
|
+
// and the only way to consent to a new gate on a final-capable declaration was to hand-edit.
|
|
665
|
+
//
|
|
666
|
+
// The rule is one sentence: a non-checker entry goes BEFORE a trailing canonical checker, everything
|
|
667
|
+
// else goes at the end. It is ADD-ONLY still — existing entries keep their relative order, none is
|
|
668
|
+
// modified or removed; only the insertion point moves. A consented CHECKER stays after the trailing
|
|
669
|
+
// one on purpose: two canonical checkers is a duplicate the validator must refuse by name, and
|
|
670
|
+
// hiding that shape behind a clever placement would refuse it for the wrong reason.
|
|
671
|
+
export const placeEntries = (existingGates, selected, projectDir) => {
|
|
672
|
+
const last = existingGates[existingGates.length - 1];
|
|
673
|
+
const isChecker = (gate) => canonicalCheckerGates([gate], projectDir).length === 1;
|
|
674
|
+
if (last === undefined || !isChecker(last)) return [...existingGates, ...selected];
|
|
675
|
+
return [
|
|
676
|
+
...existingGates.slice(0, -1),
|
|
677
|
+
...selected.filter((entry) => !isChecker(entry)),
|
|
678
|
+
last,
|
|
679
|
+
...selected.filter(isChecker),
|
|
680
|
+
];
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// ── apply (write exactly the consented entries, each at its placement) ─────────────────
|
|
576
684
|
export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
577
685
|
assertDocsAiDeployment(cwd, deps, { stop, noun: 'a gate declaration', rel: GATES_REL });
|
|
578
686
|
const stampValue = readStampValue(cwd, deps);
|
|
@@ -593,14 +701,14 @@ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
593
701
|
const collisions = selected.filter((e) => existingIds.has(e.id)).map((e) => e.id);
|
|
594
702
|
if (collisions.length) {
|
|
595
703
|
throw stop(
|
|
596
|
-
`id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (
|
|
704
|
+
`id collision — already declared in ${GATES_REL}: ${collisions.join(', ')} (add-only: the ` +
|
|
597
705
|
`fill never modifies or removes an existing entry; pick the others with --only, or edit by hand)`,
|
|
598
706
|
);
|
|
599
707
|
}
|
|
600
708
|
|
|
601
709
|
const merged = {
|
|
602
710
|
_README: existing.outcome === 'loaded' && existing.readme !== undefined ? existing.readme : templateReadme(deps),
|
|
603
|
-
gates:
|
|
711
|
+
gates: placeEntries(existingGates, selected, cwd),
|
|
604
712
|
};
|
|
605
713
|
validateDeclaration(merged); // every written declaration passes the runner's validator, always
|
|
606
714
|
// Decision 4 — the coverage invariant is enforced on the declaration that GETS WRITTEN, not on
|
|
@@ -610,7 +718,11 @@ export const applyFill = ({ cwd, onlyIds = [] }, deps = {}) => {
|
|
|
610
718
|
if (defects.length) throw stop(`${defects[0].message} — nothing was written`);
|
|
611
719
|
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
612
720
|
const { writtenPath } = writeDocsAiFileAtomic(cwd, GATES_REL, body, deps, { stop, noun: 'a gate declaration' });
|
|
613
|
-
|
|
721
|
+
const placed = selected.map((e) => e.id);
|
|
722
|
+
// `appended` is the DEPRECATED alias of `placed`, carrying the same array: this result is a public
|
|
723
|
+
// tools/ payload, and dropping a field an external consumer reads would make a placement rule a
|
|
724
|
+
// BREAKING change. The name is the only thing that was ever wrong — the value never was.
|
|
725
|
+
return { outcome: 'written', writtenPath, placed, appended: placed, notes: offer.notes };
|
|
614
726
|
};
|
|
615
727
|
|
|
616
728
|
// ── CLI ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -670,7 +782,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
670
782
|
for (const note of result.notes) log(` note: ${note}`); // the user learns WHY (same note as the preview)
|
|
671
783
|
return EXIT_OK;
|
|
672
784
|
}
|
|
673
|
-
log(`[agent-workflow-kit]
|
|
785
|
+
log(`[agent-workflow-kit] declared ${result.placed.length} consented gate(s) in ${GATES_REL}: ${result.placed.join(', ')}`);
|
|
674
786
|
for (const note of result.notes) log(` note: ${note}`); // a mixed offer never silently omits what was screened
|
|
675
787
|
log(`[agent-workflow-kit] ${TRUST_CHAIN_DISCLOSURE}`);
|
|
676
788
|
return EXIT_OK;
|
package/tools/procedures.mjs
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// steps LIVE from the installed agent-workflow-engine (references/procedures.md — AD-016 live read, no
|
|
6
6
|
// bundled mirror), reads the per-project, hand-edited config (docs/ai/orchestration.json), runs the
|
|
7
7
|
// read-only backend detector, and prints the activity's steps VERBATIM + the resolved effective recipe
|
|
8
|
-
// per slot (default = Reviewed-when-a-backend-is-ready, Council on request, slot-aware incl. Delegated)
|
|
8
|
+
// per slot (default = Reviewed-when-a-backend-is-ready, Council on request, slot-aware incl. Delegated),
|
|
9
|
+
// plus the project's DECLARED source-size practice when it declares one (D-17 U1).
|
|
9
10
|
//
|
|
10
11
|
// Invariants (mirror recipes.mjs): pure-where-possible, READ-ONLY (never writes, never commits, never
|
|
11
12
|
// runs a subscription CLI). The deterministic resolution lives in the kit (resolveActivityRecipe), not
|
|
@@ -44,6 +45,10 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from
|
|
|
44
45
|
// acyclic — pinned by test/read-graph-purity.test.mjs (FLOW-READ-GRAPH-PURITY).
|
|
45
46
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
46
47
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
48
|
+
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
49
|
+
// source-size-check.mjs, which owns the writer half: this advisor is a read root of
|
|
50
|
+
// test/read-graph-purity.test.mjs, and the core exists so a surface can ask without reaching a writer.
|
|
51
|
+
import { SOURCE_SIZE_CONFIG_REL, SOURCE_SIZE_WHY, loadSourceSizeConfig, practiceFacts } from './source-size-core.mjs';
|
|
47
52
|
export { CONFIG_REL };
|
|
48
53
|
|
|
49
54
|
// ── argument + override parsing (usage errors → exit 2) ─────────────────────────────
|
|
@@ -361,6 +366,49 @@ const flowHalvesAdvice = (flow, probe) => {
|
|
|
361
366
|
];
|
|
362
367
|
};
|
|
363
368
|
|
|
369
|
+
// ── the declared source-size practice (D-17 U1) ────────────────────────────────────
|
|
370
|
+
// A practice the agent meets only when a gate refuses is a practice learned too late: the caps, their
|
|
371
|
+
// reason and the plan-time rung ride EVERY named-activity render, so the layout is cut to them while
|
|
372
|
+
// the plan is being written. Composed from the project's live declaration, never from constants here.
|
|
373
|
+
// Each config state speaks as itself: ABSENT renders NOTHING (a project that declares no practice must
|
|
374
|
+
// not be handed invented limits); AUTHORED and INCOMPLETE render the declared caps plus the honest
|
|
375
|
+
// "nothing is recorded yet" line — both are pre-mint states, and treating INCOMPLETE as MINTED would
|
|
376
|
+
// report a half record as the whole tree's debt; MINTED renders the recorded counts too.
|
|
377
|
+
// A config that cannot be read renders ONE loud line carrying the reader's own message and the render
|
|
378
|
+
// still completes: the exit code for a broken source-size config belongs to the practice's own
|
|
379
|
+
// checker (exit 2 there, and its declared gate reds the matrix on it), while THIS tool's exit
|
|
380
|
+
// contract is about its own config and the engine.
|
|
381
|
+
|
|
382
|
+
export const DECLARED_PRACTICE_HEADER = `Declared source-size practice (${SOURCE_SIZE_CONFIG_REL}) — known BEFORE the code is written:`;
|
|
383
|
+
|
|
384
|
+
const declaredPracticeAdvice = (cwd, readFile, lstat) => {
|
|
385
|
+
let declaration;
|
|
386
|
+
try {
|
|
387
|
+
declaration = loadSourceSizeConfig(cwd, { readFile, lstat });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return [`Declared source-size practice: UNREADABLE — ${(err && err.message) || err} — fix ${SOURCE_SIZE_CONFIG_REL} by hand; a declared practice is never guessed around.`];
|
|
390
|
+
}
|
|
391
|
+
if (declaration.state === 'absent') return [];
|
|
392
|
+
const facts = practiceFacts(declaration.config);
|
|
393
|
+
// The two pre-mint states share a LANE (the ratchet holds nothing, the mint step is next) but not a
|
|
394
|
+
// FACT: an incomplete file carries half the machine record, so "no size is recorded" would be a
|
|
395
|
+
// plain untruth about it — it names the missing half instead. Neither ever prints minted counts.
|
|
396
|
+
// The mint step is named, not rendered: on a project path that does not survive double-quoting the
|
|
397
|
+
// checker deliberately withholds a paste-ready command, and this advisor never re-decides that.
|
|
398
|
+
const unmintedRecord = declaration.state === 'incomplete'
|
|
399
|
+
? ` recorded: PARTIAL — the machine record is half-written (missing ${declaration.missingMachineKeys.map((key) => `"${key}"`).join(', ')}), so the ratchet holds nothing yet; run \`source-size-check.mjs --check\` for the mint step.`
|
|
400
|
+
: ' recorded: NOTHING YET — the caps are declared but no size is recorded, so the ratchet holds nothing; run `source-size-check.mjs --check` for the mint step.';
|
|
401
|
+
return [
|
|
402
|
+
DECLARED_PRACTICE_HEADER,
|
|
403
|
+
` caps: ${facts.maxLines} lines · ${facts.maxLineBytes} bytes per line, over ${facts.roots} declared root(s).`,
|
|
404
|
+
declaration.state === 'minted'
|
|
405
|
+
? ` recorded: ${facts.recordedFiles} file(s) carry a recorded size (debt, not permission) · aggregate ${facts.aggregateLines} line(s), EXACT — growth takes a reasoned bump, never free headroom.`
|
|
406
|
+
: unmintedRecord,
|
|
407
|
+
` why: ${SOURCE_SIZE_WHY}`,
|
|
408
|
+
' at plan time: every Step that CREATES a file names the file and its single responsibility, and the planned layout fits these caps — the gate is the backstop, never the teacher.',
|
|
409
|
+
];
|
|
410
|
+
};
|
|
411
|
+
|
|
364
412
|
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
365
413
|
// the closed flag set, the grounding note, the round-2/continue delta, and the guarded passthrough
|
|
366
414
|
// tiers — every descriptor printed VERBATIM from the registry mirror of the bridge manifest
|
|
@@ -397,7 +445,7 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
397
445
|
return lines;
|
|
398
446
|
};
|
|
399
447
|
|
|
400
|
-
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves }) => {
|
|
448
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice }) => {
|
|
401
449
|
const lines = [
|
|
402
450
|
section,
|
|
403
451
|
'',
|
|
@@ -417,6 +465,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
417
465
|
const advice = reviewLoopAdvice(slots, activity);
|
|
418
466
|
if (advice.length) lines.push('', ...advice);
|
|
419
467
|
lines.push('', ...costLanesAdvice());
|
|
468
|
+
if (declaredPractice.length) lines.push('', ...declaredPractice);
|
|
420
469
|
if (warnings.length) {
|
|
421
470
|
lines.push('', 'warnings:');
|
|
422
471
|
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
@@ -424,7 +473,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
424
473
|
return lines.join('\n');
|
|
425
474
|
};
|
|
426
475
|
|
|
427
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves }) => ({
|
|
476
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice }) => ({
|
|
428
477
|
activity,
|
|
429
478
|
section,
|
|
430
479
|
slots: Object.fromEntries(
|
|
@@ -439,6 +488,9 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
439
488
|
costLanes: costLanesAdvice(),
|
|
440
489
|
// ADDITIVE (AD-044 Plan 4): the per-activity autonomy block, structured (empty when unresolvable).
|
|
441
490
|
autonomy: autonomyAdvice(activity, autonomy),
|
|
491
|
+
// ADDITIVE (D-17 U1): the SAME composed lines the human render prints — one array, two renders, so
|
|
492
|
+
// a scripted reader and a human can never be told different things about the declared practice.
|
|
493
|
+
declaredPractice,
|
|
442
494
|
// CONDITIONAL (flow P8): the armed-halves block rides ONLY a flow-carrying config — the unarmed
|
|
443
495
|
// JSON key set stays byte-exact (unarmed neutrality outranks the additive-key precedent).
|
|
444
496
|
...(flowHalves == null ? {} : { flowHalves }),
|
|
@@ -461,6 +513,12 @@ ${CONFIG_REL} + the read-only backend detector, and prints both. A per-run
|
|
|
461
513
|
--override <slot>=<recipe> (repeatable) overrides the configured/default recipe for that slot.
|
|
462
514
|
Read-only: never writes, never commits, never runs a subscription CLI.
|
|
463
515
|
|
|
516
|
+
Also prints the project's DECLARED source-size practice (${SOURCE_SIZE_CONFIG_REL}) when it declares
|
|
517
|
+
one — the caps, what is recorded, why the practice exists, and the plan-time rung — as the
|
|
518
|
+
declaredPractice block (--json: the same lines under "declaredPractice"). A project with no such
|
|
519
|
+
file renders nothing; a file that cannot be read renders ONE loud UNREADABLE line and still exits 0
|
|
520
|
+
(the practice's own checker owns the exit code for its config).
|
|
521
|
+
|
|
464
522
|
Exit codes: 0 success (an unsatisfiable override degrades loudly, still 0);
|
|
465
523
|
2 usage (unknown activity / bad --override); 1 config or engine error
|
|
466
524
|
(incl. a malformed ${AUTONOMY_REL} — the advisory still renders, the exit flips).`;
|
|
@@ -515,9 +573,10 @@ export const main = (argv, ctx = {}) => {
|
|
|
515
573
|
// unarmed project keeps byte-identical output (human AND JSON) and never pays the store probe.
|
|
516
574
|
const flowProbe = ctx.flowProbe ?? defaultFlowProbe;
|
|
517
575
|
const flowHalves = config?.flow == null ? null : flowHalvesAdvice(config.flow, flowProbe(cwd));
|
|
576
|
+
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
518
577
|
const stdout = json
|
|
519
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves }), null, 2)
|
|
520
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves });
|
|
578
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice }), null, 2)
|
|
579
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice });
|
|
521
580
|
if (autonomy?.error) {
|
|
522
581
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
523
582
|
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// ARRIVAL: a newline-terminated parseable receipt line from the dispatched backend starting
|
|
6
6
|
// at/after the watermark offset, or — PREFERRED whenever a dispatch nonce is supplied and its
|
|
7
7
|
// finding manifest exists — the nonce-matched manifest (the manifest is minted atomically BEFORE
|
|
8
|
-
// the receipt append, so its presence is the stronger dispatch-identity signal).
|
|
8
|
+
// the receipt append, so its presence is the stronger dispatch-identity signal). "Receipt line" is
|
|
9
|
+
// decided POSITIVELY, by the minimal core below: a delegation-ledger line carries a `backend` too,
|
|
10
|
+
// and a review waiter waits for a REVIEW answer (D10).
|
|
9
11
|
//
|
|
10
12
|
// Watermark semantics (P6/P18, split by surface): the PERSISTED dispatch-ledger watermark stays
|
|
11
13
|
// the plain byte-length integer; THIS RUNNER additionally binds the receipts-file PREFIX
|
|
@@ -32,10 +34,30 @@ export const DEADLINE_POLL_MS = 5000;
|
|
|
32
34
|
|
|
33
35
|
// The one contract sentence, doc-parity-bound into references/modes/receipt-deadline.md — the
|
|
34
36
|
// arrival-not-satisfaction split is the tool's identity and must not drift in the mode doc.
|
|
35
|
-
export const RECEIPT_DEADLINE_CONTRACT = 'satisfaction is receipt ARRIVAL past the watermark — a strictly-newer parseable receipt line from the dispatched backend (or its nonce-matched finding manifest, preferred when present) — never obligation satisfaction';
|
|
37
|
+
export const RECEIPT_DEADLINE_CONTRACT = 'satisfaction is receipt ARRIVAL past the watermark — a strictly-newer parseable REVIEW receipt line from the dispatched backend (or its nonce-matched finding manifest, preferred when present), never a delegation-ledger line that merely names the same backend — never obligation satisfaction';
|
|
36
38
|
|
|
37
39
|
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
38
40
|
|
|
41
|
+
// The receipts store and the delegation ledger are different files with different schemas, but both
|
|
42
|
+
// are JSONL beside the git dir and both carry a `backend` — so a ledger line reaching this store
|
|
43
|
+
// would satisfy a waiter that matched on the backend alone (D10). The rule is therefore POSITIVE,
|
|
44
|
+
// not a blacklist of foreign kinds: a blacklist goes stale the moment the other family grows a kind,
|
|
45
|
+
// and the two errors are not symmetric — an unrecognised line costs a TIMEOUT that names its
|
|
46
|
+
// watermark (loud), while a false ARRIVED answers a review dispatch with something that is not a
|
|
47
|
+
// review. This is the MINIMAL core every review receipt carries and no delegation record can: the
|
|
48
|
+
// kinds that carry `backend` (dispatch, return) have no `verdict`, and the kind that carries
|
|
49
|
+
// `verdict` (fold) has no `backend`. `fingerprint` must be PRESENT but may be null — an empty
|
|
50
|
+
// fingerprint is legal in some receipt modes, so requiring a value would refuse a real receipt.
|
|
51
|
+
const REVIEW_RECEIPT_SCHEMA = 1;
|
|
52
|
+
|
|
53
|
+
const isReviewReceiptLine = (parsed, backend) =>
|
|
54
|
+
parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
55
|
+
&& parsed.schema === REVIEW_RECEIPT_SCHEMA
|
|
56
|
+
&& parsed.backend === backend
|
|
57
|
+
&& typeof parsed.artifact === 'string' && parsed.artifact.length > 0
|
|
58
|
+
&& typeof parsed.verdict === 'string' && parsed.verdict.length > 0
|
|
59
|
+
&& Object.hasOwn(parsed, 'fingerprint');
|
|
60
|
+
|
|
39
61
|
// Every read rides the kit's ONE race-free reader (flow-store-read's no-follow/non-block
|
|
40
62
|
// discipline): store identity is never resolved through a link, a FIFO can never block the
|
|
41
63
|
// bounded wait, and an invalid-UTF-8 store refuses (a byte-unstable store cannot carry a prefix
|
|
@@ -97,7 +119,7 @@ export const pollArrival = ({ path, watermark, prefixHash, backend, nonce = null
|
|
|
97
119
|
} catch {
|
|
98
120
|
continue; // a malformed line never satisfies — and never masks a later valid one
|
|
99
121
|
}
|
|
100
|
-
if (parsed
|
|
122
|
+
if (isReviewReceiptLine(parsed, backend)) {
|
|
101
123
|
return { state: 'satisfied', reason: `a receipt line from backend "${backend}" arrived past watermark offset ${watermark} (${path})` };
|
|
102
124
|
}
|
|
103
125
|
}
|