@sabaiway/agent-workflow-kit 13.0.0 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +9 -9
- package/references/modes/review-state.md +1 -1
- package/tools/dispatch-baseline.mjs +172 -0
- package/tools/dispatch-record.mjs +28 -1
- package/tools/dispatch-store-read.mjs +6 -1
- package/tools/dispatch-store.mjs +4 -3
- package/tools/dispatch.mjs +110 -71
- package/tools/exec-producer.mjs +57 -37
- package/tools/held-session.mjs +22 -9
- package/tools/review-state.mjs +9 -5
|
@@ -131,13 +131,21 @@ const short = (v) => {
|
|
|
131
131
|
// ── the closed vocabulary ─────────────────────────────────────────────────────────────────────────
|
|
132
132
|
|
|
133
133
|
export const DELEGATION_SCHEMA_VERSION = 1;
|
|
134
|
+
export const BASELINE_KINDS = deepFreeze(['head', 'checkpoint']);
|
|
135
|
+
export const HEAD_BASELINE = deepFreeze({ kind: 'head', treeOid: null });
|
|
136
|
+
export const OPTIONAL_FIELDS = deepFreeze({ dispatch: ['baseline'] });
|
|
137
|
+
const TREE_OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
138
|
+
export const isTreeOid = (v) => typeof v === 'string' && TREE_OID_RE.test(v);
|
|
139
|
+
export const readsBaseline = (dispatch) => Object.hasOwn(dispatch, 'baseline')
|
|
140
|
+
? { kind: dispatch.baseline.kind, treeOid: dispatch.baseline.treeOid }
|
|
141
|
+
: { ...HEAD_BASELINE };
|
|
134
142
|
|
|
135
143
|
export const DELEGATION_KINDS = deepFreeze(['pre-registration', 'dispatch', 'return', 'fold', 'observation', 'degrade']);
|
|
136
144
|
|
|
137
145
|
// The closed key set per kind (schema + kind are implicit on every record).
|
|
138
146
|
export const DELEGATION_KEY_SETS = deepFreeze({
|
|
139
147
|
'pre-registration': ['waveId', 'stepClasses', 'pairingKey', 'minPerClass', 'meanLThreshold', 'firstPassNum', 'firstPassDen', 'timestamp'],
|
|
140
|
-
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp'],
|
|
148
|
+
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp', 'baseline'],
|
|
141
149
|
return: ['role', 'backend', 'nonce', 'contractDigest', 'preTreeDigest', 'postTreeDigest', 'diffDigest', 'diffLength', 'reportDigest', 'reportLength', 'bundleDigest', 'bundleLength', 'metric', 'outcome', 'exitStatus', 'sessionId', 'wrapperVersion', 'posture', 'timestamp'],
|
|
142
150
|
fold: ['nonce', 'returnDigest', 'treeDigestAtFold', 'verdict', 'timestamp'],
|
|
143
151
|
observation: ['waveId', 'stepClass', 'scope', 'metric', 'planId', 'phase', 'timestamp'],
|
|
@@ -217,6 +225,7 @@ const namesAnObject = (kind) => kind !== 'gate-output';
|
|
|
217
225
|
// ── nested closed forms ───────────────────────────────────────────────────────────────────────────
|
|
218
226
|
|
|
219
227
|
const VEHICLE_KEYS = ['requested', 'selected'];
|
|
228
|
+
const BASELINE_KEYS = ['kind', 'treeOid'];
|
|
220
229
|
const POSTURE_KEYS = ['model', 'effort', 'tier'];
|
|
221
230
|
const METRIC_KEYS = ['numeratorBytes', 'denominatorBytes', 'components', 'provenance', 'eligible', 'ineligibleReason'];
|
|
222
231
|
const COMPONENT_KEYS = ['kind', 'path', 'objectId', 'bytes'];
|
|
@@ -235,6 +244,18 @@ const checkClosedKeys = (at, value, keys) => {
|
|
|
235
244
|
return { ok: true };
|
|
236
245
|
};
|
|
237
246
|
|
|
247
|
+
const validateBaseline = (at, baseline) => {
|
|
248
|
+
const closed = checkClosedKeys(at, baseline, BASELINE_KEYS);
|
|
249
|
+
if (!closed.ok) return closed;
|
|
250
|
+
if (!BASELINE_KINDS.includes(baseline.kind)) return refuse(`${at}: unknown kind ${short(baseline.kind)} — expected ${BASELINE_KINDS.join(' | ')}`);
|
|
251
|
+
if (baseline.treeOid !== null && (typeof baseline.treeOid !== 'string' || !TREE_OID_RE.test(baseline.treeOid))) {
|
|
252
|
+
return refuse(`${at}: treeOid must be null or exactly 40 or 64 lowercase hex characters (got ${short(baseline.treeOid)})`);
|
|
253
|
+
}
|
|
254
|
+
if (baseline.kind === 'head' && baseline.treeOid !== null) return refuse(`${at}: head requires treeOid null`);
|
|
255
|
+
if (baseline.kind === 'checkpoint' && baseline.treeOid === null) return refuse(`${at}: checkpoint requires treeOid to be non-null`);
|
|
256
|
+
return { ok: true };
|
|
257
|
+
};
|
|
258
|
+
|
|
238
259
|
const validateVehicle = (at, vehicle) => {
|
|
239
260
|
const closed = checkClosedKeys(at, vehicle, VEHICLE_KEYS);
|
|
240
261
|
if (!closed.ok) return closed;
|
|
@@ -324,6 +345,7 @@ const FIELD_CHECKS = {
|
|
|
324
345
|
firstPassNum: { ok: isByteCount, want: 'a non-negative integer first-pass numerator' },
|
|
325
346
|
firstPassDen: { ok: (v) => Number.isSafeInteger(v) && v >= 1, want: 'a positive integer first-pass denominator' },
|
|
326
347
|
vehicle: { ok: isPlainObject, want: 'the closed vehicle pair {requested, selected}' },
|
|
348
|
+
baseline: { ok: isPlainObject, want: 'the closed baseline pair {kind, treeOid}' },
|
|
327
349
|
// The safe token grammar, not merely non-empty: the backend NAMES an artifact beside the store
|
|
328
350
|
// (the exec receipt and its report), so a name outside this grammar records a dispatch whose own
|
|
329
351
|
// receipt could never be written. One grammar on both sides, or the ledger accepts what the
|
|
@@ -375,6 +397,7 @@ const checkFields = (kind, record) => {
|
|
|
375
397
|
return refuse(`${kind}: unknown field "${stray}" — the key set is closed (the canonical digest is identity; a stray key would fork it)`);
|
|
376
398
|
}
|
|
377
399
|
for (const field of fields) {
|
|
400
|
+
if (!Object.hasOwn(record, field) && (OPTIONAL_FIELDS[kind] ?? []).includes(field)) continue;
|
|
378
401
|
if (!own.includes(field)) {
|
|
379
402
|
return refuse(`${kind}: missing field "${field}" — every field of the closed set is pinned as an OWN ENUMERABLE key (the digest domain)`);
|
|
380
403
|
}
|
|
@@ -492,6 +515,10 @@ export const validateDelegationRecord = (record) => {
|
|
|
492
515
|
}
|
|
493
516
|
if (record.kind === 'dispatch') {
|
|
494
517
|
const vehicle = validateVehicle('dispatch: vehicle', record.vehicle);
|
|
518
|
+
if (Object.hasOwn(record, 'baseline')) {
|
|
519
|
+
const baseline = validateBaseline('dispatch: baseline', record.baseline);
|
|
520
|
+
if (!baseline.ok) return baseline;
|
|
521
|
+
}
|
|
495
522
|
return vehicle.ok ? validateDispatchCrossFields(record) : vehicle;
|
|
496
523
|
}
|
|
497
524
|
if (record.kind === 'return') return validateReturnCrossFields(record);
|
|
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
validateDelegationRecord, canonicalDelegationDigest, allowedSuccessorKinds,
|
|
6
|
-
isThreadTerminalRecord,
|
|
6
|
+
isThreadTerminalRecord, readsBaseline,
|
|
7
7
|
} from './dispatch-record.mjs';
|
|
8
8
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
9
9
|
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
@@ -175,6 +175,11 @@ export const delegationSemanticPreflight = ({ records, snapshot, storePath }) =>
|
|
|
175
175
|
if (snapshot.stepClass !== prior.dispatch.stepClass) {
|
|
176
176
|
throw stop(`refusing a retry: it declares step class "${snapshot.stepClass}" but its retry origin "${snapshot.retryOf}" was dispatched as "${prior.dispatch.stepClass}" — the pairing key is the step class, so a chain that changes it mid-way would split one attempt's accounting across two classes; nothing was written`);
|
|
177
177
|
}
|
|
178
|
+
const base = readsBaseline(snapshot);
|
|
179
|
+
const priorBase = readsBaseline(prior.dispatch);
|
|
180
|
+
if (base.kind !== priorBase.kind || base.treeOid !== priorBase.treeOid) {
|
|
181
|
+
throw stop(`refusing a retry: its base {kind "${base.kind}", treeOid ${base.treeOid}} differs from its retry origin "${snapshot.retryOf}"'s {kind "${priorBase.kind}", treeOid ${priorBase.treeOid}} — a retry chain is measured against ONE base; nothing was written`);
|
|
182
|
+
}
|
|
178
183
|
const origin = findRetryChainOrigin(records, prior.dispatch);
|
|
179
184
|
if (snapshot.retryIndex > origin.retryCap) {
|
|
180
185
|
throw stop(`refusing a retry: retryIndex ${snapshot.retryIndex} exceeds the retryCap ${origin.retryCap} recorded on the thread's ORIGIN dispatch ("${origin.nonce}") — a fresh contract never manufactures a fresh retry budget; nothing was written`);
|
package/tools/dispatch-store.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// The read half lives in dispatch-store-read.mjs so read-only consumers never reach this writer.
|
|
3
3
|
|
|
4
4
|
import { computeTreeFingerprint } from './core-evidence.mjs';
|
|
5
|
-
import { validateDelegationRecord } from './dispatch-record.mjs';
|
|
5
|
+
import { validateDelegationRecord, HEAD_BASELINE } from './dispatch-record.mjs';
|
|
6
|
+
import { computeBaseFingerprint } from './dispatch-baseline.mjs';
|
|
6
7
|
import {
|
|
7
8
|
delegationStoreStop,
|
|
8
9
|
resolveDelegationStorePath,
|
|
@@ -30,8 +31,8 @@ export const resolveDelegationLockPath = (storePath) => `${storePath}${DELEGATIO
|
|
|
30
31
|
|
|
31
32
|
export const UNCOMMITTED_STATE_FINGERPRINT = 'the uncommitted-state fingerprint';
|
|
32
33
|
|
|
33
|
-
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx) => {
|
|
34
|
-
const fingerprint = computeTreeFingerprint(cwd, fsx);
|
|
34
|
+
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx, base = HEAD_BASELINE) => {
|
|
35
|
+
const fingerprint = base.kind === 'head' ? computeTreeFingerprint(cwd, fsx) : computeBaseFingerprint(cwd, base, fsx);
|
|
35
36
|
if (fingerprint == null) {
|
|
36
37
|
throw stop(`cannot compute ${UNCOMMITTED_STATE_FINGERPRINT} — not inside a git work tree (or a git probe failed); a record never carries a null tree digest (fail closed)`);
|
|
37
38
|
}
|
package/tools/dispatch.mjs
CHANGED
|
@@ -60,9 +60,10 @@
|
|
|
60
60
|
//
|
|
61
61
|
// Writer: appends to the delegation ledger through the store's lock-serialized append (the store's
|
|
62
62
|
// preflight is the single legality door — this module adds NO second validator). Never commits,
|
|
63
|
-
// never runs a subscription CLI, spawns nothing but git READS — with
|
|
63
|
+
// never runs a subscription CLI, spawns nothing but git READS — with TWO stated exceptions:
|
|
64
64
|
// `handoff-return` attests MAIN's index with `git write-tree`, which may write a tree OBJECT into
|
|
65
|
-
// the odb
|
|
65
|
+
// the odb (the same probe `land --prepare` itself uses); checkpoint measurement writes the leaf's
|
|
66
|
+
// OWN temporary index and forced-add blob objects — never a ref, never the real index. Dependency-free,
|
|
66
67
|
// Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
67
68
|
|
|
68
69
|
import { readFileSync, realpathSync, readlinkSync } from 'node:fs';
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
DELEGATION_SCHEMA_VERSION, STEP_CLASSES, OBSERVATION_PROVENANCE, RETURN_OUTCOMES,
|
|
74
75
|
SESSION_ID_NULLABLE_OUTCOMES, checkDispatchContractForm, checkDispatchMintConsistency,
|
|
75
76
|
contractDigest, canonicalDelegationDigest, computeNumerator, evaluateMetricEligibility,
|
|
77
|
+
HEAD_BASELINE, readsBaseline, isTreeOid,
|
|
76
78
|
} from './dispatch-record.mjs';
|
|
77
79
|
import {
|
|
78
80
|
appendDelegationRecord, readDelegationStore, resolveDelegationStorePath, delegationThreadState,
|
|
@@ -86,7 +88,11 @@ import { execReceiptBasename, execReportBasename, parseExecReceipt } from './exe
|
|
|
86
88
|
import {
|
|
87
89
|
enumerateReturnedObjects, computeReturnedDiff, assembleIntegrationBundle,
|
|
88
90
|
} from './exec-producer.mjs';
|
|
89
|
-
import { gitBuf
|
|
91
|
+
import { gitBuf } from './core-evidence.mjs';
|
|
92
|
+
import { isCleanAgainstBase, resolveTreeObject } from './dispatch-baseline.mjs';
|
|
93
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
94
|
+
import { judgeLedger, threadVerdict, HELD_RECEIPT_BACKEND } from './held-session.mjs';
|
|
95
|
+
import { degradeRecordSet, selectHeldSessionDegrades } from './review-state.mjs';
|
|
90
96
|
import { lstatNoFollowRead, readRegularFileNoFollow, readFileBytesNoFollow } from './fs-read-nofollow.mjs';
|
|
91
97
|
import { gitLine } from './flow-store-read.mjs';
|
|
92
98
|
import {
|
|
@@ -98,7 +104,7 @@ const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 }
|
|
|
98
104
|
|
|
99
105
|
// The ONE contract sentence, doc-parity-bound into references/modes/dispatch.md: the FORM-only limit
|
|
100
106
|
// and the aggregator's refusals are what a reader must not be able to mis-learn from the mode doc.
|
|
101
|
-
export const DISPATCH_CONTRACT = 'the contract check is FORM-only — fields present, grammars respected, never boundedness, design-decidedness or acceptance adequacy — and `aggregate` REFUSES instead of computing acceptance for a wave with no pre-registration record, over an OPEN thread in scope, over a PRE-DISPATCH degrade that opens no thread, or across several waves with no `--wave`; the writer verbs add NO second legality door — the store\'s preflight is the only one, and its refusals travel verbatim — while `open` copies every mint-time field from the contract header and refuses a deadline below the wrapper cap plus the kill grace, `return` absorbs only a TERMINAL exec receipt whose backend, nonce and independently computed contractDigest match the dispatch it answers, and `fold` binds the CURRENT tree to the folded return\'s postTreeDigest, so a tree that moved between the two never folds, and `await` is satisfied ONLY by the TERMINAL exec receipt of its own dispatch\'s {backend, nonce} — never by a review receipt, a ledger line or a finding manifest — while an expiry names a supervision question and releases NO writer slot';
|
|
107
|
+
export const DISPATCH_CONTRACT = 'the contract check is FORM-only — fields present, grammars respected, never boundedness, design-decidedness or acceptance adequacy — and `aggregate` REFUSES instead of computing acceptance for a wave with no pre-registration record, over an OPEN thread in scope, over a PRE-DISPATCH degrade that opens no thread, or across several waves with no `--wave`; the writer verbs add NO second legality door — the store\'s preflight is the only one, and its refusals travel verbatim — while `open` copies every mint-time field from the contract header and refuses a deadline below the wrapper cap plus the kill grace, `return` absorbs only a TERMINAL exec receipt whose backend, nonce and independently computed contractDigest match the dispatch it answers, and `fold` binds the CURRENT tree to the folded return\'s postTreeDigest, so a tree that moved between the two never folds, and `await` is satisfied ONLY by the TERMINAL exec receipt of its own dispatch\'s {backend, nonce} — never by a review receipt, a ledger line or a finding manifest — while an expiry names a supervision question and releases NO writer slot; `open` records the thread\'s BASE (`head` by default, `checkpoint` under `--checkpoint <oid>`), `return` and `fold` measure against the base the dispatch recorded, never a flag, and the `fold` VERB itself refuses a checkpoint `code` thread the held-session judge names SUBSTITUTED in that thread\'s own verdict — a precondition like its hidden-path and content-blind guards, the store\'s preflight staying the only door on the record';
|
|
102
108
|
|
|
103
109
|
// ── the flag surface (the CLI tests pin it against the D3 key sets) ────────────────────────────────
|
|
104
110
|
// flag → the record field it decides. The fields NOT on a flag are DERIVED and listed beside them,
|
|
@@ -138,6 +144,7 @@ export const OPEN_FLAG_FIELDS = Object.freeze({
|
|
|
138
144
|
'--backend': 'backend',
|
|
139
145
|
'--rationale': 'rationale',
|
|
140
146
|
'--retry-of': 'retryOf',
|
|
147
|
+
'--checkpoint': 'baseline.treeOid',
|
|
141
148
|
});
|
|
142
149
|
// COPIED from the contract header at mint and bound by contractDigest (D3): a dispatch that
|
|
143
150
|
// disagreed with the header it claims to carry is refused by checkDispatchMintConsistency.
|
|
@@ -525,7 +532,14 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
525
532
|
// and a verb should refuse in its own words before another module has to. Not decidable is never
|
|
526
533
|
// CLEAN: baselineClean:false is what makes the eventual return honestly ineligible, and guessing
|
|
527
534
|
// true would let an unattributable metric into the acceptance number.
|
|
528
|
-
const
|
|
535
|
+
const oid = values['--checkpoint'];
|
|
536
|
+
if (oid !== undefined) {
|
|
537
|
+
if (!isTreeOid(oid)) return refusal('open', `--checkpoint "${oid}" is not a git object id of exactly 40 or 64 lowercase hex characters; nothing was written`);
|
|
538
|
+
const resolved = resolveTreeObject(cwd, oid);
|
|
539
|
+
if (!resolved.ok) return refusal('open', resolved.reason);
|
|
540
|
+
}
|
|
541
|
+
const base = oid === undefined ? HEAD_BASELINE : { kind: 'checkpoint', treeOid: oid };
|
|
542
|
+
const baselineClean = isCleanAgainstBase(cwd, base);
|
|
529
543
|
if (baselineClean === null) {
|
|
530
544
|
return refusal('open', 'the working state could not be probed, so the baseline is undecidable — a dispatch never records a guessed baseline, and outside a git work tree there is no tree to fingerprint either (fail closed); nothing was written');
|
|
531
545
|
}
|
|
@@ -539,12 +553,12 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
539
553
|
// with another run's evidence.
|
|
540
554
|
const leftover = existingArtifactRefusal(dirname(resolveDelegationStorePath(cwd, env)), need(values, '--backend'), contract.nonce);
|
|
541
555
|
if (leftover !== null) return refusal('open', `${leftover}; nothing was written`);
|
|
542
|
-
// The baseline is a RECORDED CLAIM about this tree, so it is refused before it is made: a
|
|
543
|
-
// conceals a change
|
|
556
|
+
// The baseline is a RECORDED CLAIM about this tree, so it is refused before it is made: under a head
|
|
557
|
+
// base a tree that conceals a change probes CLEAN and then hands the concealed change to the
|
|
544
558
|
// delegate's account at return time. `open` is where that lie is cheapest to catch.
|
|
545
559
|
const honest = hiddenFromPlainDiff(top);
|
|
546
560
|
if (!honest.ok) return refusal('open', `${honest.reason}; nothing was written`);
|
|
547
|
-
const preTreeDigest = uncommittedStateFingerprint(cwd);
|
|
561
|
+
const preTreeDigest = uncommittedStateFingerprint(cwd, undefined, base);
|
|
548
562
|
const record = {
|
|
549
563
|
schema: DELEGATION_SCHEMA_VERSION,
|
|
550
564
|
kind: 'dispatch',
|
|
@@ -556,6 +570,7 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
556
570
|
contractDigest: contractDigest(contract),
|
|
557
571
|
preTreeDigest,
|
|
558
572
|
baselineClean,
|
|
573
|
+
baseline: { kind: base.kind, treeOid: base.treeOid },
|
|
559
574
|
deadlineS: contract.deadlineS,
|
|
560
575
|
retryOf: values['--retry-of'] ?? null,
|
|
561
576
|
retryIndex: contract.retry.index,
|
|
@@ -576,7 +591,7 @@ const runOpen = ({ baseCwd, env, argv, now }) => {
|
|
|
576
591
|
const { writtenPath } = appendDelegationRecord({ cwd, record, env });
|
|
577
592
|
return {
|
|
578
593
|
code: 0,
|
|
579
|
-
stdout: `dispatch open: thread "${record.nonce}" opened in wave "${record.waveId}" — class ${record.stepClass} · backend ${record.backend} · vehicle ${record.vehicle.requested} → ${record.vehicle.selected} · deadline ${record.deadlineS}s (floor ${floorS}s) · retry ${record.retryIndex}/${record.retryCap} · baseline ${baselineClean ? 'CLEAN' : 'DIRTY — the return will be metric-INELIGIBLE (dirty-baseline)'} → ${writtenPath}`,
|
|
594
|
+
stdout: `dispatch open: thread "${record.nonce}" opened in wave "${record.waveId}" — class ${record.stepClass} · backend ${record.backend} · vehicle ${record.vehicle.requested} → ${record.vehicle.selected} · deadline ${record.deadlineS}s (floor ${floorS}s) · retry ${record.retryIndex}/${record.retryCap} · baseline ${baselineClean ? (base.kind === 'checkpoint' ? `CLEAN against checkpoint ${base.treeOid}` : 'CLEAN') : 'DIRTY — the return will be metric-INELIGIBLE (dirty-baseline)'} → ${writtenPath}`,
|
|
580
595
|
stderr: '',
|
|
581
596
|
};
|
|
582
597
|
};
|
|
@@ -741,17 +756,9 @@ const runAwait = async ({ baseCwd, env, argv, now, sleep, pollMs }) => {
|
|
|
741
756
|
|
|
742
757
|
// ── return: the wrapper's exec receipt, ABSORBED ──────────────────────────────────────────────────
|
|
743
758
|
|
|
744
|
-
// The return-time guard the Phase-1 council required. `git diff` SKIPS index entries
|
|
745
|
-
// assume-unchanged or skip-worktree
|
|
746
|
-
//
|
|
747
|
-
// payload the denominator is framed from run the same plain probes. The kit already owns a probe
|
|
748
|
-
// that sees them (computeWorkingState forces --ignore-submodules=none and folds in flaggedIndexLag),
|
|
749
|
-
// so the absorb door compares the two views and refuses the DIFFERENCE by name.
|
|
750
|
-
//
|
|
751
|
-
// It is deliberately NOT a producer fix: D7 binds the numerator to computeFingerprintPayload's byte
|
|
752
|
-
// domain, and forcing the flags on one side only would let the numerator count objects the
|
|
753
|
-
// denominator cannot see — a worse failure than the blindness. The payload-side fix is queued as its
|
|
754
|
-
// own frozen-shared-surface change.
|
|
759
|
+
// The return-time guard the Phase-1 council required. Under a head base `git diff` SKIPS index entries
|
|
760
|
+
// carrying assume-unchanged or skip-worktree, so a path can be CHANGED and invisible to the producer's
|
|
761
|
+
// enumeration and to the canonical payload alike — a tree that conceals a change is refused, never measured.
|
|
755
762
|
//
|
|
756
763
|
// The untracked section is excluded from the comparison because BOTH views read it with the same
|
|
757
764
|
// `ls-files --others --exclude-standard` probe: there is no divergence there to find. Both sides
|
|
@@ -759,17 +766,18 @@ const runAwait = async ({ baseCwd, env, argv, now, sleep, pollMs }) => {
|
|
|
759
766
|
// alone — and if it ever did, the answer would be a refusal, never a wrong number.
|
|
760
767
|
// A tree can LIE about itself in two independent ways, and neither is decidable by comparing two
|
|
761
768
|
// views that share the same blindness. So this asks two questions of its own, and every door that
|
|
762
|
-
// measures or binds a tree (`open`, `return`, `fold`) asks them
|
|
769
|
+
// measures or binds a tree (`open`, `return`, `fold`) asks them.
|
|
763
770
|
//
|
|
764
771
|
// (a) INDEX BITS, fail closed on their PRESENCE. `git ls-files -v` tags an assume-unchanged entry
|
|
765
|
-
// lowercase and a skip-worktree entry `S`, and the second one is the sharp case: a MISSING
|
|
772
|
+
// lowercase and a skip-worktree entry `S`, and the second one is the sharp case: under a head base a MISSING
|
|
766
773
|
// skip-worktree path is an ordinary sparse checkout to every probe this kit owns — probed live, the
|
|
767
774
|
// deletion of a materialized skip-worktree file is invisible to `computeWorkingState`, to the plain
|
|
768
775
|
// diff, to the tree fingerprint AND to the producer's enumeration. Nothing can compare its way to
|
|
769
776
|
// that, so the BIT is refused rather than its effect. It bites hardest at `open`: set the bit, delete
|
|
770
777
|
// the file, open (a FALSE clean baseline is recorded), then clear the bit — and the return credits the
|
|
771
778
|
// delegate with a deletion it never made. Refusing any tag but `H` also refuses an unmerged index,
|
|
772
|
-
// which is honest for a measurement
|
|
779
|
+
// which is honest for a measurement; a checkpoint measurement sees the deletion because its scope
|
|
780
|
+
// names the path from the base tree.
|
|
773
781
|
//
|
|
774
782
|
// (b) The --ignore-submodules axis, PER SIDE and on BYTES. Against the UNION of the two name lists a
|
|
775
783
|
// path visible on one side masks its own hidden state on the other; against DECODED names two
|
|
@@ -847,12 +855,12 @@ const canonicalStoreRefusal = (cwd, env) => {
|
|
|
847
855
|
return null;
|
|
848
856
|
};
|
|
849
857
|
|
|
850
|
-
//
|
|
851
|
-
// valid UTF-8 are gone before any framing question arises — probed, two links whose targets are the
|
|
852
|
-
// single bytes 0xff and 0xfe produce the SAME payload and the SAME enumeration, so swapping one for
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
// narrow case where the payload provably cannot follow one object's bytes.
|
|
858
|
+
// Under a head base an UNTRACKED symlink's TARGET is read by the payload with `readlink` as a STRING, so bytes that
|
|
859
|
+
// are not valid UTF-8 are gone before any framing question arises — probed, two untracked links whose targets are the
|
|
860
|
+
// single bytes 0xff and 0xfe produce the SAME payload and the SAME enumeration, so swapping one for the other after a
|
|
861
|
+
// return is invisible to the fold. A TRACKED link's target rides `git diff` RAW, and a checkpoint payload carries it
|
|
862
|
+
// too, so this is not a fourth blind CLASS: it is the CONSERVATIVE refusal over the class, holding under either base
|
|
863
|
+
// by one rule, for the narrow case where the payload provably cannot follow one object's bytes.
|
|
856
864
|
// ENOENT is the ONLY error that means "there is nothing to check here" — a deleted link, whose bytes
|
|
857
865
|
// ride the diff exactly. EVERY other errno refuses, EINVAL emphatically included: the producer labels
|
|
858
866
|
// an object `symlink` when ANY layer carries mode 120000, so a committed symlink REPLACED by a binary
|
|
@@ -876,7 +884,7 @@ const symlinkTargetRefusal = (top, entries, io = {}) => {
|
|
|
876
884
|
if (!Buffer.from(target.toString('utf8'), 'utf8').equals(target)) lost.push(`${entry.path} -> 0x${target.toString('hex')}`);
|
|
877
885
|
}
|
|
878
886
|
if (lost.length === 0) return null;
|
|
879
|
-
return `${lost.length} symlink target(s) are not valid UTF-8 (${lost.join(', ')}) — the
|
|
887
|
+
return `${lost.length} symlink target(s) are not valid UTF-8 (${lost.join(', ')}) — refused conservatively: the head payload reads an UNTRACKED link's target as a STRING and folds those bytes to U+FFFD, so this lane is fail-closed for them under either base`;
|
|
880
888
|
};
|
|
881
889
|
|
|
882
890
|
// NUL-delimited git output, kept as BUFFER segments — a decoded split cannot be undone.
|
|
@@ -919,7 +927,7 @@ export const hiddenFromPlainDiff = (top) => {
|
|
|
919
927
|
.filter((seg) => seg.length > 2 && String.fromCharCode(seg[0]) !== LS_FILES_CACHED_TAG)
|
|
920
928
|
.map((seg) => `${String.fromCharCode(seg[0])} ${showPath(seg.subarray(2))}`);
|
|
921
929
|
if (flagged.length > 0) {
|
|
922
|
-
return { ok: false, reason: `${flagged.length} index entr(ies) carry a tag other than "${LS_FILES_CACHED_TAG}" (${flagged.join('; ')}) — an assume-unchanged or skip-worktree entry makes the index lie about the worktree, and the sharpest case is invisible to EVERY probe this kit owns: deleting a materialized skip-worktree file changes no diff, no fingerprint and no enumeration, so a delegated measurement over such a tree is not honest at all; clear the bits (git update-index --no-assume-unchanged / --no-skip-worktree) and try again` };
|
|
930
|
+
return { ok: false, reason: `${flagged.length} index entr(ies) carry a tag other than "${LS_FILES_CACHED_TAG}" (${flagged.join('; ')}) — an assume-unchanged or skip-worktree entry makes the index lie about the worktree, and under a head base the sharpest case is invisible to EVERY probe this kit owns: deleting a materialized skip-worktree file changes no diff, no fingerprint and no enumeration, so a delegated measurement over such a tree is not honest at all; clear the bits (git update-index --no-assume-unchanged / --no-skip-worktree) and try again` };
|
|
923
931
|
}
|
|
924
932
|
const key = (seg) => seg.toString('latin1');
|
|
925
933
|
const missing = (forced, plain) => {
|
|
@@ -936,15 +944,16 @@ export const hiddenFromPlainDiff = (top) => {
|
|
|
936
944
|
hiddenStaged.length > 0 ? `staged: ${hiddenStaged.map(showPath).join(', ')}` : null,
|
|
937
945
|
hiddenUnstaged.length > 0 ? `unstaged: ${hiddenUnstaged.map(showPath).join(', ')}` : null,
|
|
938
946
|
].filter((part) => part !== null).join('; ');
|
|
939
|
-
return { ok: false, reason: `${count} changed path(s) are HIDDEN from the plain git diff
|
|
947
|
+
return { ok: false, reason: `${count} changed path(s) are HIDDEN from the plain git diff the head-base metric is computed over (${where}) — clear the diff config and try again` };
|
|
940
948
|
};
|
|
941
949
|
|
|
942
950
|
// THE CONTENT-BLIND CLASSES, and the line between them and the merely ambiguous ones.
|
|
943
951
|
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
// • `binary` — the payload
|
|
947
|
-
//
|
|
952
|
+
// Under a head base, for three kinds the payload cannot be trusted to follow an object's bytes, so they can move
|
|
953
|
+
// underneath a digest — each arm probed live, and two of the three are CONSERVATIVE over their whole class:
|
|
954
|
+
// • `binary` — an UNTRACKED one reaches the payload as `untracked-binary:<path>`, its name alone, so
|
|
955
|
+
// neither a same-size mutation nor a SIZE change moves the digest; a TRACKED one's diff carries
|
|
956
|
+
// both blob ids on its `index` line, so that half IS followed and the refusal is the conservative one.
|
|
948
957
|
// • `non-regular` — `untracked-nonregular:<path>`, a name and nothing else.
|
|
949
958
|
// • `submodule` — the first transition to dirty moves the digest (the superproject line gains
|
|
950
959
|
// "-dirty") and NOTHING after it does: a second, different nested edit and a whole new nested
|
|
@@ -954,11 +963,11 @@ export const hiddenFromPlainDiff = (top) => {
|
|
|
954
963
|
// is SUBTRACTED: a change set carrying one is refused, by name, at both doors.
|
|
955
964
|
//
|
|
956
965
|
// Regular-file content and a symlink's TARGET are deliberately NOT here. They are present in the
|
|
957
|
-
// payload — only unframed, so adjacent entries can alias each other (the residual named in the mode
|
|
966
|
+
// payload — under a head base only unframed, so adjacent entries can alias each other (the residual named in the mode
|
|
958
967
|
// doc and in --help). Subtracting them would refuse `new` and `symlink`, which is every delegated
|
|
959
968
|
// change set there is, and the lane would measure nothing at all.
|
|
960
969
|
//
|
|
961
|
-
// Restoring the three means changing `computeFingerprintPayload` — a Plan-1 frozen surface the
|
|
970
|
+
// Restoring the three under a head base means changing `computeFingerprintPayload` — a Plan-1 frozen surface the
|
|
962
971
|
// REVIEW lane also binds — which is queued, not done here.
|
|
963
972
|
export const CONTENT_BLIND_KINDS = Object.freeze(['binary', 'non-regular', 'submodule']);
|
|
964
973
|
|
|
@@ -966,7 +975,7 @@ export const CONTENT_BLIND_KINDS = Object.freeze(['binary', 'non-regular', 'subm
|
|
|
966
975
|
// clean staged gitlink replacement carries exact OIDs in the payload; it is the nested state of an
|
|
967
976
|
// already-DIRTY one that becomes invisible, and no cheap probe separates the two at this door.
|
|
968
977
|
const CONTENT_BLIND_WHY = {
|
|
969
|
-
binary: 'the payload
|
|
978
|
+
binary: 'refused conservatively: an untracked binary reaches the head payload as its name only, so neither its content nor its size reaches its digest',
|
|
970
979
|
'non-regular': 'the payload holds its name only',
|
|
971
980
|
submodule: 'refused conservatively: a clean pointer change does carry its OIDs, but once the submodule is dirty the payload records nothing further about its nested state',
|
|
972
981
|
};
|
|
@@ -975,7 +984,7 @@ export const contentBlindRefusal = (entries) => {
|
|
|
975
984
|
const blind = entries.filter((e) => CONTENT_BLIND_KINDS.includes(e.kind));
|
|
976
985
|
if (blind.length === 0) return null;
|
|
977
986
|
const listed = blind.map((e) => `${e.kind} ${e.path} (${CONTENT_BLIND_WHY[e.kind]})`).join('; ');
|
|
978
|
-
return `the change set carries ${blind.length} object(s)
|
|
987
|
+
return `the change set carries ${blind.length} object(s) this lane will not measure (${listed}) — refused CONSERVATIVELY over the class, because for a blind one the bytes can move under a tree digest that cannot follow them and the numerator would count a size the fold's binding cannot re-confirm. This lane, under either base, is fail-closed for them until the shared payload can carry their content (a frozen surface the review lane binds too): keep such objects out of a measured dispatch, or close the thread with degrade`;
|
|
979
988
|
};
|
|
980
989
|
|
|
981
990
|
// Pre-spend: neither artifact name may be taken before the dispatch that will own it exists. The
|
|
@@ -1126,14 +1135,15 @@ const runReturn = ({ baseCwd, env, argv, now }) => {
|
|
|
1126
1135
|
// fingerprint is taken before the walks and the payload's own digest is compared against it after.
|
|
1127
1136
|
// A tree that moved between them would hand the numerator one change set and the denominator
|
|
1128
1137
|
// another, and the record would bind a postTreeDigest neither of them describes.
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1138
|
+
const base = readsBaseline(dispatch);
|
|
1139
|
+
const openingDigest = uncommittedStateFingerprint(cwd, undefined, base);
|
|
1140
|
+
const enumerated = enumerateReturnedObjects(cwd, { base });
|
|
1131
1141
|
if (!enumerated.ok) return refusal('return', enumerated.reason);
|
|
1132
1142
|
const opaque = contentBlindRefusal(enumerated.entries);
|
|
1133
1143
|
if (opaque !== null) return refusal('return', `${opaque}; nothing was written`);
|
|
1134
1144
|
const lostTarget = symlinkTargetRefusal(top, enumerated.entries);
|
|
1135
1145
|
if (lostTarget !== null) return refusal('return', `${lostTarget}; nothing was written`);
|
|
1136
|
-
const produced = computeReturnedDiff(cwd);
|
|
1146
|
+
const produced = computeReturnedDiff(cwd, { base });
|
|
1137
1147
|
if (!produced.ok) return refusal('return', produced.reason);
|
|
1138
1148
|
// The payload IS the diff and its digest IS the uncommitted-state fingerprint — one computation,
|
|
1139
1149
|
// so diffDigest and postTreeDigest are equal by construction rather than by coincidence.
|
|
@@ -1251,7 +1261,7 @@ const runFold = ({ baseCwd, env, argv, now }) => {
|
|
|
1251
1261
|
return refusal('fold', `nonce "${nonce}" carries no return to fold — a fold folds a RETURN, and the thread's last record is a ${state.last.kind}; nothing was written`);
|
|
1252
1262
|
}
|
|
1253
1263
|
// THE SAME hidden-path guard the absorb door runs, and for a sharper reason: `treeDigestAtFold` is
|
|
1254
|
-
// computed from the very payload that is blind to these paths, so a change made behind an index bit
|
|
1264
|
+
// computed from the very payload that is blind to these paths, so under a head base a change made behind an index bit
|
|
1255
1265
|
// between the return and the fold leaves the digest EQUAL and the fold would accept bytes nobody
|
|
1256
1266
|
// returned. The digest cannot catch what the payload cannot see; this guard is what does.
|
|
1257
1267
|
const hidden = hiddenFromPlainDiff(top);
|
|
@@ -1259,30 +1269,45 @@ const runFold = ({ baseCwd, env, argv, now }) => {
|
|
|
1259
1269
|
// …and the same subtraction: an object the payload holds by name alone can be mutated between the
|
|
1260
1270
|
// return and the fold without moving the digest this fold is about to bind. The enumeration runs
|
|
1261
1271
|
// here for that one question — the fold computes no metric of its own.
|
|
1262
|
-
const
|
|
1272
|
+
const base = readsBaseline(state.dispatch);
|
|
1273
|
+
const enumerated = enumerateReturnedObjects(cwd, { base });
|
|
1263
1274
|
if (!enumerated.ok) return refusal('fold', enumerated.reason);
|
|
1264
1275
|
const opaque = contentBlindRefusal(enumerated.entries);
|
|
1265
1276
|
if (opaque !== null) return refusal('fold', `${opaque}; nothing was written`);
|
|
1266
1277
|
const lostTarget = symlinkTargetRefusal(top, enumerated.entries);
|
|
1267
1278
|
if (lostTarget !== null) return refusal('fold', `${lostTarget}; nothing was written`);
|
|
1268
|
-
|
|
1269
|
-
|
|
1279
|
+
if (state.dispatch.stepClass === 'code' && base.kind === 'checkpoint') {
|
|
1280
|
+
const evidence = degradeRecordSet({ cwd, env, fingerprint: null });
|
|
1281
|
+
const facts = judgeLedger(readDelegationLedger(cwd, env), {
|
|
1282
|
+
backend: HELD_RECEIPT_BACKEND, degrades: selectHeldSessionDegrades(evidence.records),
|
|
1283
|
+
});
|
|
1284
|
+
if (facts.state === 'error') return refusal('fold', `${facts.reason} (fail closed); nothing was written`);
|
|
1285
|
+
const thread = threadVerdict(facts, nonce);
|
|
1286
|
+
if (thread?.substituted) {
|
|
1287
|
+
const unavailable = evidence.unavailable ? ' (the evidence store is unavailable: judged with no degrade record)' : '';
|
|
1288
|
+
return refusal('fold', `refusing to fold thread "${nonce}": a checkpoint-based code thread whose return rode session "${thread.actualId}" while the held session it was opened against was "${thread.expectedId}" — record its ledger degrade (dispatch degrade --wave ${state.dispatch.waveId} --nonce ${nonce} --step-class code --rationale "..."), then retry it (cap permitting) or open a new thread on the held session, and fold that; nothing was written${unavailable}`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
// WHAT THIS BINDING IS, exactly: EQUALITY OF THE VISIBLE PAYLOAD between the return and
|
|
1292
|
+
// the fold — never an identity of the tree. Under a HEAD base the payload is an
|
|
1270
1293
|
// unframed concatenation, so two DIFFERENT trees can produce the same bytes: probed live, one file
|
|
1271
1294
|
// containing the line `untracked:two.txt` yields the same fingerprint as a tree of two files, and a
|
|
1272
|
-
// symlink target can imitate the marker that opens the next untracked entry.
|
|
1273
|
-
// git-relevant mode, so making an untracked file executable moves nothing at all. Those are stated
|
|
1295
|
+
// symlink target can imitate the marker that opens the next untracked entry. Its UNTRACKED section
|
|
1296
|
+
// carries no git-relevant mode either, so making an untracked file executable moves nothing at all. Those are stated
|
|
1274
1297
|
// residual limits of a Plan-1 frozen surface the review lane binds too, queued as its own item —
|
|
1275
|
-
// not defects of this verb, and not something this verb may quietly claim away.
|
|
1298
|
+
// not defects of this verb, and not something this verb may quietly claim away. Under a checkpoint
|
|
1299
|
+
// base the payload is one indexed diff: a mode change moves it and no untracked marker line exists.
|
|
1276
1300
|
//
|
|
1277
1301
|
// Within that domain the binding does its job: a tree whose payload moved between the return and
|
|
1278
|
-
// the fold refuses, and the recovery is a fresh dispatch.
|
|
1302
|
+
// the fold refuses, and the recovery is a fresh dispatch. Under a HEAD base staging usually moves the payload — every
|
|
1279
1303
|
// change set carrying an untracked path does — which is why the fold precedes `git add -A` (D16).
|
|
1280
1304
|
// The one shape it does not move is a tracked-only change passing from the worktree into a CLEAN
|
|
1281
1305
|
// index: the payload concatenates the staged and unstaged diffs, so those bytes are identical
|
|
1282
1306
|
// either side of `git add` (the fingerprint's stated blindness to the index↔worktree split). The
|
|
1283
1307
|
// fold is honest there — identical payload bytes, identical content — so the rule is "the fold
|
|
1284
|
-
// precedes staging", never "staging refuses the fold by construction".
|
|
1285
|
-
|
|
1308
|
+
// precedes staging", never "staging refuses the fold by construction". A checkpoint measurement
|
|
1309
|
+
// never reads the real index, so staging moves nothing there; the fold still precedes `git add`.
|
|
1310
|
+
const treeDigestAtFold = uncommittedStateFingerprint(cwd, undefined, base);
|
|
1286
1311
|
const record = {
|
|
1287
1312
|
schema: DELEGATION_SCHEMA_VERSION,
|
|
1288
1313
|
kind: 'fold',
|
|
@@ -1575,7 +1600,7 @@ Usage:
|
|
|
1575
1600
|
[--denominator-bytes <n>] [--cwd <dir>]
|
|
1576
1601
|
node dispatch.mjs open --contract <dispatch-file> --wave <id> --backend <name>
|
|
1577
1602
|
--rationale <text> --wrapper-cap-s <n> --kill-grace-s <n>
|
|
1578
|
-
[--retry-of <nonce>] [--cwd <dir>]
|
|
1603
|
+
[--checkpoint <oid>] [--retry-of <nonce>] [--cwd <dir>]
|
|
1579
1604
|
node dispatch.mjs await --nonce <n> [--timeout <s>] [--cwd <dir>]
|
|
1580
1605
|
node dispatch.mjs return --nonce <n> [--outcome <o>]
|
|
1581
1606
|
[--no-receipt --exit-status <n>] [--cwd <dir>]
|
|
@@ -1624,6 +1649,9 @@ dirty one is recorded, which makes the eventual return metric-INELIGIBLE by name
|
|
|
1624
1649
|
deadlineS below --wrapper-cap-s plus --kill-grace-s (both explicit — the kit never reads another
|
|
1625
1650
|
package's default), REFUSES a tree that conceals a change (below), and surfaces every retry and wave
|
|
1626
1651
|
refusal from the store verbatim.
|
|
1652
|
+
--checkpoint <oid> records a resolvable TREE object of exactly 40 or 64 lowercase hex characters
|
|
1653
|
+
(a commit, blob, tag or unknown id refuses by name); the base rides the record and return/fold read
|
|
1654
|
+
it there, while a checkpoint CLEAN line names the base.
|
|
1627
1655
|
|
|
1628
1656
|
THE LEDGER a tree-binding verb uses is the CANONICAL one, exactly: <git common dir>/
|
|
1629
1657
|
agent-workflow-delegation.jsonl. A store inside the work tree is measured as part of the change set
|
|
@@ -1649,7 +1677,7 @@ without an answer never authorizes the next dispatch.
|
|
|
1649
1677
|
A CONCEALING TREE is refused at every door that measures or binds one (open, return, fold), because
|
|
1650
1678
|
the recorded baseline, the counted bytes and the folded identity are all claims about a tree that is
|
|
1651
1679
|
telling the truth. Two arms: any index entry whose ls-files tag is not "H" refuses on the BIT's
|
|
1652
|
-
presence — deleting a materialized skip-worktree file is invisible to every probe this kit owns, so
|
|
1680
|
+
presence — under a head base deleting a materialized skip-worktree file is invisible to every probe this kit owns, so
|
|
1653
1681
|
its effect cannot be compared for — and a per-side, BYTE-keyed comparison of the forced against the
|
|
1654
1682
|
plain diff refuses a path an ignore-submodules setting hides. Both name the path; the recovery is
|
|
1655
1683
|
git update-index --no-assume-unchanged / --no-skip-worktree, or clearing the diff config.
|
|
@@ -1659,7 +1687,7 @@ the TERMINAL receipt (a RESERVED one is a supervision question, never a timeout)
|
|
|
1659
1687
|
receipt's independently computed contractDigest, its {backend, nonce}, its capS+killGraceS against
|
|
1660
1688
|
the recorded deadlineS and its timestamp against the ABSOLUTE deadline (the dispatch's timestamp plus
|
|
1661
1689
|
deadlineS), re-verifies the report's digest and length, refuses a change set HIDDEN from the plain
|
|
1662
|
-
git diff the metric is computed over (assume-unchanged, skip-worktree, ignore-submodules), and then
|
|
1690
|
+
git diff the head-base metric is computed over (assume-unchanged, skip-worktree, ignore-submodules), and then
|
|
1663
1691
|
enumerates the returned objects and frames the canonical integration bundle — bracketed by the tree
|
|
1664
1692
|
fingerprint, so a tree that moves mid-computation refuses instead of mixing two change sets.
|
|
1665
1693
|
--outcome records an orchestrator judgment under the closed override form: a wrapper outcome either
|
|
@@ -1674,19 +1702,24 @@ diff, and no-op-diff where the tree is unchanged too, since the eligibility rule
|
|
|
1674
1702
|
first. With no artifact at all there is no honest return: close the thread with degrade.
|
|
1675
1703
|
|
|
1676
1704
|
fold appends the integration re-confirmation and CLOSES the thread. It runs the same hidden-path
|
|
1677
|
-
guard as return — a change made behind an index bit leaves the tree digest EQUAL, so the digest alone
|
|
1705
|
+
guard as return — under a head base a change made behind an index bit leaves the tree digest EQUAL, so the digest alone
|
|
1678
1706
|
cannot catch it — then computes the CURRENT tree digest, which the store binds to the folded return's
|
|
1679
|
-
postTreeDigest. What that binding IS, exactly: equality of the VISIBLE
|
|
1680
|
-
identity of the tree.
|
|
1707
|
+
postTreeDigest. What that binding IS, exactly: equality of the VISIBLE PAYLOAD, never an
|
|
1708
|
+
identity of the tree. Under a HEAD base the payload is an unframed concatenation, so two different trees can produce
|
|
1681
1709
|
the same bytes (a file's content or a symlink's target can imitate the marker line that opens the
|
|
1682
|
-
next untracked entry), and
|
|
1710
|
+
next untracked entry), and its UNTRACKED section carries no git-relevant mode, so making an untracked file executable
|
|
1683
1711
|
moves nothing. Those residuals belong to a frozen shared surface and are queued, not claimed away
|
|
1684
|
-
here.
|
|
1685
|
-
|
|
1712
|
+
here. Under a checkpoint base the payload is one indexed diff: a mode change moves it and no untracked
|
|
1713
|
+
marker line exists. Within that domain a tree whose payload moved refuses, and the recovery is a fresh dispatch,
|
|
1714
|
+
never a fold. Under a HEAD base staging usually moves the payload (every change set carrying an untracked path does),
|
|
1686
1715
|
so the fold precedes git add. The one shape staging does NOT move is a tracked-only change passing
|
|
1687
1716
|
into a CLEAN index — the payload concatenates the staged and unstaged diffs, so those bytes are
|
|
1688
1717
|
identical either side of git add. The fold stays honest there (same bytes, same content), which is
|
|
1689
1718
|
why the rule is "fold before staging", not "staging refuses the fold".
|
|
1719
|
+
A checkpoint measurement never reads the real index, so staging moves nothing there; the fold still
|
|
1720
|
+
precedes git add. Before binding the digest, fold refuses a checkpoint code thread whose own
|
|
1721
|
+
held-session verdict is SUBSTITUTED, naming its ledger degrade and retry as recovery, and fails
|
|
1722
|
+
closed on an error epoch or an unreadable ledger.
|
|
1690
1723
|
|
|
1691
1724
|
degrade appends the recorded no-fold closure, threaded (with --nonce) or PRE-DISPATCH (without). The
|
|
1692
1725
|
pre-dispatch form opens no nonce thread, so aggregate REFUSES the whole wave by name once one is
|
|
@@ -1734,19 +1767,25 @@ directory, named agent-workflow-exec-receipt-<backendLength>-<backend>-<nonce>.j
|
|
|
1734
1767
|
agent-workflow-exec-report-<backendLength>-<backend>-<nonce>.txt. Honest v1 limits: gate output is
|
|
1735
1768
|
never accounted (the wrapper's exit trap removes its trace, so no gate-output component is emitted);
|
|
1736
1769
|
a change set carrying a BINARY, non-regular or SUBMODULE object is REFUSED, because the shared
|
|
1737
|
-
payload
|
|
1738
|
-
dirty transition — so their bytes can move under a digest that cannot follow
|
|
1739
|
-
fail-closed for them until that frozen payload can carry it
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1770
|
+
head payload cannot be trusted to follow their bytes — an untracked binary by name only, a submodule
|
|
1771
|
+
nothing beyond its first dirty transition — so their bytes can move under a digest that cannot follow
|
|
1772
|
+
them, and the lane is fail-closed for them until that frozen payload can carry it; the binary and
|
|
1773
|
+
submodule arms are CONSERVATIVE over their class. STATED RESIDUALS on what it does accept:
|
|
1774
|
+
under a HEAD base the payload is UNFRAMED, so a file's content or a symlink's target can imitate the marker line that
|
|
1775
|
+
opens the next untracked entry and two different trees can share one fingerprint, and that section
|
|
1776
|
+
carries no git-relevant MODE, so making an untracked file executable moves nothing — both are queued
|
|
1777
|
+
against the head payload, not claimed away here. Under a checkpoint base the payload is one indexed
|
|
1778
|
+
diff: a mode change moves it and no untracked marker line exists. Under a head base a delegate's rename
|
|
1779
|
+
arrives as a deletion plus a creation, over-counting a plain rename; under a checkpoint base git's own
|
|
1780
|
+
rename detection applies inside the one base-relative layer and a detected rename is counted once.
|
|
1781
|
+
A receipt is forgeable exactly like every record here;
|
|
1744
1782
|
and D10 stands as a BAR, not a mechanism — at most one in-tree exec dispatch at a time, and nothing
|
|
1745
1783
|
refuses a second.
|
|
1746
1784
|
|
|
1747
|
-
Never commits, never runs a subscription CLI, spawns nothing but git READS —
|
|
1785
|
+
Never commits, never runs a subscription CLI, spawns nothing but git READS — with TWO stated exceptions: handoff-return,
|
|
1748
1786
|
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)
|
|
1787
|
+
no ref (the same probe land --prepare itself uses); checkpoint measurement writes the leaf's OWN
|
|
1788
|
+
temporary index file and forced-add blob objects — never a ref, never the real index. Exit codes: 0 success;
|
|
1750
1789
|
1 a refusal (store STOP verbatim, a form violation, an unreadable file, a supervision question); 2
|
|
1751
1790
|
usage; ${AWAIT_UNANSWERED_STATUS} an await that ended with no terminal receipt (the absolute deadline or the --timeout
|
|
1752
1791
|
bound) — its own status so a caller that BRANCHES on the code can tell it from a refusal; a caller
|