@sabaiway/agent-workflow-kit 10.5.0 → 11.0.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/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +5 -1
- package/references/modes/review-state.md +3 -3
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +24 -12
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +269 -0
- package/tools/dispatch-store.mjs +21 -357
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/held-session.mjs +165 -0
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +47 -4
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +127 -39
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- package/tools/worktrees.mjs +2 -2
package/tools/flow-writer.mjs
CHANGED
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
// → { code, stdout, stderr }; cwd / env / now are injectable. Dependency-free, Node >= 22. No
|
|
47
47
|
// side effects on import (the isDirectRun idiom).
|
|
48
48
|
|
|
49
|
-
import { readFileSync, lstatSync } from 'node:fs';
|
|
50
|
-
import { join, dirname } from 'node:path';
|
|
49
|
+
import { readFileSync, lstatSync, realpathSync } from 'node:fs';
|
|
50
|
+
import { join, dirname, resolve, isAbsolute, sep } from 'node:path';
|
|
51
51
|
import { fileURLToPath } from 'node:url';
|
|
52
52
|
import { createHash, randomBytes } from 'node:crypto';
|
|
53
53
|
import {
|
|
@@ -72,13 +72,24 @@ import { classifyDeltaChain, deltaCustodyIssue } from './flow-check.mjs';
|
|
|
72
72
|
import { loadConfig } from './orchestration-config.mjs';
|
|
73
73
|
import { computePlanAdoptionCoverage, plansInFlight, quoteReportName } from './review-state.mjs';
|
|
74
74
|
import { writeContainedFileAtomic, lstatNoFollow } from './atomic-write.mjs';
|
|
75
|
-
|
|
75
|
+
import { readFileBytesNoFollowCapped } from './fs-read-nofollow.mjs';
|
|
76
|
+
import { readShippedRobustnessLiterals } from './robustness-literals.mjs';
|
|
77
|
+
import { expandSweepPaths } from './plan-shape-facts.mjs';
|
|
78
|
+
import { checkPlanStructure, isSweep } from './plan-shape.mjs';
|
|
79
|
+
import { escapeForDisplay } from './repo-lex.mjs';
|
|
80
|
+
import { computeRowCoverage, isCoverageDocument } from './robustness-brief.mjs';
|
|
81
|
+
import { decideFoldScope, findDebtRow, normalize } from './fold-scope.mjs';
|
|
82
|
+
import { capIssue, walkIssue, escalationIssue, findingQuoted, validateWalk } from './flow-round-gates.mjs';
|
|
76
83
|
const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
|
|
77
84
|
const refuse = (message) => Object.assign(new Error(message), { exitCode: 1 });
|
|
78
85
|
|
|
79
86
|
const PLANS_DIR = 'docs/plans';
|
|
80
87
|
const HEX64_RE = /^[0-9a-f]{64}$/;
|
|
81
|
-
|
|
88
|
+
const PLAN_READ_CAP = 1_048_576;
|
|
89
|
+
const WALK_READ_CAP = 65_536;
|
|
90
|
+
const sha256 = (value) => createHash('sha256').update(value, typeof value === 'string' ? 'utf8' : undefined).digest('hex');
|
|
91
|
+
const inside = (root, path) => path === root || path.startsWith(`${root}${sep}`);
|
|
92
|
+
const safeWalkPath = (name) => typeof name === 'string' && name.length > 0 && !isAbsolute(name) && !name.includes('\\') && name.split('/').every((part) => !['', '.', '..'].includes(part));
|
|
82
93
|
// ── shared derivation (reads only; legality stays the store preflight's) ────────────
|
|
83
94
|
|
|
84
95
|
const treeContext = (cwd) => {
|
|
@@ -372,6 +383,65 @@ const requireReceiptsPath = (cwd, env, why) => {
|
|
|
372
383
|
return receiptsPath;
|
|
373
384
|
};
|
|
374
385
|
|
|
386
|
+
const decodeUtf8 = (bytes, label, stop = refuse) => {
|
|
387
|
+
try {
|
|
388
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
389
|
+
} catch {
|
|
390
|
+
throw stop(`${label} is malformed UTF-8`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const readBoundPlan = (root, plan, adoption) => {
|
|
394
|
+
const path = join(root, PLANS_DIR, plan);
|
|
395
|
+
const read = readFileBytesNoFollowCapped(path, PLAN_READ_CAP);
|
|
396
|
+
if (read.outcome !== 'ok') throw refuse(`the adopted plan ${plan} is not a readable regular file (${read.className ?? read.code ?? read.outcome})`);
|
|
397
|
+
if (sha256(read.bytes) !== adoption.planDigest) throw refuse(`the adopted plan ${plan} no longer matches its adoption record`);
|
|
398
|
+
return decodeUtf8(read.bytes, `the adopted plan ${plan}`);
|
|
399
|
+
};
|
|
400
|
+
const resolveAdoptedPlan = ({ root, records, planId, owner, arm }) => {
|
|
401
|
+
const adoption = records.find((record) => record.kind === CHAIN_KIND && record.purpose === 'adoption' && record.planId === planId);
|
|
402
|
+
const candidate = computePlanAdoptionCoverage({ root, plans: plansInFlight(root), records, owner })
|
|
403
|
+
.find((entry) => entry.planId === planId);
|
|
404
|
+
const plan = candidate?.covered ? candidate : undefined;
|
|
405
|
+
const name = candidate?.plan ?? planId;
|
|
406
|
+
if (adoption === undefined || plan === undefined) {
|
|
407
|
+
throw refuse(`cannot resolve the adopted plan "${name}" for ${arm} — the adoption record binds its bytes; restore the file or re-adopt (fail closed)`);
|
|
408
|
+
}
|
|
409
|
+
let planText;
|
|
410
|
+
try {
|
|
411
|
+
planText = readBoundPlan(root, plan.plan, adoption);
|
|
412
|
+
} catch {
|
|
413
|
+
throw refuse(`cannot resolve the adopted plan "${name}" for ${arm} — the adoption record binds its bytes; restore the file or re-adopt (fail closed)`);
|
|
414
|
+
}
|
|
415
|
+
return { plan, adoption, planText };
|
|
416
|
+
};
|
|
417
|
+
const readWalkInput = (cwd, name) => {
|
|
418
|
+
if (!safeWalkPath(name)) throw usageFail(`--walk must be a safe forward-slash relative path without an empty, "." or ".." segment (got ${JSON.stringify(name)})`);
|
|
419
|
+
const rawCommon = gitLine(['rev-parse', '--git-common-dir'], cwd);
|
|
420
|
+
if (rawCommon == null) throw usageFail('--walk cannot resolve the git common dir');
|
|
421
|
+
const path = resolve(cwd, rawCommon, name);
|
|
422
|
+
let resolved;
|
|
423
|
+
try {
|
|
424
|
+
resolved = { common: realpathSync(resolve(cwd, rawCommon)), file: realpathSync(path) };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
throw usageFail(`--walk ${JSON.stringify(name)} is ${error?.code === 'ENOENT' ? 'absent' : `unreadable (${error?.code ?? error.message})`}`);
|
|
427
|
+
}
|
|
428
|
+
if (!inside(resolved.common, resolved.file)) throw usageFail(`--walk ${JSON.stringify(name)} resolves to another location outside the git common dir`);
|
|
429
|
+
const read = readFileBytesNoFollowCapped(path, WALK_READ_CAP);
|
|
430
|
+
if (read.outcome !== 'ok') {
|
|
431
|
+
const state = read.outcome === 'foreign' ? `a non-regular ${read.className}` : read.outcome === 'over-cap' ? 'over 64 KiB' : `unreadable (${read.code ?? read.outcome})`;
|
|
432
|
+
throw usageFail(`--walk ${JSON.stringify(name)} is ${state}`);
|
|
433
|
+
}
|
|
434
|
+
let parsed;
|
|
435
|
+
try {
|
|
436
|
+
parsed = JSON.parse(decodeUtf8(read.bytes, `--walk ${JSON.stringify(name)}`, usageFail));
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error.exitCode === 2) throw error;
|
|
439
|
+
throw usageFail(`--walk ${JSON.stringify(name)} is malformed JSON`);
|
|
440
|
+
}
|
|
441
|
+
const stray = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? Object.keys(parsed).find((key) => !['listVersion', 'rows'].includes(key)) : undefined;
|
|
442
|
+
if (stray !== undefined) throw usageFail(`--walk ${JSON.stringify(name)} has unknown key ${JSON.stringify(stray)}`);
|
|
443
|
+
return parsed;
|
|
444
|
+
};
|
|
375
445
|
// The consult-attestation arm reads the {backend, nonce}-named finding manifest beside the
|
|
376
446
|
// receipts file fail-closed: findingDigest is COMPUTED from the manifest's findings payload
|
|
377
447
|
// (form-provable binding — a hand-supplied digest could name findings nobody delivered), and the
|
|
@@ -428,7 +498,7 @@ const RECEIPT_DEADLINE_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'rec
|
|
|
428
498
|
// subset-attempt diagnosis discipline).
|
|
429
499
|
const gateCapJustification = ({ justification, overCap, refusal }) => {
|
|
430
500
|
if (!overCap) {
|
|
431
|
-
if (justification !== undefined) throw usageFail('--justification rides only an over-cap mint (Decision 8) — inside the cap it binds nothing; drop it');
|
|
501
|
+
if (justification !== undefined) throw usageFail('--justification rides only an over-cap mint or a missing-walk lift (Decision 8) — inside the cap with the walk satisfied it binds nothing; drop it');
|
|
432
502
|
return null;
|
|
433
503
|
}
|
|
434
504
|
if (typeof justification !== 'string' || justification.length === 0) {
|
|
@@ -616,6 +686,7 @@ const terminalMoveIssue = ({ flowRecords, chain, cycle, stepId, tree, cwd }) =>
|
|
|
616
686
|
// receipts-file byte-length watermark, a fresh nonce}; receipt/manifest digests stay null until
|
|
617
687
|
// round-land. A boundary invocation opens the step (opensFrom = the chain's prior terminal); an
|
|
618
688
|
// in-step invocation opens the NEXT round (a fingerprint move never rides a revision).
|
|
689
|
+
const walkObligation = (planText) => checkPlanStructure(planText, {}).parsed.rows.some((row) => row.valid);
|
|
619
690
|
const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd, env, timestamp }) => {
|
|
620
691
|
// The backend list is judged BEFORE any store read — a usage-shaped input never surfaces as a
|
|
621
692
|
// state-dependent refusal.
|
|
@@ -628,21 +699,34 @@ const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd
|
|
|
628
699
|
const tree = treeContext(cwd);
|
|
629
700
|
const records = readStoreRecords(cwd, env);
|
|
630
701
|
const { chain, state, commitEpoch } = chainContext(records, planId, tree.owner);
|
|
702
|
+
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
703
|
+
const adopted = resolveAdoptedPlan({ root, records, planId, owner: tree.owner, arm: 'round-open' });
|
|
704
|
+
const walkRequired = walkObligation(adopted.planText);
|
|
631
705
|
const inStep = state.mode === 'in-step' && !state.parked && !state.completed;
|
|
632
706
|
if (newCycle && inStep) throw usageFail('--new-cycle opens at a step boundary only — an open step continues through its own rounds');
|
|
633
707
|
// The in-step guard re-runs on the LOCKED snapshot too (M6) — this closure is the preflight.
|
|
634
708
|
const roundOpenGuard = (flowRecords) => {
|
|
635
709
|
const lockedChain = flowRecords.filter((r) => r.kind === CHAIN_KIND && r.planId === planId);
|
|
636
710
|
const lockedState = walkChainState(lockedChain);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
711
|
+
const lockedReceipts = readReceiptsBytesFailClosed(cwd, env, 'the locked round-open cap check re-reads the receipts store');
|
|
712
|
+
const lockedReceiptMap = new Map((lockedReceipts.bytes == null ? [] : parseReceiptLines(lockedReceipts.bytes, lockedReceipts.path))
|
|
713
|
+
.map(({ record: receipt }) => [canonicalFlowDigest(receipt), receipt]));
|
|
714
|
+
const lockedReadManifest = (backend, nonce) => readManifestDecoded({ receiptsPath: lockedReceipts.path, backend, nonce });
|
|
715
|
+
const lockedCap = capIssue({ heads: stepRoundHeads(lockedChain, target.cycle, target.stepId), receipts: lockedReceiptMap, readManifest: lockedReadManifest });
|
|
716
|
+
if (lockedCap.issue != null) throw refuse([...lockedCap.advisories, lockedCap.issue].join('\n'));
|
|
717
|
+
const lockedMissingWalk = walkRequired ? walkIssue({ records: flowRecords, planId, cycle: target.cycle, stepId: target.stepId, round: target.round, base: tree.base, fingerprint: tree.fingerprint }) : null;
|
|
718
|
+
if (lockedMissingWalk != null && recordedJustification == null) throw refuse(lockedMissingWalk);
|
|
719
|
+
if (lockedMissingWalk == null && missingWalk != null && !roundOverCap && !valveOverCap) {
|
|
720
|
+
gateCapJustification({ justification, overCap: false, refusal: null });
|
|
721
|
+
}
|
|
722
|
+
if (lockedState.mode === 'in-step' && !lockedState.parked && !lockedState.completed) {
|
|
723
|
+
const issue = stepCompletenessIssue({
|
|
724
|
+
flowRecords, chain: lockedChain, cycle: lockedState.cycle, stepId: lockedState.stepId,
|
|
725
|
+
onlyRound: lockedState.round, label: `round-open refuses — a NEW round would strand round ${lockedState.round}`, cwd, env,
|
|
726
|
+
});
|
|
727
|
+
if (issue != null) throw refuse(issue);
|
|
728
|
+
}
|
|
729
|
+
return lockedCap.advisories;
|
|
646
730
|
};
|
|
647
731
|
let target;
|
|
648
732
|
if (inStep) {
|
|
@@ -650,7 +734,6 @@ const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd
|
|
|
650
734
|
throw usageFail(`--step "${stepId}" does not match the open step "${state.stepId}" — an in-step round-open derives its step from the chain walk`);
|
|
651
735
|
}
|
|
652
736
|
target = { stepId: state.stepId, round: state.round + 1, opensFrom: null, cycle: state.cycle };
|
|
653
|
-
roundOpenGuard(records);
|
|
654
737
|
} else {
|
|
655
738
|
if (stepId === undefined) throw usageFail('round-open at a step boundary requires --step <stepId> (the step this round opens)');
|
|
656
739
|
const prior = priorChainTerminal(chain);
|
|
@@ -662,14 +745,21 @@ const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd
|
|
|
662
745
|
);
|
|
663
746
|
const roundOverCap = !existingRounds.has(target.round) && existingRounds.size >= ROUND_HARD_MAX;
|
|
664
747
|
const valveOverCap = target.cycle > REDESIGN_CYCLE_CAP;
|
|
748
|
+
const { path: receiptsPath, bytes } = readReceiptsBytesFailClosed(cwd, env, 'the dispatch watermark is the receipts-file byte length');
|
|
749
|
+
const receiptMap = new Map((bytes == null ? [] : parseReceiptLines(bytes, receiptsPath))
|
|
750
|
+
.map(({ record: receipt }) => [canonicalFlowDigest(receipt), receipt]));
|
|
751
|
+
const readManifest = (backend, nonce) => readManifestDecoded({ receiptsPath, backend, nonce });
|
|
752
|
+
const cap = capIssue({ heads: stepRoundHeads(chain, target.cycle, target.stepId), receipts: receiptMap, readManifest });
|
|
753
|
+
if (cap.issue != null) throw refuse([...cap.advisories, cap.issue].join('\n'));
|
|
754
|
+
const missingWalk = walkRequired ? walkIssue({ records, planId, cycle: target.cycle, stepId: target.stepId, round: target.round, base: tree.base, fingerprint: tree.fingerprint }) : null;
|
|
665
755
|
const recordedJustification = gateCapJustification({
|
|
666
756
|
justification,
|
|
667
|
-
overCap: roundOverCap || valveOverCap,
|
|
757
|
+
overCap: roundOverCap || valveOverCap || missingWalk != null,
|
|
668
758
|
refusal: roundOverCap
|
|
669
759
|
? `round ${target.round} of step "${target.stepId}" (cycle ${target.cycle}) exceeds HARD_MAX ${ROUND_HARD_MAX} rounds per cycle (design §2)`
|
|
670
|
-
: `cycle ${target.cycle} passes the redesign valve (capped at ${REDESIGN_CYCLE_CAP} cycles per plan, design §2)
|
|
760
|
+
: valveOverCap ? `cycle ${target.cycle} passes the redesign valve (capped at ${REDESIGN_CYCLE_CAP} cycles per plan, design §2)` : missingWalk,
|
|
671
761
|
});
|
|
672
|
-
const
|
|
762
|
+
const advisories = roundOpenGuard(records);
|
|
673
763
|
const watermark = bytes == null ? 0 : bytes.byteLength;
|
|
674
764
|
// The receipt-deadline boundary rule, applied at MINT time: a watermark on an unterminated tail
|
|
675
765
|
// would let an appended receipt physically continue that malformed line.
|
|
@@ -686,7 +776,7 @@ const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd
|
|
|
686
776
|
cycle: target.cycle, round: target.round, purpose: 'round', stepId: target.stepId,
|
|
687
777
|
fingerprint: tree.fingerprint, opensFrom: target.opensFrom, dispatches, dispositions: [],
|
|
688
778
|
};
|
|
689
|
-
return { record, justification: recordedJustification, preflight: roundOpenGuard };
|
|
779
|
+
return { record, justification: recordedJustification, walkLift: missingWalk != null && !roundOverCap && !valveOverCap, walkRefusal: missingWalk, advisories, preflight: roundOpenGuard };
|
|
690
780
|
};
|
|
691
781
|
|
|
692
782
|
// round-land — the POST-ARRIVAL half (#42/#13/#33): revises the open round IN PLACE. Arrival
|
|
@@ -697,7 +787,7 @@ const buildRoundOpen = ({ planId, stepId, backends, newCycle, justification, cwd
|
|
|
697
787
|
// from the files (receipt line → canonical digest; manifest bytes → sha256) and land together;
|
|
698
788
|
// a foreign-tree receipt, a missing/malformed/symlinked manifest, or a foreign-identity manifest
|
|
699
789
|
// refuses the binding. A --dispose input appends ONE disposition entry whose findingDigest is the
|
|
700
|
-
// sha256 of the QUOTED finding text, verified
|
|
790
|
+
// sha256 of the QUOTED finding text, verified against a landed manifest's wrapper vocabulary.
|
|
701
791
|
const buildRoundLand = ({ planId, dispose, cwd, env, timestamp }) => {
|
|
702
792
|
const tree = treeContext(cwd);
|
|
703
793
|
const records = readStoreRecords(cwd, env);
|
|
@@ -707,6 +797,7 @@ const buildRoundLand = ({ planId, dispose, cwd, env, timestamp }) => {
|
|
|
707
797
|
if (head == null) throw refuse(`plan "${planId}" has no round record at the open context (cycle ${state.cycle}, step "${state.stepId}", round ${state.round}) — the chain is broken (fail closed)`);
|
|
708
798
|
const { path: receiptsPath, bytes } = readReceiptsBytesFailClosed(cwd, env, 'round-land binds arrivals from the receipts store');
|
|
709
799
|
const lines = bytes == null ? [] : parseReceiptLines(bytes, receiptsPath);
|
|
800
|
+
const receiptMap = new Map(lines.map(({ record: receipt }) => [canonicalFlowDigest(receipt), receipt]));
|
|
710
801
|
const landed = [];
|
|
711
802
|
const pending = [];
|
|
712
803
|
const dispatches = head.dispatches.map((entry) => {
|
|
@@ -770,54 +861,90 @@ const buildRoundLand = ({ planId, dispose, cwd, env, timestamp }) => {
|
|
|
770
861
|
let dispositions = head.dispositions;
|
|
771
862
|
let disposed = null;
|
|
772
863
|
if (dispose != null) {
|
|
773
|
-
const findingDigest = createHash('sha256').update(dispose.finding, 'utf8').digest('hex');
|
|
774
864
|
const carriers = dispatches.filter((e) => e.receiptDigest !== null);
|
|
775
865
|
if (carriers.length === 0) throw refuse('no landed dispatch carries a finding manifest yet — a disposition binds a delivered finding of a landed round (fail closed)');
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const m = readManifestDecoded({ receiptsPath, backend:
|
|
781
|
-
if (m.outcome
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
785
|
-
|
|
866
|
+
if (dispose.action === 'custody-lost') {
|
|
867
|
+
const carrier = carriers.find((e) => e.receiptDigest === dispose.receipt);
|
|
868
|
+
if (carrier === undefined) throw refuse(`--receipt ${dispose.receipt} is not a landed dispatch of this round`);
|
|
869
|
+
if (head.dispositions.some((d) => d.action === 'custody-lost' && d.receiptDigest === dispose.receipt)) throw refuse('this receipt already has a custody-lost disposition');
|
|
870
|
+
const m = readManifestDecoded({ receiptsPath, backend: carrier.backend, nonce: carrier.dispatchNonce });
|
|
871
|
+
if (m.outcome === 'unsafe') throw refuse('the landed dispatch composes no safe manifest name — the ledger is corrupt');
|
|
872
|
+
const reason = m.outcome === 'ok' ? (sha256(m.bytes) === carrier.findingManifestDigest ? null : 'swapped') : m.outcome;
|
|
873
|
+
if (reason === null) throw refuse('no loss to record — this receipt manifest reads cleanly and matches its landed digest');
|
|
874
|
+
if (!['absent', 'foreign', 'error', 'malformed', 'foreign-identity', 'swapped'].includes(reason)) throw refuse(`the manifest loss outcome ${reason} is not recordable`);
|
|
875
|
+
const entry = { action: 'custody-lost', receiptDigest: carrier.receiptDigest, findingManifestDigest: carrier.findingManifestDigest, reason };
|
|
876
|
+
dispositions = [...head.dispositions, entry];
|
|
877
|
+
disposed = { action: dispose.action, receiptDigest: carrier.receiptDigest };
|
|
878
|
+
} else {
|
|
879
|
+
const findingDigest = sha256(dispose.finding);
|
|
880
|
+
const lost = new Set(head.dispositions.filter((d) => d.action === 'custody-lost').map((d) => d.receiptDigest));
|
|
881
|
+
const payloads = carriers.filter((e) => !lost.has(e.receiptDigest)).map((e) => {
|
|
882
|
+
const m = readManifestDecoded({ receiptsPath, backend: e.backend, nonce: e.dispatchNonce });
|
|
883
|
+
if (m.outcome !== 'ok' || sha256(m.bytes) !== e.findingManifestDigest) throw refuse(`the landed manifest is no longer cleanly readable or no longer matches findingManifestDigest; record custody-lost for the broken carrier before any quote-bearing disposition: round-land --dispose custody-lost --receipt ${e.receiptDigest}`);
|
|
884
|
+
return { carrier: e, manifest: m.manifest };
|
|
885
|
+
});
|
|
886
|
+
const quoted = payloads.find(({ carrier, manifest }) => findingQuoted(carrier.backend, manifest.findings, dispose.finding));
|
|
887
|
+
if (quoted === undefined) {
|
|
888
|
+
throw refuse('the quoted finding matches no landed manifest of this round — a substring of its payload in text mode, an exact issue in schema mode — a disposition binds a real delivered finding, verbatim (fail closed)');
|
|
786
889
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
throw refuse(`the core evidence store is unreadable or malformed (${coreRead.readError ?? coreRead.malformedReasons[0]}) — cannot resolve the red-proof (fail closed)`);
|
|
890
|
+
const entry = (() => {
|
|
891
|
+
if (dispose.action === 'folded') {
|
|
892
|
+
// The fold's proof must RESOLVE (the consult-attestation precedent — an unverified digest
|
|
893
|
+
// could name a proof nobody minted): consult-attestation in the flow store, red-proof in
|
|
894
|
+
// the core store.
|
|
895
|
+
if (dispose.proofKind === 'consult-attestation') {
|
|
896
|
+
const target = records.findLast((r) => r.kind === 'consult-attestation' && canonicalFlowDigest(r) === dispose.proofDigest);
|
|
897
|
+
if (target === undefined) throw refuse(`--proof-digest ${dispose.proofDigest.slice(0, 12)}… does not resolve to a consult-attestation in the flow store — a fold's proof binds an existing record (fail closed)`);
|
|
898
|
+
} else {
|
|
899
|
+
const corePath = resolveEvidencePath(cwd, env);
|
|
900
|
+
const coreRead = corePath == null ? { records: [] } : readEvidence(corePath);
|
|
901
|
+
if (coreRead.readError || (coreRead.malformed ?? 0) > 0) {
|
|
902
|
+
throw refuse(`the core evidence store is unreadable or malformed (${coreRead.readError ?? coreRead.malformedReasons[0]}) — cannot resolve the red-proof (fail closed)`);
|
|
903
|
+
}
|
|
904
|
+
if (!coreRead.records.some((r) => r.kind === 'red-proof' && canonicalFlowDigest(r) === dispose.proofDigest)) {
|
|
905
|
+
throw refuse(`--proof-digest ${dispose.proofDigest.slice(0, 12)}… does not resolve to a red-proof in the core evidence store — a fold's proof binds an existing record (fail closed)`);
|
|
906
|
+
}
|
|
805
907
|
}
|
|
806
|
-
|
|
807
|
-
|
|
908
|
+
return { findingDigest, action: 'folded', proofKind: dispose.proofKind, proofDigest: dispose.proofDigest };
|
|
909
|
+
}
|
|
910
|
+
if (dispose.action === 'queued') {
|
|
911
|
+
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
912
|
+
const adopted = resolveAdoptedPlan({ root, records, planId, owner: tree.owner, arm: 'round-land --dispose queued' });
|
|
913
|
+
const queue = loadConfig(root).config?.flow?.debtQueue;
|
|
914
|
+
if (typeof queue !== 'string') throw refuse('flow.debtQueue is absent — a queued disposition resolves one declared queue row');
|
|
915
|
+
const queueRead = readFileBytesNoFollow(join(root, queue));
|
|
916
|
+
if (queueRead.outcome !== 'ok') throw refuse(`flow.debtQueue ${queue} is not a readable regular file (${queueRead.className ?? queueRead.code ?? queueRead.outcome})`);
|
|
917
|
+
const queueText = decodeUtf8(queueRead.bytes, `flow.debtQueue ${queue}`);
|
|
918
|
+
const debtRow = findDebtRow(queueText, dispose.claim);
|
|
919
|
+
const decision = decideFoldScope({ cls: 'new-invariant', claim: dispose.claim, planText: adopted.planText, queueText });
|
|
920
|
+
if (decision.verdict !== 'ACCEPT') throw refuse(decision.lines.join('\n'));
|
|
921
|
+
if (dispose.proofKind === 'consult-attestation') {
|
|
922
|
+
const proof = records.findLast((r) => r.kind === 'consult-attestation' && canonicalFlowDigest(r) === dispose.proofDigest);
|
|
923
|
+
const bound = proof && payloads.find(({ carrier }) => carrier.backend === proof.backend && carrier.dispatchNonce === proof.nonce);
|
|
924
|
+
if (!proof || !bound || ['planId', 'cycle', 'stepId', 'round', 'base', 'fingerprint'].some((key) => proof[key] !== ({ planId, ...head })[key]) || !findingQuoted(bound.carrier.backend, bound.manifest.findings, dispose.finding) || proof.findingDigest !== sha256(bound.manifest.findings)) throw refuse('the consult-attestation proof is not bound to this round, landed dispatch and quoted finding');
|
|
925
|
+
if (proof.proposedFixDigest !== sha256(`${findingDigest}\n${dispose.claim}`)) throw refuse('consult-attestation proposedFixDigest does not equal sha256(itemDigest + "\\n" + claim)');
|
|
926
|
+
} else {
|
|
927
|
+
const corePath = resolveEvidencePath(cwd, env);
|
|
928
|
+
const coreRead = corePath == null ? { records: [] } : readEvidence(corePath);
|
|
929
|
+
if (coreRead.readError || (coreRead.malformed ?? 0) > 0) throw refuse(`the core evidence store is unreadable or malformed (${coreRead.readError ?? coreRead.malformedReasons[0]}) — cannot resolve the red-proof (fail closed)`);
|
|
930
|
+
const proof = authoritativeOfKind(coreRead.records, 'red-proof').find((r) => canonicalFlowDigest(r) === dispose.proofDigest);
|
|
931
|
+
if (!proof || proof.base !== head.base || proof.fingerprint !== tree.fingerprint || !debtRow.fields.proof.includes(proof.testId)) throw refuse('the red-proof does not bind the round head base, the current tree and the resolved queue row proof');
|
|
808
932
|
}
|
|
933
|
+
return { findingDigest, action: 'queued', debtId: normalize(decision.row.firstLine), debtDigest: sha256(decision.row.block), claim: dispose.claim, proofKind: dispose.proofKind, proofDigest: dispose.proofDigest };
|
|
809
934
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
935
|
+
if (dispose.action === 'escalated') {
|
|
936
|
+
const escalation = escalationIssue({ records, digest: dispose.overrideDigest, planId, head });
|
|
937
|
+
if (escalation.issue) throw refuse(escalation.issue);
|
|
938
|
+
const bound = payloads.find(({ carrier }) => carrier.receiptDigest === escalation.dispatch.receiptDigest);
|
|
939
|
+
const receipt = receiptMap.get(escalation.dispatch.receiptDigest);
|
|
940
|
+
if (!bound || !findingQuoted(bound.carrier.backend, bound.manifest.findings, dispose.finding) || !receipt || !isRecognizedVerdict(receipt.verdict) || isShipVerdict(receipt.verdict)) throw refuse('the raiser did not veto this receipt — dispose it as folded, queued or rejected');
|
|
941
|
+
return { findingDigest, action: 'escalated', overrideDigest: dispose.overrideDigest };
|
|
942
|
+
}
|
|
943
|
+
return { findingDigest, action: 'rejected', reason: dispose.reason };
|
|
944
|
+
})();
|
|
945
|
+
dispositions = [...head.dispositions, entry];
|
|
946
|
+
disposed = { action: dispose.action, findingDigest };
|
|
947
|
+
}
|
|
821
948
|
}
|
|
822
949
|
if (landed.length === 0 && disposed == null) {
|
|
823
950
|
throw refuse(`nothing to land — no pending dispatch has an arrived receipt (${pending.length} still pending) and no --dispose was given; a no-op revision never mints`);
|
|
@@ -891,25 +1018,42 @@ const buildUnfreeze = ({ planId, justification, cwd, env, timestamp }) => {
|
|
|
891
1018
|
// covered by an adopted chain (frontmatter planId + content digest + owner, the ONE coverage
|
|
892
1019
|
// predicate review-state's internal-only floor consumes) — an uncovered plan is a refusal naming
|
|
893
1020
|
// the file, never a relaxation.
|
|
894
|
-
const buildInternalAttestation = ({ planId, lenses, degraded, model, effort, tier, authority, cwd, env, timestamp, ctx }) => {
|
|
1021
|
+
const buildInternalAttestation = ({ planId, lenses, degraded, model, effort, tier, authority, walk, cwd, env, timestamp, ctx }) => {
|
|
895
1022
|
const tree = treeContext(cwd);
|
|
896
1023
|
const records = readStoreRecords(cwd, env);
|
|
897
1024
|
const { state } = chainContext(records, planId, tree.owner);
|
|
898
1025
|
openStepState(state, planId, 'an internal-attestation binds an open step\'s round (#28); open the step\'s round first');
|
|
899
1026
|
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
readFile: ctx.readFileSync ?? readFileSync
|
|
903
|
-
|
|
904
|
-
const uncovered = coverage.filter((p) => !p.covered);
|
|
1027
|
+
const adopted = walk === undefined ? null : resolveAdoptedPlan({ root, records, planId, owner: tree.owner, arm: 'internal-attestation --walk' });
|
|
1028
|
+
const planCoverage = computePlanAdoptionCoverage({ root, plans: plansInFlight(root), records, owner: tree.owner,
|
|
1029
|
+
readFile: ctx.readFileSync ?? readFileSync });
|
|
1030
|
+
const uncovered = planCoverage.filter((p) => !p.covered);
|
|
905
1031
|
if (uncovered.length > 0) {
|
|
906
1032
|
throw refuse(`internal-attestation refuses (#68): ${uncovered.map((p) => `plan ${quoteReportName(p.plan)} — ${p.reason}`).join('; ')} — an uncovered in-flight plan is a refusal, never a relaxation`);
|
|
907
1033
|
}
|
|
1034
|
+
const checkedWalk = walk === undefined ? undefined : (() => {
|
|
1035
|
+
const shape = checkPlanStructure(adopted.planText, {});
|
|
1036
|
+
const list = readShippedRobustnessLiterals();
|
|
1037
|
+
if (shape.findings.length > 0) throw usageFail('--walk cannot measure an adopted plan that fails structural validation');
|
|
1038
|
+
const patterns = shape.parsed.rows.filter((row) => row.valid && isSweep(row.path) && !isCoverageDocument(row.path)).map((row) => row.path);
|
|
1039
|
+
const expansions = patterns.length === 0 ? {} : expandSweepPaths(root, patterns);
|
|
1040
|
+
const measured = computeRowCoverage(adopted.planText, list, { expandRowPath: (path) => expansions[path] ?? [],
|
|
1041
|
+
readRowFile: (path) => readFileBytesNoFollowCapped(join(root, path), PLAN_READ_CAP) });
|
|
1042
|
+
if (measured.refusal) {
|
|
1043
|
+
const detail = measured.refusal.reason === undefined
|
|
1044
|
+
? `path ${escapeForDisplay(measured.refusal.path)}: ${measured.refusal.outcome}`
|
|
1045
|
+
: measured.refusal.reason;
|
|
1046
|
+
throw usageFail(`--walk coverage refuses row ${measured.refusal.id}: ${detail}`);
|
|
1047
|
+
}
|
|
1048
|
+
const validated = validateWalk(readWalkInput(cwd, walk), { listVersion: list.version, rows: measured.rows });
|
|
1049
|
+
if (!validated.ok) throw usageFail(`--walk ${validated.reason}`);
|
|
1050
|
+
return validated.walk;
|
|
1051
|
+
})();
|
|
908
1052
|
return {
|
|
909
1053
|
schema: FLOW_SCHEMA_VERSION, kind: 'internal-attestation', fingerprint: tree.fingerprint,
|
|
910
1054
|
planId, stepId: state.stepId, cycle: state.cycle, round: state.round,
|
|
911
1055
|
lenses, degraded, posture: { model, effort: effort ?? null, tier: tier ?? null }, authority,
|
|
912
|
-
base: tree.base, timestamp,
|
|
1056
|
+
base: tree.base, timestamp, ...(checkedWalk === undefined ? {} : { walk: checkedWalk }),
|
|
913
1057
|
};
|
|
914
1058
|
};
|
|
915
1059
|
|
|
@@ -994,12 +1138,14 @@ Usage:
|
|
|
994
1138
|
node flow-writer.mjs consult-attestation <planId> --backend <name> --nonce <nonce> --proposed-fix-digest <digest>
|
|
995
1139
|
node flow-writer.mjs round-open <planId> --backend <name> [--backend <name> ...] [--step <stepId>] [--new-cycle] [--justification <text>]
|
|
996
1140
|
node flow-writer.mjs round-land <planId> [--dispose folded --finding <quote> --proof-kind consult-attestation|red-proof --proof-digest <digest>]
|
|
997
|
-
node flow-writer.mjs round-land <planId> [--dispose queued --finding <quote> --
|
|
1141
|
+
node flow-writer.mjs round-land <planId> [--dispose queued --finding <quote> --claim <invariant> --proof-kind consult-attestation|red-proof --proof-digest <digest>]
|
|
998
1142
|
node flow-writer.mjs round-land <planId> [--dispose rejected --finding <quote> --reason <text>]
|
|
1143
|
+
node flow-writer.mjs round-land <planId> [--dispose escalated --finding <quote> --override-digest <digest>]
|
|
1144
|
+
node flow-writer.mjs round-land <planId> [--dispose custody-lost --receipt <receiptDigest>]
|
|
999
1145
|
node flow-writer.mjs freeze <planId>
|
|
1000
1146
|
node flow-writer.mjs unfreeze <planId> [--justification <text>]
|
|
1001
1147
|
node flow-writer.mjs converged <planId>
|
|
1002
|
-
node flow-writer.mjs internal-attestation <planId> --lens <name> [--lens <name> ...] [--degraded <backend> ...] --model <model> [--effort <effort>] [--tier <tier>] --authority <text>
|
|
1148
|
+
node flow-writer.mjs internal-attestation <planId> --lens <name> [--lens <name> ...] [--degraded <backend> ...] --model <model> [--effort <effort>] [--tier <tier>] --authority <text> [--walk <file>]
|
|
1003
1149
|
node flow-writer.mjs write-plan-id <plan-file> --plan-id <id>
|
|
1004
1150
|
|
|
1005
1151
|
Operand shapes: a positional may follow a literal -- and a value flag accepts --flag=<value>
|
|
@@ -1075,7 +1221,8 @@ export const main = (argv, ctx = {}) => {
|
|
|
1075
1221
|
const lines = [
|
|
1076
1222
|
`flow-writer: appended chain/round (open) for plan "${planId}" — step "${r.stepId}" cycle ${r.cycle} round ${r.round} — digest ${canonicalFlowDigest(r)}`,
|
|
1077
1223
|
` store: ${appended.writtenPath}`,
|
|
1078
|
-
...(built.justification != null ? [` justification (Decision 8, over-cap mint — the record above is the durable trail): ${built.justification}`] : []),
|
|
1224
|
+
...(built.justification != null ? [built.walkLift ? ` justification (Decision 8, walk lift trail — ${built.justification}; ${built.walkRefusal}; the ABSENCE of that walk-carrying attestation is the lift's trail; the record above is the durable trail)` : ` justification (Decision 8, over-cap mint — the record above is the durable trail): ${built.justification}`] : []),
|
|
1225
|
+
...built.advisories.map((advisory) => ` ${advisory}`),
|
|
1079
1226
|
...r.dispatches.flatMap((d) => [
|
|
1080
1227
|
` dispatch backend=${d.backend} nonce=${d.dispatchNonce} watermark=${d.receiptWatermark}`,
|
|
1081
1228
|
` dispatch with --nonce ${d.dispatchNonce} on the wrapper invocation (the plain-argument lane onto the AW_REVIEW_NONCE seam) so the wrapper mints the {backend, nonce}-named finding manifest`,
|
|
@@ -1086,24 +1233,27 @@ export const main = (argv, ctx = {}) => {
|
|
|
1086
1233
|
}
|
|
1087
1234
|
|
|
1088
1235
|
if (arm === 'round-land') {
|
|
1089
|
-
const
|
|
1236
|
+
const legacy = rest.find((arg) => ['--debt-id', '--debt-digest'].includes(arg.split('=')[0]));
|
|
1237
|
+
if (legacy !== undefined) throw usageFail(`unknown argument "${legacy.split('=')[0]}"`);
|
|
1238
|
+
const { values, positionals } = parseFlags(rest, { '--dispose': 'value', '--finding': 'value', '--claim': 'value', '--proof-kind': 'value', '--proof-digest': 'value', '--reason': 'value', '--override-digest': 'value', '--receipt': 'value' });
|
|
1090
1239
|
const planId = onePlanId(positionals, arm);
|
|
1091
1240
|
const dispose = (() => {
|
|
1092
1241
|
if (values.dispose === undefined) {
|
|
1093
|
-
const stray = ['finding', 'proof-kind', 'proof-digest', '
|
|
1242
|
+
const stray = ['finding', 'claim', 'proof-kind', 'proof-digest', 'reason', 'override-digest', 'receipt'].find((f) => values[f] !== undefined);
|
|
1094
1243
|
if (stray !== undefined) throw usageFail(`--${stray} rides only a --dispose invocation`);
|
|
1095
1244
|
return null;
|
|
1096
1245
|
}
|
|
1097
1246
|
const action = values.dispose;
|
|
1098
|
-
if (!['folded', 'queued', 'rejected'].includes(action)) throw usageFail(`--dispose must be folded | queued | rejected (got "${action}")`);
|
|
1247
|
+
if (!['folded', 'queued', 'rejected', 'escalated', 'custody-lost'].includes(action)) throw usageFail(`--dispose must be folded | queued | rejected | escalated | custody-lost (got "${action}")`);
|
|
1099
1248
|
// G3 (round-2 fold): a flag of another disposition branch refuses as usage BEFORE any
|
|
1100
1249
|
// required-value error — an incompatible input is named, never silently dropped.
|
|
1101
|
-
const branchFlags = { folded: ['proof-kind', 'proof-digest'], queued: ['
|
|
1250
|
+
const branchFlags = { folded: ['finding', 'proof-kind', 'proof-digest'], queued: ['finding', 'claim', 'proof-kind', 'proof-digest'], rejected: ['finding', 'reason'], escalated: ['finding', 'override-digest'], 'custody-lost': ['receipt'] };
|
|
1102
1251
|
const allowedFlags = new Set(branchFlags[action]);
|
|
1103
|
-
const strayFlag = ['proof-kind', 'proof-digest', '
|
|
1252
|
+
const strayFlag = ['finding', 'claim', 'proof-kind', 'proof-digest', 'reason', 'override-digest', 'receipt'].find((f) => values[f] !== undefined && !allowedFlags.has(f));
|
|
1104
1253
|
if (strayFlag !== undefined) {
|
|
1105
|
-
throw usageFail(`--${strayFlag} does not ride --dispose ${action} — the ${action} arm takes exactly {
|
|
1254
|
+
throw usageFail(`--${strayFlag} does not ride --dispose ${action} — the ${action} arm takes exactly {${branchFlags[action].map((f) => `--${f}`).join(', ')}}`);
|
|
1106
1255
|
}
|
|
1256
|
+
if (action === 'custody-lost') return { action, receipt: requireDigest(requireValue(values, 'receipt', arm), 'receipt') };
|
|
1107
1257
|
const finding = requireValue(values, 'finding', arm);
|
|
1108
1258
|
// An empty quote must never reach the substring check — '' is a substring of EVERYTHING.
|
|
1109
1259
|
if (finding.length === 0) throw usageFail('--finding must be the non-empty quoted finding text (verbatim from the delivered manifest)');
|
|
@@ -1113,8 +1263,11 @@ export const main = (argv, ctx = {}) => {
|
|
|
1113
1263
|
return { action, finding, proofKind, proofDigest: requireDigest(requireValue(values, 'proof-digest', arm), 'proof-digest') };
|
|
1114
1264
|
}
|
|
1115
1265
|
if (action === 'queued') {
|
|
1116
|
-
|
|
1266
|
+
if (['claim', 'proof-kind', 'proof-digest'].some((flag) => values[flag] === undefined)) throw usageFail('--dispose queued requires --finding, --claim, --proof-kind and --proof-digest together');
|
|
1267
|
+
if (!['consult-attestation', 'red-proof'].includes(values['proof-kind'])) throw usageFail(`--proof-kind must be consult-attestation | red-proof (got "${values['proof-kind']}")`);
|
|
1268
|
+
return { action, finding, claim: requireValue(values, 'claim', arm), proofKind: values['proof-kind'], proofDigest: requireDigest(values['proof-digest'], 'proof-digest') };
|
|
1117
1269
|
}
|
|
1270
|
+
if (action === 'escalated') return { action, finding, overrideDigest: requireDigest(requireValue(values, 'override-digest', arm), 'override-digest') };
|
|
1118
1271
|
return { action, finding, reason: requireValue(values, 'reason', arm) };
|
|
1119
1272
|
})();
|
|
1120
1273
|
const built = buildRoundLand({ planId, dispose, cwd, env, timestamp });
|
|
@@ -1124,7 +1277,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
1124
1277
|
` store: ${appended.writtenPath}`,
|
|
1125
1278
|
...built.landed.map((l) => ` landed backend=${l.backend} nonce=${l.nonce}`),
|
|
1126
1279
|
...built.pending.map((p) => ` pending backend=${p.backend} nonce=${p.dispatchNonce} watermark=${p.receiptWatermark}`),
|
|
1127
|
-
...(built.disposed != null ? [` disposition ${built.disposed.action} findingDigest=${built.disposed.findingDigest}`] : []),
|
|
1280
|
+
...(built.disposed != null ? [` disposition ${built.disposed.action} ${built.disposed.receiptDigest ? `receiptDigest=${built.disposed.receiptDigest}` : `findingDigest=${built.disposed.findingDigest}`}`] : []),
|
|
1128
1281
|
];
|
|
1129
1282
|
return { code: 0, stdout: lines.join('\n'), stderr: '' };
|
|
1130
1283
|
}
|
|
@@ -1211,14 +1364,14 @@ export const main = (argv, ctx = {}) => {
|
|
|
1211
1364
|
});
|
|
1212
1365
|
}
|
|
1213
1366
|
if (arm === 'internal-attestation') {
|
|
1214
|
-
const { values, positionals } = parseFlags(rest, { '--lens': 'list', '--degraded': 'list', '--model': 'value', '--effort': 'value', '--tier': 'value', '--authority': 'value' });
|
|
1367
|
+
const { values, positionals } = parseFlags(rest, { '--lens': 'list', '--degraded': 'list', '--model': 'value', '--effort': 'value', '--tier': 'value', '--authority': 'value', '--walk': 'value' });
|
|
1215
1368
|
if (!Array.isArray(values.lens) || values.lens.length === 0) {
|
|
1216
1369
|
throw usageFail('internal-attestation requires at least one --lens <name> (the required-lens set, #28)');
|
|
1217
1370
|
}
|
|
1218
1371
|
return buildInternalAttestation({
|
|
1219
1372
|
planId: onePlanId(positionals, arm), lenses: values.lens, degraded: values.degraded ?? [],
|
|
1220
1373
|
model: requireValue(values, 'model', arm), effort: values.effort, tier: values.tier,
|
|
1221
|
-
authority: requireValue(values, 'authority', arm), cwd, env, timestamp, ctx,
|
|
1374
|
+
authority: requireValue(values, 'authority', arm), walk: values.walk, cwd, env, timestamp, ctx,
|
|
1222
1375
|
});
|
|
1223
1376
|
}
|
|
1224
1377
|
return null; // maintainer-override — handled below (it prints its bound set first)
|
package/tools/fold-scope.mjs
CHANGED
|
@@ -39,7 +39,7 @@ const ORIGIN_SHAPE = /^\S+:[1-9]\d*(\s|$)/;
|
|
|
39
39
|
const CLOSED_MARKERS = ['DONE', 'CLOSED'];
|
|
40
40
|
const ORIGIN_MISSING = 'origin (the canon requires a file:line)';
|
|
41
41
|
|
|
42
|
-
const normalize = (s) => String(s ?? '').replace(/\r/g, '').replace(/\s+/g, ' ').trim();
|
|
42
|
+
export const normalize = (s) => String(s ?? '').replace(/\r/g, '').replace(/\s+/g, ' ').trim();
|
|
43
43
|
const contains = (haystack, needle) => normalize(haystack).toLowerCase().includes(needle);
|
|
44
44
|
|
|
45
45
|
|
|
@@ -119,7 +119,7 @@ const EMPTY_ROW = () => ({ found: false, matches: 0, fields: {}, missing: [...RO
|
|
|
119
119
|
export const findDebtRow = (queueText, claim) => {
|
|
120
120
|
const needle = normalize(claim).toLowerCase();
|
|
121
121
|
if (!needle) return EMPTY_ROW();
|
|
122
|
-
const rows = topLevelRows(queueText).map((block) => ({ block, ...parseFields(block) }));
|
|
122
|
+
const rows = topLevelRows(queueText).map((block) => ({ block, ...parseFields(block.replace(/\r$/gm, '')) }));
|
|
123
123
|
// EVERY recorded value of `invariant` is a candidate key, not just the first: a claim sitting in a
|
|
124
124
|
// repeated label would otherwise be invisible here and resolve to some OTHER row.
|
|
125
125
|
const owns = (r) => (r.values.invariant ?? []).some((v) => contains(v, needle));
|
|
@@ -272,8 +272,12 @@ export const decideFoldScope = ({ cls, claim, planText, queueText } = {}) => {
|
|
|
272
272
|
' silence is not a declaration — state the disposition in the row.',
|
|
273
273
|
]);
|
|
274
274
|
}
|
|
275
|
-
|
|
275
|
+
const accepted = accept('new-invariant', claimText, [
|
|
276
276
|
` the queue row carries all five fields and declares its residual exposure NOT live: ${row.fields.origin}`,
|
|
277
277
|
' the narrow fix ships in this phase; ONLY the generalization defers.',
|
|
278
278
|
]);
|
|
279
|
+
const terminator = row.block.includes('\r\n') ? '\r\n' : '\n';
|
|
280
|
+
const block = row.block.replace(/(?:\r?\n[ \t]*)+$/, terminator);
|
|
281
|
+
accepted.row = { firstLine: block.split('\n')[0].replace(/\r$/, ''), block };
|
|
282
|
+
return accepted;
|
|
279
283
|
};
|