ultra-builder-pro-cli 5.0.0 → 6.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 +113 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +16 -38
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -315
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- package/skills/ultra-think/references/interaction-boundary.md +0 -42
|
@@ -1,1928 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
7
|
-
const { execFileSync } = require('node:child_process');
|
|
8
|
-
|
|
9
|
-
const REPORT_SCHEMA = 'ultra-primary-transfer-validation-v1';
|
|
10
|
-
const OFFER_SCHEMA = 'ultra-primary-transfer-offer-v1';
|
|
11
|
-
const ACK_SCHEMA = 'ultra-primary-transfer-ack-v1';
|
|
12
|
-
const RESULT_SCHEMA = 'ultra-primary-transfer-result-v1';
|
|
13
|
-
const RESULT_V2_SCHEMA = 'ultra-primary-transfer-result-v2';
|
|
14
|
-
const CLOSEOUT_SCHEMA = 'ultra-primary-transfer-closeout-v1';
|
|
15
|
-
// The existing external-review receipt schema this contract binds — never a
|
|
16
|
-
// second invented review schema (skills/ultra-plan/references/task-evidence-v2.md).
|
|
17
|
-
const EXTERNAL_RECEIPT_SCHEMA = 'ultra-external-review-receipt-v1';
|
|
18
|
-
const EXTERNAL_RECEIPT_KEYS = Object.freeze([
|
|
19
|
-
'$schema', 'reviewer', 'reviewer_role', 'task_id', 'change_id',
|
|
20
|
-
'reviewer_authority', 'reviewed_contract', 'subject', 'verdict', 'findings',
|
|
21
|
-
'timestamp',
|
|
22
|
-
]);
|
|
23
|
-
const EXTERNAL_SEVERITIES = Object.freeze(['P0', 'P1', 'P2', 'P3']);
|
|
24
|
-
// The task-context sections a prescribed closeout may rewrite; everything
|
|
25
|
-
// before the earliest of them stays byte-frozen across the closeout.
|
|
26
|
-
const CLOSEOUT_SECTION_HEADINGS = Object.freeze(['## Resume Note', '## Task Review', '## Completion']);
|
|
27
|
-
const HANDOFF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
28
|
-
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
29
|
-
const HEAD_PATTERN = /^[0-9a-f]{40}$/u;
|
|
30
|
-
const TERMINAL_STATES = Object.freeze(['completed', 'blocked', 'revoked', 'cancelled', 'failed']);
|
|
31
|
-
const RECEIPT_FILES = Object.freeze(['OFFER.json', 'ACK.json', 'RESULT.json', 'CLOSEOUT.json']);
|
|
32
|
-
const GIT_TIMEOUT_MS = 5000;
|
|
33
|
-
const GIT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
34
|
-
const MAX_RECEIPT_BYTES = 1024 * 1024;
|
|
35
|
-
const MAX_INPUT_BYTES = 8 * 1024 * 1024;
|
|
36
|
-
const MAX_HANDOFF_ENTRIES = 256;
|
|
37
|
-
const DIGEST_TOOL_TIMEOUT_MS = 120000;
|
|
38
|
-
const DIGEST_TOOL = path.resolve(__dirname, '..', '..', 'ultra-test', 'scripts', 'worktree_digest.cjs');
|
|
39
|
-
// One bounded mechanical primitive set, owned by the worktree-digest tool: the
|
|
40
|
-
// product-subject pathspec and the stable repository-contained file snapshot.
|
|
41
|
-
const { PRODUCT_PATHSPEC, streamStableRepositoryFile } = require(DIGEST_TOOL);
|
|
42
|
-
|
|
43
|
-
function diagnostic(code, message, location = null, severity = 'error') {
|
|
44
|
-
return { code, severity, message, location };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isPlainObject(value) {
|
|
48
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function nonemptyString(value) {
|
|
52
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stringArray(value) {
|
|
56
|
-
return Array.isArray(value) && value.length > 0 && value.every(nonemptyString);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizedRelativePath(value) {
|
|
60
|
-
if (!nonemptyString(value) || value.includes('\\') || path.posix.normalize(value) !== value) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
if (value === '.' || value.startsWith('/') || value.includes('../') || value === '..') {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
return !value.split('/').includes('..');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Typed mapping from the shared snapshot primitive's codes to receipt
|
|
70
|
-
// diagnostics. Every rejection keeps the primitive's own recovery text.
|
|
71
|
-
const SNAPSHOT_CODE_MAP = Object.freeze({
|
|
72
|
-
ULTRA_SNAPSHOT_PATH_ESCAPE: 'receipt_path_escape',
|
|
73
|
-
ULTRA_SNAPSHOT_PATH_ENCODING: 'receipt_path_escape',
|
|
74
|
-
ULTRA_SNAPSHOT_SYMLINK: 'receipt_unsafe',
|
|
75
|
-
ULTRA_SNAPSHOT_NOT_DIRECTORY: 'receipt_unsafe',
|
|
76
|
-
ULTRA_SNAPSHOT_UNSUPPORTED: 'receipt_unsafe',
|
|
77
|
-
ULTRA_SNAPSHOT_NOT_REGULAR: 'receipt_not_regular',
|
|
78
|
-
ULTRA_SNAPSHOT_TOO_LARGE: 'receipt_oversize',
|
|
79
|
-
ULTRA_SNAPSHOT_RESOURCE_LIMIT: 'receipt_oversize',
|
|
80
|
-
ULTRA_SNAPSHOT_REPLACED: 'receipt_replaced',
|
|
81
|
-
ULTRA_SNAPSHOT_CHANGED_DURING_OBSERVATION: 'receipt_replaced',
|
|
82
|
-
ULTRA_SNAPSHOT_UNREADABLE: 'receipt_unreadable',
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Stable, repository-contained, ordinary-file, no-follow read through the one
|
|
86
|
-
// shared bounded snapshot primitive: the repository-relative path is validated
|
|
87
|
-
// before any filesystem access, the complete parent chain is walked as
|
|
88
|
-
// ordinary non-symlink directories, the leaf opens O_NOFOLLOW|O_NONBLOCK, and
|
|
89
|
-
// fresh root rewalks replay every parent and file identity after reading.
|
|
90
|
-
function stableReadBytes(repoRoot, relative, maxBytes) {
|
|
91
|
-
const chunks = [];
|
|
92
|
-
try {
|
|
93
|
-
streamStableRepositoryFile(repoRoot, relative, (chunk) => chunks.push(Buffer.from(chunk)));
|
|
94
|
-
} catch (error) {
|
|
95
|
-
const message = error && error.message ? error.message : String(error);
|
|
96
|
-
const match = /^(ULTRA_SNAPSHOT_[A-Z_]+):/.exec(message);
|
|
97
|
-
const code = match ? SNAPSHOT_CODE_MAP[match[1]] || 'receipt_unreadable' : 'receipt_unreadable';
|
|
98
|
-
if (match && match[1] === 'ULTRA_SNAPSHOT_MISSING') return { missing: true };
|
|
99
|
-
throw { code, message };
|
|
100
|
-
}
|
|
101
|
-
const bytes = Buffer.concat(chunks);
|
|
102
|
-
if (bytes.length > maxBytes) {
|
|
103
|
-
throw {
|
|
104
|
-
code: 'receipt_oversize',
|
|
105
|
-
message: `ULTRA_RECEIPT_OVERSIZE: ${relative} exceeds the ${maxBytes}-byte receipt/input ceiling. Split or shrink the file, then re-run the validator.`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const text = bytes.toString('utf8');
|
|
109
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
110
|
-
throw {
|
|
111
|
-
code: 'receipt_encoding',
|
|
112
|
-
message: `ULTRA_RECEIPT_ENCODING: ${relative} is not strict UTF-8. Restore the file as valid UTF-8 bytes, then re-run the validator.`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return { bytes, text };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function readJsonFile(repoRoot, relative) {
|
|
119
|
-
let read;
|
|
120
|
-
try {
|
|
121
|
-
read = stableReadBytes(repoRoot, relative, MAX_RECEIPT_BYTES);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return { readCode: error.code, readMessage: error.message };
|
|
124
|
-
}
|
|
125
|
-
if (read.missing) return { missing: true };
|
|
126
|
-
try {
|
|
127
|
-
return { parsed: JSON.parse(read.text) };
|
|
128
|
-
} catch (error) {
|
|
129
|
-
return { jsonError: error.message };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// One generic ancestor-first directory observation, shared by repo-wide
|
|
134
|
-
// optional handoffs-root discovery and required per-handoff validation.
|
|
135
|
-
// Starting from the ordinary repository root, every existing path component is
|
|
136
|
-
// lstat'd without following links before its child is touched, so a symlinked
|
|
137
|
-
// or special ancestor fails typed even when a later component is absent. For
|
|
138
|
-
// an optional observation, a genuinely missing component under ordinary
|
|
139
|
-
// ancestors is a legal absent boundary — and absence itself is replayed
|
|
140
|
-
// before the result is trusted. An existing final directory streams bounded
|
|
141
|
-
// under max+1 and is replayed as an exact entry name/type/identity set.
|
|
142
|
-
// Replacement, drift, unreadability, and (for required observations)
|
|
143
|
-
// absence are typed fail-closed diagnostics with restore-and-retry recovery.
|
|
144
|
-
function stableDirectoryObservation(repoRoot, relative, { required = false } = {}) {
|
|
145
|
-
const dirError = (code, message) => ({ code, message });
|
|
146
|
-
const parts = relative.split('/');
|
|
147
|
-
|
|
148
|
-
let rootStat;
|
|
149
|
-
try {
|
|
150
|
-
rootStat = fs.lstatSync(repoRoot, { bigint: true });
|
|
151
|
-
} catch (error) {
|
|
152
|
-
throw dirError('receipt_unsafe', `ULTRA_RECEIPT_UNSAFE: cannot observe the repository root (${error && error.code ? error.code : 'unknown error'}). Restore an ordinary repository directory, then re-run the validator.`);
|
|
153
|
-
}
|
|
154
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
155
|
-
throw dirError('receipt_unsafe', 'ULTRA_RECEIPT_UNSAFE: the repository root is not an ordinary directory. Restore an ordinary repository directory, then re-run the validator.');
|
|
156
|
-
}
|
|
157
|
-
const chain = [{ absolute: repoRoot, stat: rootStat, component: '.' }];
|
|
158
|
-
|
|
159
|
-
let absentBoundary = null;
|
|
160
|
-
let current = repoRoot;
|
|
161
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
162
|
-
current = path.join(current, parts[index]);
|
|
163
|
-
const component = parts.slice(0, index + 1).join('/');
|
|
164
|
-
let stat;
|
|
165
|
-
try {
|
|
166
|
-
stat = fs.lstatSync(current, { bigint: true });
|
|
167
|
-
} catch (error) {
|
|
168
|
-
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
|
|
169
|
-
if (required) {
|
|
170
|
-
throw dirError('handoff_dir_missing', `ULTRA_HANDOFF_DIR_MISSING: the required handoff directory ${relative} is absent. Restore the ordinary directory, then re-run the validator.`);
|
|
171
|
-
}
|
|
172
|
-
// Optional component: record the absent boundary and stop before
|
|
173
|
-
// touching any deeper child.
|
|
174
|
-
absentBoundary = { absolute: current, component };
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
throw dirError('receipt_unsafe', `ULTRA_RECEIPT_UNSAFE: cannot observe ${component} (${error && error.code ? error.code : 'unknown error'}). Restore the ordinary directory, then re-run the validator.`);
|
|
178
|
-
}
|
|
179
|
-
if (stat.isSymbolicLink()) {
|
|
180
|
-
throw dirError('receipt_unsafe', `ULTRA_RECEIPT_UNSAFE: ${component} is a symlink; ancestor components are never followed, and an empty external target never reads as an absent or empty directory. Restore the ordinary repository directory, then re-run the validator.`);
|
|
181
|
-
}
|
|
182
|
-
if (!stat.isDirectory()) {
|
|
183
|
-
throw dirError('receipt_not_regular', `ULTRA_RECEIPT_NOT_REGULAR: ${component} is not an ordinary directory. Restore the ordinary directory, then re-run the validator.`);
|
|
184
|
-
}
|
|
185
|
-
chain.push({ absolute: current, stat, component });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const replayAncestors = () => {
|
|
189
|
-
for (const link of chain) {
|
|
190
|
-
let again;
|
|
191
|
-
try {
|
|
192
|
-
again = fs.lstatSync(link.absolute, { bigint: true });
|
|
193
|
-
} catch (error) {
|
|
194
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: ${link.component} disappeared during observation. Let the concurrent change finish, then re-run the validator once.`);
|
|
195
|
-
}
|
|
196
|
-
if (again.dev !== link.stat.dev || again.ino !== link.stat.ino
|
|
197
|
-
|| again.mode !== link.stat.mode || again.mtimeNs !== link.stat.mtimeNs) {
|
|
198
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: ${link.component} changed identity during observation. Let the concurrent change finish, then re-run the validator once.`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
if (absentBoundary !== null) {
|
|
204
|
-
replayAncestors();
|
|
205
|
-
// Replay the still-absent boundary: an optional component that appears
|
|
206
|
-
// during the observation is drift, never a quiet success.
|
|
207
|
-
let appeared = false;
|
|
208
|
-
try {
|
|
209
|
-
fs.lstatSync(absentBoundary.absolute, { bigint: true });
|
|
210
|
-
appeared = true;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
if (!(error && (error.code === 'ENOENT' || error.code === 'ENOTDIR'))) {
|
|
213
|
-
throw dirError('receipt_unsafe', `ULTRA_RECEIPT_UNSAFE: cannot re-observe the absent boundary ${absentBoundary.component} (${error && error.code ? error.code : 'unknown error'}). Restore the ordinary directory, then re-run the validator.`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (appeared) {
|
|
217
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: ${absentBoundary.component} appeared during observation; the absent boundary is not stable. Let the concurrent change finish, then re-run the validator once.`);
|
|
218
|
-
}
|
|
219
|
-
return { absent: true, entries: null };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const finalDir = chain[chain.length - 1].absolute;
|
|
223
|
-
const snapshotEntries = () => {
|
|
224
|
-
const entries = new Map();
|
|
225
|
-
let handle;
|
|
226
|
-
try {
|
|
227
|
-
handle = fs.opendirSync(finalDir);
|
|
228
|
-
} catch (error) {
|
|
229
|
-
throw dirError('receipt_unsafe', `ULTRA_RECEIPT_UNSAFE: cannot open ${relative} (${error && error.code ? error.code : 'unknown error'}). Restore the ordinary directory, then re-run the validator.`);
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
for (;;) {
|
|
233
|
-
const entry = handle.readSync();
|
|
234
|
-
if (!entry) break;
|
|
235
|
-
if (entries.size >= MAX_HANDOFF_ENTRIES) {
|
|
236
|
-
throw dirError('handoff_scan_limit', `${relative} exceeds the ${MAX_HANDOFF_ENTRIES}-entry physical ceiling. Split or archive runtime observations, then re-run the validator.`);
|
|
237
|
-
}
|
|
238
|
-
let stat;
|
|
239
|
-
try {
|
|
240
|
-
stat = fs.lstatSync(path.join(finalDir, entry.name), { bigint: true });
|
|
241
|
-
} catch (error) {
|
|
242
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: ${relative} entry ${entry.name} disappeared during observation. Let the concurrent change finish, then re-run the validator once.`);
|
|
243
|
-
}
|
|
244
|
-
entries.set(entry.name, { name: entry.name, stat });
|
|
245
|
-
}
|
|
246
|
-
} finally {
|
|
247
|
-
handle.closeSync();
|
|
248
|
-
}
|
|
249
|
-
return entries;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const first = snapshotEntries();
|
|
253
|
-
replayAncestors();
|
|
254
|
-
const second = snapshotEntries();
|
|
255
|
-
if (second.size !== first.size) {
|
|
256
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: the ${relative} entry set changed during observation. Let the concurrent change finish, then re-run the validator once.`);
|
|
257
|
-
}
|
|
258
|
-
for (const [name, entry] of first) {
|
|
259
|
-
const other = second.get(name);
|
|
260
|
-
if (!other
|
|
261
|
-
|| other.stat.dev !== entry.stat.dev
|
|
262
|
-
|| other.stat.ino !== entry.stat.ino
|
|
263
|
-
|| other.stat.mode !== entry.stat.mode) {
|
|
264
|
-
throw dirError('receipt_replaced', `ULTRA_RECEIPT_REPLACED: ${relative} entry ${name} changed identity during observation. Let the concurrent change finish, then re-run the validator once.`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return { absent: false, entries: first };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function gitText(cwd, args, maxBuffer = 1024 * 1024) {
|
|
271
|
-
return execFileSync('git', args, {
|
|
272
|
-
cwd,
|
|
273
|
-
encoding: 'utf8',
|
|
274
|
-
maxBuffer,
|
|
275
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
276
|
-
timeout: GIT_TIMEOUT_MS,
|
|
277
|
-
}).trim();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function gitHead(cwd) {
|
|
281
|
-
try {
|
|
282
|
-
return gitText(cwd, ['rev-parse', 'HEAD']);
|
|
283
|
-
} catch {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function gitIsAncestor(cwd, ancestor, descendant) {
|
|
289
|
-
try {
|
|
290
|
-
execFileSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
291
|
-
cwd,
|
|
292
|
-
encoding: 'utf8',
|
|
293
|
-
maxBuffer: 1024 * 1024,
|
|
294
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
295
|
-
timeout: GIT_TIMEOUT_MS,
|
|
296
|
-
});
|
|
297
|
-
return true;
|
|
298
|
-
} catch (error) {
|
|
299
|
-
if (error && error.status === 1) return false;
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// The full final worktree subject against a base HEAD, using the digest
|
|
305
|
-
// primitive's own product pathspec and untracked file list: present tracked
|
|
306
|
-
// changes plus product-scope untracked files, and separately deleted paths.
|
|
307
|
-
function manifestAgainst(root, baseHead, digestObservation) {
|
|
308
|
-
const raw = execFileSync('git', [
|
|
309
|
-
'diff', '--raw', '-z', '--no-renames', baseHead, ...PRODUCT_PATHSPEC,
|
|
310
|
-
], {
|
|
311
|
-
cwd: root,
|
|
312
|
-
encoding: 'buffer',
|
|
313
|
-
maxBuffer: GIT_MAX_OUTPUT_BYTES,
|
|
314
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
315
|
-
timeout: GIT_TIMEOUT_MS,
|
|
316
|
-
});
|
|
317
|
-
const fields = raw.toString('utf8').split('\0').filter(Boolean);
|
|
318
|
-
const present = [];
|
|
319
|
-
const deleted = [];
|
|
320
|
-
for (let index = 0; index < fields.length; index += 2) {
|
|
321
|
-
const status = fields[index].split(' ').at(-1);
|
|
322
|
-
const file = fields[index + 1];
|
|
323
|
-
if (status === 'D') deleted.push(file);
|
|
324
|
-
else present.push(file);
|
|
325
|
-
}
|
|
326
|
-
return {
|
|
327
|
-
changed: [...new Set([...present, ...digestObservation.untracked_files])].sort(),
|
|
328
|
-
deleted: deleted.sort(),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function digestObservation(root) {
|
|
333
|
-
try {
|
|
334
|
-
const stdout = execFileSync(process.execPath, [DIGEST_TOOL, '--project', root], {
|
|
335
|
-
encoding: 'utf8',
|
|
336
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
337
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
338
|
-
timeout: DIGEST_TOOL_TIMEOUT_MS,
|
|
339
|
-
});
|
|
340
|
-
const parsed = JSON.parse(stdout);
|
|
341
|
-
if (!isPlainObject(parsed) || !SHA256_PATTERN.test(String(parsed.diff_digest || ''))) {
|
|
342
|
-
return { error: 'the worktree-digest primitive returned an unusable report' };
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
head: parsed.head,
|
|
346
|
-
diff_digest: parsed.diff_digest,
|
|
347
|
-
dirty: parsed.dirty,
|
|
348
|
-
untracked_files: Array.isArray(parsed.untracked_files) ? parsed.untracked_files : [],
|
|
349
|
-
};
|
|
350
|
-
} catch (error) {
|
|
351
|
-
const detail = error && error.stderr
|
|
352
|
-
? Buffer.from(error.stderr).toString('utf8').trim().split('\n')[0]
|
|
353
|
-
: (error && error.message ? error.message.split('\n')[0] : 'the worktree-digest primitive failed');
|
|
354
|
-
return { error: detail };
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function handoffRootFor(handoffDir) {
|
|
359
|
-
// <repoRoot>/.ultra/.runtime/handoffs/<handoff-id>
|
|
360
|
-
const id = path.basename(handoffDir);
|
|
361
|
-
const handoffs = path.dirname(handoffDir);
|
|
362
|
-
if (path.basename(handoffs) !== 'handoffs') return null;
|
|
363
|
-
const runtime = path.dirname(handoffs);
|
|
364
|
-
if (path.basename(runtime) !== '.runtime') return null;
|
|
365
|
-
const ultra = path.dirname(runtime);
|
|
366
|
-
if (path.basename(ultra) !== '.ultra') return null;
|
|
367
|
-
const root = path.dirname(ultra);
|
|
368
|
-
return { root, id };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const HANDOFFS_ROOT_RELATIVE = '.ultra/.runtime/handoffs';
|
|
372
|
-
|
|
373
|
-
// Repo-wide handoff-root discovery is one optional ancestor-first directory
|
|
374
|
-
// observation — no separate final-leaf preflight. A genuinely missing .ultra,
|
|
375
|
-
// .runtime, or handoffs leaf under ordinary ancestors means zero transfers;
|
|
376
|
-
// a symlinked, special, unreadable, replaced, or drifting component at any
|
|
377
|
-
// depth is a typed failure, even when a later component is absent; and every
|
|
378
|
-
// existing entry must be a normalized-id ordinary directory.
|
|
379
|
-
function listHandoffDirs(repoRoot) {
|
|
380
|
-
const observation = stableDirectoryObservation(repoRoot, HANDOFFS_ROOT_RELATIVE, { required: false });
|
|
381
|
-
if (observation.absent) return [];
|
|
382
|
-
const handoffsRoot = path.join(repoRoot, HANDOFFS_ROOT_RELATIVE);
|
|
383
|
-
const dirs = [];
|
|
384
|
-
for (const [name, entry] of observation.entries) {
|
|
385
|
-
if (!HANDOFF_ID_PATTERN.test(name)) {
|
|
386
|
-
throw {
|
|
387
|
-
code: 'handoff_entry_malformed',
|
|
388
|
-
message: `the handoffs root holds an entry that is not a normalized handoff id: ${name}. Remove or rename the stray entry, then re-run the validator.`,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
if (entry.stat.isSymbolicLink()) {
|
|
392
|
-
throw {
|
|
393
|
-
code: 'receipt_unsafe',
|
|
394
|
-
message: `ULTRA_RECEIPT_UNSAFE: handoff entry ${name} is a symlink; symlinked handoffs are never followed. Restore the ordinary directory, then re-run the validator.`,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
if (!entry.stat.isDirectory()) {
|
|
398
|
-
throw {
|
|
399
|
-
code: 'handoff_entry_malformed',
|
|
400
|
-
message: `the handoffs root holds a non-directory entry under a handoff id: ${name}. Remove the stray entry, then re-run the validator.`,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
dirs.push(path.join(handoffsRoot, name));
|
|
404
|
-
}
|
|
405
|
-
return dirs.sort();
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function receiptTime(payload) {
|
|
409
|
-
if (!isPlainObject(payload) || typeof payload.created_at !== 'string') return null;
|
|
410
|
-
const parsed = Date.parse(payload.created_at);
|
|
411
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function validateOffer(handoffId, repoRoot, payload, diagnostics) {
|
|
415
|
-
if (!isPlainObject(payload)) {
|
|
416
|
-
diagnostics.push(diagnostic('offer_schema', 'OFFER.json must be a JSON object', 'OFFER.json'));
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const offer = payload;
|
|
420
|
-
if (offer.$schema !== OFFER_SCHEMA) {
|
|
421
|
-
diagnostics.push(diagnostic('offer_schema', `OFFER $schema must be ${OFFER_SCHEMA}`, 'OFFER.json.$schema'));
|
|
422
|
-
}
|
|
423
|
-
if (offer.handoff_id !== handoffId) {
|
|
424
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER handoff_id must match the handoff directory name', 'OFFER.json.handoff_id'));
|
|
425
|
-
}
|
|
426
|
-
if (offer.mode !== 'primary-transfer') {
|
|
427
|
-
diagnostics.push(diagnostic('offer_mode_invalid', 'primary-transfer is the only handoff mode; delegated workers use ultra-delegate receipts', 'OFFER.json.mode'));
|
|
428
|
-
}
|
|
429
|
-
if (offer.state !== 'offered') {
|
|
430
|
-
diagnostics.push(diagnostic('offer_state_invalid', 'OFFER state must be offered', 'OFFER.json.state'));
|
|
431
|
-
}
|
|
432
|
-
for (const [holder, field] of [['sender', 'agent'], ['sender', 'role'], ['receiver', 'agent'], ['receiver', 'role']]) {
|
|
433
|
-
if (!nonemptyString(offer[holder]?.[field])) {
|
|
434
|
-
diagnostics.push(diagnostic('offer_field_missing', `OFFER ${holder}.${field} is required`, `OFFER.json.${holder}.${field}`));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
const repository = offer.repository;
|
|
438
|
-
if (!isPlainObject(repository)) {
|
|
439
|
-
diagnostics.push(diagnostic('offer_field_missing', 'OFFER repository is required', 'OFFER.json.repository'));
|
|
440
|
-
} else {
|
|
441
|
-
if (!nonemptyString(repository.origin)) {
|
|
442
|
-
diagnostics.push(diagnostic('offer_field_missing', 'OFFER repository.origin is required', 'OFFER.json.repository.origin'));
|
|
443
|
-
}
|
|
444
|
-
if (!HEAD_PATTERN.test(String(repository.base_head || ''))) {
|
|
445
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.base_head must be a 40-character lowercase commit id', 'OFFER.json.repository.base_head'));
|
|
446
|
-
}
|
|
447
|
-
const digest = repository.worktree_digest;
|
|
448
|
-
if (typeof digest === 'string') {
|
|
449
|
-
if (!SHA256_PATTERN.test(digest)) {
|
|
450
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.worktree_digest must be a 64-character lowercase SHA-256', 'OFFER.json.repository.worktree_digest'));
|
|
451
|
-
}
|
|
452
|
-
} else if (isPlainObject(digest)) {
|
|
453
|
-
if (!SHA256_PATTERN.test(String(digest.diff_digest || ''))) {
|
|
454
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.worktree_digest.diff_digest must be a 64-character lowercase SHA-256', 'OFFER.json.repository.worktree_digest.diff_digest'));
|
|
455
|
-
}
|
|
456
|
-
} else {
|
|
457
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.worktree_digest is required', 'OFFER.json.repository.worktree_digest'));
|
|
458
|
-
}
|
|
459
|
-
if (typeof repository.dirty !== 'boolean') {
|
|
460
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.dirty must be a boolean', 'OFFER.json.repository.dirty'));
|
|
461
|
-
}
|
|
462
|
-
if (!Array.isArray(repository.known_untracked)
|
|
463
|
-
|| !repository.known_untracked.every((entry) => nonemptyString(entry) && normalizedRelativePath(entry))) {
|
|
464
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER repository.known_untracked must list normalized repository-relative paths', 'OFFER.json.repository.known_untracked'));
|
|
465
|
-
}
|
|
466
|
-
if (nonemptyString(repository.root)) {
|
|
467
|
-
let observedRoot = null;
|
|
468
|
-
try {
|
|
469
|
-
observedRoot = fs.realpathSync(repository.root);
|
|
470
|
-
} catch {
|
|
471
|
-
observedRoot = null;
|
|
472
|
-
}
|
|
473
|
-
if (observedRoot === null || observedRoot !== repoRoot) {
|
|
474
|
-
diagnostics.push(diagnostic('repository_root_mismatch', 'OFFER repository.root must be the repository that contains this handoff directory', 'OFFER.json.repository.root'));
|
|
475
|
-
}
|
|
476
|
-
} else {
|
|
477
|
-
diagnostics.push(diagnostic('offer_field_missing', 'OFFER repository.root is required', 'OFFER.json.repository.root'));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (!nonemptyString(offer.owner_authorization?.accepted_direction)) {
|
|
481
|
-
diagnostics.push(diagnostic('offer_field_missing', 'OFFER owner_authorization.accepted_direction is required', 'OFFER.json.owner_authorization.accepted_direction'));
|
|
482
|
-
}
|
|
483
|
-
const frozenInputs = offer.frozen_inputs;
|
|
484
|
-
if (!Array.isArray(frozenInputs) || frozenInputs.length === 0) {
|
|
485
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER frozen_inputs must list at least one frozen input', 'OFFER.json.frozen_inputs'));
|
|
486
|
-
} else {
|
|
487
|
-
const seen = new Set();
|
|
488
|
-
for (const input of frozenInputs) {
|
|
489
|
-
if (!isPlainObject(input)) {
|
|
490
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'each frozen input must be an object', 'OFFER.json.frozen_inputs'));
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
if (!nonemptyString(input.purpose)
|
|
494
|
-
|| !normalizedRelativePath(input.path)
|
|
495
|
-
|| !SHA256_PATTERN.test(String(input.sha256 || ''))) {
|
|
496
|
-
diagnostics.push(diagnostic(
|
|
497
|
-
'offer_field_invalid',
|
|
498
|
-
'each frozen input requires a nonempty purpose, a normalized repository-relative path, and a 64-character SHA-256',
|
|
499
|
-
`OFFER.json.frozen_inputs[${input.path || '?'}]`,
|
|
500
|
-
));
|
|
501
|
-
}
|
|
502
|
-
if (seen.has(input.path)) {
|
|
503
|
-
diagnostics.push(diagnostic('offer_field_invalid', `frozen input path repeated: ${input.path}`, `OFFER.json.frozen_inputs[${input.path}]`));
|
|
504
|
-
}
|
|
505
|
-
seen.add(input.path);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
if (!nonemptyString(offer.accepted_scope?.new_task_identity)) {
|
|
509
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER accepted_scope.new_task_identity is required', 'OFFER.json.accepted_scope.new_task_identity'));
|
|
510
|
-
}
|
|
511
|
-
const effects = offer.effects;
|
|
512
|
-
if (!isPlainObject(effects)
|
|
513
|
-
|| !stringArray(effects.allowed)
|
|
514
|
-
|| !stringArray(effects.forbidden)) {
|
|
515
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'OFFER effects.allowed and effects.forbidden must list at least one entry each', 'OFFER.json.effects'));
|
|
516
|
-
}
|
|
517
|
-
const protocol = offer.receiver_protocol;
|
|
518
|
-
if (!isPlainObject(protocol)) {
|
|
519
|
-
diagnostics.push(diagnostic('offer_field_missing', 'OFFER receiver_protocol is required', 'OFFER.json.receiver_protocol'));
|
|
520
|
-
} else {
|
|
521
|
-
if (protocol.ack_path !== `.ultra/.runtime/handoffs/${handoffId}/ACK.json`
|
|
522
|
-
|| protocol.result_path !== `.ultra/.runtime/handoffs/${handoffId}/RESULT.json`) {
|
|
523
|
-
diagnostics.push(diagnostic('offer_field_invalid', 'receiver_protocol receipt paths must address this handoff directory', 'OFFER.json.receiver_protocol'));
|
|
524
|
-
}
|
|
525
|
-
if (!Array.isArray(protocol.terminal_states)
|
|
526
|
-
|| !TERMINAL_STATES.every((state) => protocol.terminal_states.includes(state))) {
|
|
527
|
-
diagnostics.push(diagnostic(
|
|
528
|
-
'offer_field_invalid',
|
|
529
|
-
`receiver_protocol.terminal_states must include ${TERMINAL_STATES.join(', ')}`,
|
|
530
|
-
'OFFER.json.receiver_protocol.terminal_states',
|
|
531
|
-
));
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function offeredDigest(offer) {
|
|
537
|
-
const digest = offer?.repository?.worktree_digest;
|
|
538
|
-
if (typeof digest === 'string') return digest;
|
|
539
|
-
if (isPlainObject(digest)) return typeof digest.diff_digest === 'string' ? digest.diff_digest : null;
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function observedDigest(ack) {
|
|
544
|
-
const digest = ack?.observed?.worktree_digest;
|
|
545
|
-
if (typeof digest === 'string') return digest;
|
|
546
|
-
if (isPlainObject(digest)) return typeof digest.diff_digest === 'string' ? digest.diff_digest : null;
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function validateAck(offer, payload, handoffId, diagnostics) {
|
|
551
|
-
if (!isPlainObject(payload)) {
|
|
552
|
-
diagnostics.push(diagnostic('ack_schema', 'ACK.json must be a JSON object', 'ACK.json'));
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
const ack = payload;
|
|
556
|
-
if (ack.$schema !== ACK_SCHEMA) {
|
|
557
|
-
diagnostics.push(diagnostic('ack_schema', `ACK $schema must be ${ACK_SCHEMA}`, 'ACK.json.$schema'));
|
|
558
|
-
}
|
|
559
|
-
if (ack.handoff_id !== handoffId) {
|
|
560
|
-
diagnostics.push(diagnostic('ack_schema', 'ACK handoff_id must match the OFFER handoff id', 'ACK.json.handoff_id'));
|
|
561
|
-
}
|
|
562
|
-
const state = ack.state;
|
|
563
|
-
if (state !== 'ready' && state !== 'blocked') {
|
|
564
|
-
diagnostics.push(diagnostic('ack_state_invalid', 'ACK state must be ready or blocked', 'ACK.json.state'));
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
const acceptedRole = ack.receiver?.accepted_role;
|
|
568
|
-
if (acceptedRole !== offer?.receiver?.role) {
|
|
569
|
-
diagnostics.push(diagnostic('ack_role_mismatch', 'ACK accepted_role must equal the OFFER receiver role', 'ACK.json.receiver.accepted_role'));
|
|
570
|
-
}
|
|
571
|
-
const observations = Array.isArray(ack.observed?.frozen_inputs) ? ack.observed.frozen_inputs : [];
|
|
572
|
-
const byPath = new Map(observations.filter(isPlainObject).map((entry) => [entry.path, entry]));
|
|
573
|
-
const offeredInputs = Array.isArray(offer?.frozen_inputs) ? offer.frozen_inputs.filter(isPlainObject) : [];
|
|
574
|
-
for (const input of offeredInputs) {
|
|
575
|
-
const observed = byPath.get(input.path);
|
|
576
|
-
if (!observed) {
|
|
577
|
-
diagnostics.push(diagnostic(
|
|
578
|
-
'ack_missing_input_observation',
|
|
579
|
-
`ACK must record a stable-read observation for frozen input ${input.path}`,
|
|
580
|
-
`ACK.json.observed.frozen_inputs[${input.path}]`,
|
|
581
|
-
));
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
if (observed.offered_sha256 !== input.sha256) {
|
|
585
|
-
diagnostics.push(diagnostic(
|
|
586
|
-
'ack_ready_with_mismatch',
|
|
587
|
-
`ACK offered_sha256 must copy the OFFER sha256 for ${input.path}`,
|
|
588
|
-
`ACK.json.observed.frozen_inputs[${input.path}].offered_sha256`,
|
|
589
|
-
));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
if (state === 'ready') {
|
|
593
|
-
for (const input of offeredInputs) {
|
|
594
|
-
const observed = byPath.get(input.path);
|
|
595
|
-
if (observed && (observed.match !== true || observed.observed_sha256 !== input.sha256)) {
|
|
596
|
-
diagnostics.push(diagnostic(
|
|
597
|
-
'ack_ready_with_mismatch',
|
|
598
|
-
'a ready ACK requires every frozen input observation to match; a mismatch must be recorded as blocked',
|
|
599
|
-
`ACK.json.observed.frozen_inputs[${input.path}]`,
|
|
600
|
-
));
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (ack.observed?.repository?.base_head !== offer?.repository?.base_head) {
|
|
604
|
-
diagnostics.push(diagnostic('ack_ready_with_mismatch', 'a ready ACK must observe the OFFER base HEAD', 'ACK.json.observed.repository.base_head'));
|
|
605
|
-
}
|
|
606
|
-
const offerDigest = offeredDigest(offer);
|
|
607
|
-
const ackDigest = observedDigest(ack);
|
|
608
|
-
if (offerDigest !== null && ackDigest !== offerDigest) {
|
|
609
|
-
diagnostics.push(diagnostic('ack_ready_with_mismatch', 'a ready ACK must observe the OFFER worktree digest', 'ACK.json.observed.worktree_digest'));
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
if (state === 'blocked') {
|
|
613
|
-
if (!stringArray(ack.blocked_reasons)) {
|
|
614
|
-
diagnostics.push(diagnostic('ack_blocked_without_reason', 'a blocked ACK must record at least one blocked reason', 'ACK.json.blocked_reasons'));
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return state;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function validateResultPaths(handoffId, values, field, diagnostics) {
|
|
621
|
-
if (!Array.isArray(values) || !values.every(nonemptyString)) {
|
|
622
|
-
diagnostics.push(diagnostic('result_schema', `RESULT ${field} must be an array of strings`, `RESULT.json.${field}`));
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
for (const value of values) {
|
|
626
|
-
if (!normalizedRelativePath(value)) {
|
|
627
|
-
diagnostics.push(diagnostic('result_path_invalid', `RESULT ${field} entries must be normalized repository-relative paths: ${value}`, `RESULT.json.${field}`));
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
const ownReceiptRoot = `.ultra/.runtime/handoffs/${handoffId}/`;
|
|
631
|
-
if (value.startsWith('.ultra/.runtime/') && !value.startsWith(ownReceiptRoot)) {
|
|
632
|
-
diagnostics.push(diagnostic(
|
|
633
|
-
'result_path_invalid',
|
|
634
|
-
`RESULT ${field} may not claim unrelated runtime state: ${value}`,
|
|
635
|
-
`RESULT.json.${field}`,
|
|
636
|
-
));
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function validateResult(offer, ackState, payload, handoffId, repoRoot, diagnostics) {
|
|
642
|
-
if (!isPlainObject(payload)) {
|
|
643
|
-
diagnostics.push(diagnostic('result_schema', 'RESULT.json must be a JSON object', 'RESULT.json'));
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
const result = payload;
|
|
647
|
-
if (result.$schema !== RESULT_SCHEMA && result.$schema !== RESULT_V2_SCHEMA) {
|
|
648
|
-
diagnostics.push(diagnostic('result_schema', `RESULT $schema must be ${RESULT_SCHEMA} or ${RESULT_V2_SCHEMA}`, 'RESULT.json.$schema'));
|
|
649
|
-
}
|
|
650
|
-
if (result.handoff_id !== handoffId) {
|
|
651
|
-
diagnostics.push(diagnostic('result_schema', 'RESULT handoff_id must match the OFFER handoff id', 'RESULT.json.handoff_id'));
|
|
652
|
-
}
|
|
653
|
-
const terminal = result.terminal_state;
|
|
654
|
-
if (!TERMINAL_STATES.includes(terminal)) {
|
|
655
|
-
diagnostics.push(diagnostic('result_terminal_invalid', `RESULT terminal_state must be one of ${TERMINAL_STATES.join(', ')}`, 'RESULT.json.terminal_state'));
|
|
656
|
-
}
|
|
657
|
-
if (terminal === 'completed' && ackState !== 'ready') {
|
|
658
|
-
diagnostics.push(diagnostic(
|
|
659
|
-
'result_without_ready_ack',
|
|
660
|
-
'a completed RESULT requires a ready ACK; no ACK or a blocked ACK cannot complete a transfer',
|
|
661
|
-
'RESULT.json.terminal_state',
|
|
662
|
-
));
|
|
663
|
-
}
|
|
664
|
-
if (terminal === 'blocked' && ackState !== 'blocked' && !stringArray(result.blocked_reasons)) {
|
|
665
|
-
diagnostics.push(diagnostic(
|
|
666
|
-
'result_blocked_without_reason',
|
|
667
|
-
'a blocked RESULT requires either a blocked ACK or explicit blocked_reasons',
|
|
668
|
-
'RESULT.json.blocked_reasons',
|
|
669
|
-
));
|
|
670
|
-
}
|
|
671
|
-
if (terminal === 'revoked') {
|
|
672
|
-
const refs = Array.isArray(result.evidence_refs) ? result.evidence_refs.filter(nonemptyString) : [];
|
|
673
|
-
const revocation = refs.filter((ref) => /^\.ultra\/decisions\/[A-Za-z0-9][A-Za-z0-9._-]*\.md$/u.test(ref));
|
|
674
|
-
// Revocation evidence must be an ordinary stable repository-contained file,
|
|
675
|
-
// never a symlink or special file that existsSync would follow or trust.
|
|
676
|
-
const readable = revocation.some((ref) => {
|
|
677
|
-
try {
|
|
678
|
-
return !stableReadBytes(repoRoot, ref, MAX_INPUT_BYTES).missing;
|
|
679
|
-
} catch {
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
if (revocation.length === 0 || !readable) {
|
|
684
|
-
diagnostics.push(diagnostic(
|
|
685
|
-
'result_revocation_evidence_missing',
|
|
686
|
-
'a revoked RESULT must cite an existing ordinary .ultra/decisions owner record',
|
|
687
|
-
'RESULT.json.evidence_refs',
|
|
688
|
-
));
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
if (!HEAD_PATTERN.test(String(result.final_head || ''))) {
|
|
692
|
-
diagnostics.push(diagnostic('result_schema', 'RESULT final_head must be a 40-character lowercase commit id', 'RESULT.json.final_head'));
|
|
693
|
-
}
|
|
694
|
-
if (!SHA256_PATTERN.test(String(result.final_worktree_digest || ''))) {
|
|
695
|
-
diagnostics.push(diagnostic('result_schema', 'RESULT final_worktree_digest must be a 64-character lowercase SHA-256', 'RESULT.json.final_worktree_digest'));
|
|
696
|
-
}
|
|
697
|
-
validateResultPaths(handoffId, result.changed_paths, 'changed_paths', diagnostics);
|
|
698
|
-
validateResultPaths(handoffId, result.deleted_paths, 'deleted_paths', diagnostics);
|
|
699
|
-
if (!Array.isArray(result.commands)
|
|
700
|
-
|| !result.commands.every((entry) => isPlainObject(entry) && nonemptyString(entry.command) && Number.isInteger(entry.exit_code))) {
|
|
701
|
-
diagnostics.push(diagnostic('result_schema', 'RESULT commands must record exact commands with integer exit codes', 'RESULT.json.commands'));
|
|
702
|
-
}
|
|
703
|
-
for (const field of ['evidence_refs', 'fakes', 'limitations', 'not_done', 'external_effects', 'review_risks']) {
|
|
704
|
-
if (!Array.isArray(result[field]) || !result[field].every(nonemptyString)) {
|
|
705
|
-
diagnostics.push(diagnostic('result_schema', `RESULT ${field} must be an array of strings`, `RESULT.json.${field}`));
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (result.$schema === RESULT_V2_SCHEMA) {
|
|
709
|
-
validateFrozenInputFinalDigests(offer, result, diagnostics);
|
|
710
|
-
}
|
|
711
|
-
return terminal;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// A v2 RESULT separately binds the final bytes of every OFFER frozen input: each
|
|
715
|
-
// entry declares either the actual final SHA-256 or an explicit absence. The
|
|
716
|
-
// structural pass checks coverage and shape; the live pass recomputes the bytes.
|
|
717
|
-
function validateFrozenInputFinalDigests(offer, result, diagnostics) {
|
|
718
|
-
const entries = result.frozen_input_final_digests;
|
|
719
|
-
if (!Array.isArray(entries) || !entries.every(isPlainObject)) {
|
|
720
|
-
diagnostics.push(diagnostic(
|
|
721
|
-
'result_frozen_input_digest_missing',
|
|
722
|
-
`a ${RESULT_V2_SCHEMA} RESULT must list frozen_input_final_digests covering every OFFER frozen input`,
|
|
723
|
-
'RESULT.json.frozen_input_final_digests',
|
|
724
|
-
));
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
const byPath = new Map();
|
|
728
|
-
for (const entry of entries) {
|
|
729
|
-
if (!normalizedRelativePath(entry.path)
|
|
730
|
-
|| !(entry.absent === true || SHA256_PATTERN.test(String(entry.sha256 || '')))) {
|
|
731
|
-
diagnostics.push(diagnostic(
|
|
732
|
-
'result_frozen_input_digest_missing',
|
|
733
|
-
'each frozen_input_final_digests entry needs a normalized path and a 64-character SHA-256 or an explicit absent flag',
|
|
734
|
-
`RESULT.json.frozen_input_final_digests[${entry.path || '?'}]`,
|
|
735
|
-
));
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
if (byPath.has(entry.path)) {
|
|
739
|
-
diagnostics.push(diagnostic(
|
|
740
|
-
'result_frozen_input_digest_missing',
|
|
741
|
-
`frozen_input_final_digests path repeated: ${entry.path}`,
|
|
742
|
-
`RESULT.json.frozen_input_final_digests[${entry.path}]`,
|
|
743
|
-
));
|
|
744
|
-
}
|
|
745
|
-
byPath.set(entry.path, entry);
|
|
746
|
-
}
|
|
747
|
-
const offeredInputs = Array.isArray(offer?.frozen_inputs) ? offer.frozen_inputs.filter(isPlainObject) : [];
|
|
748
|
-
for (const input of offeredInputs) {
|
|
749
|
-
if (!byPath.has(input.path)) {
|
|
750
|
-
diagnostics.push(diagnostic(
|
|
751
|
-
'result_frozen_input_digest_missing',
|
|
752
|
-
`frozen_input_final_digests must cover the OFFER frozen input ${input.path}`,
|
|
753
|
-
`RESULT.json.frozen_input_final_digests[${input.path}]`,
|
|
754
|
-
));
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
for (const entryPath of byPath.keys()) {
|
|
758
|
-
if (!offeredInputs.some((input) => input.path === entryPath)) {
|
|
759
|
-
diagnostics.push(diagnostic(
|
|
760
|
-
'result_frozen_input_digest_missing',
|
|
761
|
-
`frozen_input_final_digests lists a path the OFFER never froze: ${entryPath}`,
|
|
762
|
-
`RESULT.json.frozen_input_final_digests[${entryPath}]`,
|
|
763
|
-
));
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Phase-correct live observation. The ACK's bytes are a pre-write boundary
|
|
769
|
-
// record, never present-current freshness: expected receiver edits after a
|
|
770
|
-
// ready ACK are legal, so active validation observes HEAD, the ledger row, and
|
|
771
|
-
// whether the product worktree digest still equals the ACK-observed digest
|
|
772
|
-
// (boundary intact) or receiver writes have begun. Terminal v1 receipts and any
|
|
773
|
-
// handoff superseded by a newer handoff are historical structure. A newest v2
|
|
774
|
-
// terminal receipt is bound to current reality: final HEAD, the recomputed
|
|
775
|
-
// worktree digest, the exact final product path inventory, and the final bytes
|
|
776
|
-
// of every frozen input.
|
|
777
|
-
function liveObservations(offer, ack, ackState, terminal, result, repoRoot, diagnostics, live, superseded, closeout) {
|
|
778
|
-
if (ackState !== 'ready') return live;
|
|
779
|
-
const currentHead = gitHead(repoRoot);
|
|
780
|
-
live.current_head = currentHead;
|
|
781
|
-
if (currentHead === null) {
|
|
782
|
-
diagnostics.push(diagnostic(
|
|
783
|
-
'git_unavailable',
|
|
784
|
-
'the repository HEAD could not be observed (Git missing from PATH, unresponsive, or the path is not a worktree); restore a responsive Git, then re-run the validator — live validation fails closed and never guesses',
|
|
785
|
-
'live.head',
|
|
786
|
-
));
|
|
787
|
-
return live;
|
|
788
|
-
}
|
|
789
|
-
const baseHead = offer?.repository?.base_head;
|
|
790
|
-
|
|
791
|
-
if (terminal === null) {
|
|
792
|
-
if (baseHead !== currentHead) {
|
|
793
|
-
diagnostics.push(diagnostic(
|
|
794
|
-
'stale_head',
|
|
795
|
-
'the current repository HEAD differs from the ACK-verified base HEAD; the active transfer subject is stale',
|
|
796
|
-
'live.head',
|
|
797
|
-
));
|
|
798
|
-
}
|
|
799
|
-
const identity = offer?.accepted_scope?.new_task_identity;
|
|
800
|
-
if (nonemptyString(identity)) {
|
|
801
|
-
observeTaskIdentity(repoRoot, identity, diagnostics, live);
|
|
802
|
-
}
|
|
803
|
-
const digest = digestObservation(repoRoot);
|
|
804
|
-
if (digest.error) {
|
|
805
|
-
diagnostics.push(diagnostic(
|
|
806
|
-
'live_digest_unavailable',
|
|
807
|
-
`the worktree digest could not be observed: ${digest.error}`,
|
|
808
|
-
'live.worktree_digest',
|
|
809
|
-
'warning',
|
|
810
|
-
));
|
|
811
|
-
} else {
|
|
812
|
-
const ackDigest = observedDigest(ack);
|
|
813
|
-
live.worktree_digest = digest.diff_digest;
|
|
814
|
-
live.boundary_intact = ackDigest !== null && digest.diff_digest === ackDigest;
|
|
815
|
-
live.receiver_writes_begun = !live.boundary_intact;
|
|
816
|
-
}
|
|
817
|
-
return live;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
live.historical = superseded === true || result?.$schema !== RESULT_V2_SCHEMA;
|
|
821
|
-
if (live.historical) return live;
|
|
822
|
-
|
|
823
|
-
if (currentHead !== result.final_head) {
|
|
824
|
-
if (gitIsAncestor(repoRoot, result.final_head, currentHead)) {
|
|
825
|
-
diagnostics.push(diagnostic(
|
|
826
|
-
'result_head_advanced',
|
|
827
|
-
'history advanced past the frozen final HEAD (for example an owner commit); the receipt stays historical and current reality lives in Git',
|
|
828
|
-
'live.head',
|
|
829
|
-
'warning',
|
|
830
|
-
));
|
|
831
|
-
return live;
|
|
832
|
-
}
|
|
833
|
-
diagnostics.push(diagnostic(
|
|
834
|
-
'result_head_diverged',
|
|
835
|
-
'the current repository HEAD diverged from the RESULT final HEAD without preserving it in history; recapture from Git or record the divergence for the owner',
|
|
836
|
-
'live.head',
|
|
837
|
-
));
|
|
838
|
-
return live;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
// The prescribed closeout transition: current reality binds to the recorded
|
|
842
|
-
// closeout end-state instead of the freeze digest, while every reviewed
|
|
843
|
-
// byte outside the prescribed paths stays pinned. Without a CLOSEOUT
|
|
844
|
-
// receipt, the freeze binding below applies unchanged.
|
|
845
|
-
if (closeout) {
|
|
846
|
-
if (closeout.continuation === undefined
|
|
847
|
-
&& closeout.subject_before?.worktree_digest !== result.final_worktree_digest) {
|
|
848
|
-
diagnostics.push(diagnostic(
|
|
849
|
-
'closeout_binding',
|
|
850
|
-
'the closeout starts from a worktree digest the RESULT never froze and records no owner-authorized continuation; record the continuation or return to the owner',
|
|
851
|
-
'live.subject_before',
|
|
852
|
-
));
|
|
853
|
-
return live;
|
|
854
|
-
}
|
|
855
|
-
return liveCloseoutObservations(offer, result, closeout, repoRoot, diagnostics, live);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const digest = digestObservation(repoRoot);
|
|
859
|
-
if (digest.error) {
|
|
860
|
-
diagnostics.push(diagnostic(
|
|
861
|
-
'result_binding_unverifiable',
|
|
862
|
-
`the terminal subject could not be re-observed: ${digest.error}`,
|
|
863
|
-
'live.final_worktree_digest',
|
|
864
|
-
));
|
|
865
|
-
verifyFrozenInputFinalDigests(repoRoot, result, diagnostics);
|
|
866
|
-
return live;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
let manifest = null;
|
|
870
|
-
try {
|
|
871
|
-
manifest = manifestAgainst(repoRoot, baseHead, digest);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
diagnostics.push(diagnostic(
|
|
874
|
-
'result_binding_unverifiable',
|
|
875
|
-
`the final product path inventory could not be observed: ${error && error.message ? error.message.split('\n')[0] : 'git failed'}`,
|
|
876
|
-
'live.final_path_inventory',
|
|
877
|
-
));
|
|
878
|
-
}
|
|
879
|
-
if (manifest) {
|
|
880
|
-
const changed = new Set((Array.isArray(result.changed_paths) ? result.changed_paths : []).filter(nonemptyString));
|
|
881
|
-
const manifestChanged = new Set(manifest.changed);
|
|
882
|
-
const missing = manifest.changed.filter((file) => !changed.has(file));
|
|
883
|
-
const extra = [...changed].filter((file) => !manifestChanged.has(file));
|
|
884
|
-
if (missing.length > 0) {
|
|
885
|
-
diagnostics.push(diagnostic(
|
|
886
|
-
'result_inventory_missing',
|
|
887
|
-
`RESULT changed_paths omits final product paths: ${missing.join(', ')}`,
|
|
888
|
-
'live.final_path_inventory',
|
|
889
|
-
));
|
|
890
|
-
}
|
|
891
|
-
if (extra.length > 0) {
|
|
892
|
-
diagnostics.push(diagnostic(
|
|
893
|
-
'result_inventory_extra',
|
|
894
|
-
`RESULT changed_paths claims paths outside the final product subject: ${extra.join(', ')}`,
|
|
895
|
-
'live.final_path_inventory',
|
|
896
|
-
));
|
|
897
|
-
}
|
|
898
|
-
const deleted = new Set((Array.isArray(result.deleted_paths) ? result.deleted_paths : []).filter(nonemptyString));
|
|
899
|
-
const manifestDeleted = new Set(manifest.deleted);
|
|
900
|
-
const deletedMismatch = [
|
|
901
|
-
...manifest.deleted.filter((file) => !deleted.has(file)),
|
|
902
|
-
...[...deleted].filter((file) => !manifestDeleted.has(file)),
|
|
903
|
-
];
|
|
904
|
-
if (deletedMismatch.length > 0) {
|
|
905
|
-
diagnostics.push(diagnostic(
|
|
906
|
-
'result_inventory_deleted_mismatch',
|
|
907
|
-
`RESULT deleted_paths disagrees with the deleted paths of the final product subject: ${deletedMismatch.join(', ')}`,
|
|
908
|
-
'live.final_path_inventory',
|
|
909
|
-
));
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
// Frozen-input final bytes are verified independently of the aggregate digest
|
|
913
|
-
// so a single unsafe identity is reported as itself, not masked by the
|
|
914
|
-
// digest tool's own refusal to observe the repository.
|
|
915
|
-
verifyFrozenInputFinalDigests(repoRoot, result, diagnostics);
|
|
916
|
-
|
|
917
|
-
// One finite coherent observation: the digest is re-observed exactly once
|
|
918
|
-
// after the manifest and frozen-input reads. A different second digest means
|
|
919
|
-
// the subject moved across the digest-to-manifest boundary and the receipt
|
|
920
|
-
// cannot be bound to what was actually verified — fail closed, re-run once
|
|
921
|
-
// after the repository is quiet. No retry loop.
|
|
922
|
-
const digestSecond = digestObservation(repoRoot);
|
|
923
|
-
if (digestSecond.error) {
|
|
924
|
-
diagnostics.push(diagnostic(
|
|
925
|
-
'result_binding_unverifiable',
|
|
926
|
-
`the terminal subject could not be re-observed coherently: ${digestSecond.error}`,
|
|
927
|
-
'live.final_worktree_digest',
|
|
928
|
-
));
|
|
929
|
-
return live;
|
|
930
|
-
}
|
|
931
|
-
if (digestSecond.diff_digest !== digest.diff_digest) {
|
|
932
|
-
diagnostics.push(diagnostic(
|
|
933
|
-
'subject_changed_during_observation',
|
|
934
|
-
'the product worktree digest changed between the first and the closing observation of this validation; the manifest and frozen-input reads were taken against a moving subject. Let the concurrent change finish, then re-run the validator once',
|
|
935
|
-
'live.final_worktree_digest',
|
|
936
|
-
));
|
|
937
|
-
return live;
|
|
938
|
-
}
|
|
939
|
-
live.final_worktree_digest = digestSecond.diff_digest;
|
|
940
|
-
if (digestSecond.diff_digest !== result.final_worktree_digest) {
|
|
941
|
-
diagnostics.push(diagnostic(
|
|
942
|
-
'result_digest_mismatch',
|
|
943
|
-
'the recomputed current product worktree digest differs from the RESULT final_worktree_digest; if this is the prescribed post-review closeout, publish the CLOSEOUT receipt of the closeout-transition contract — never rewrite the frozen RESULT, open a new handoff for it, or commit',
|
|
944
|
-
'live.final_worktree_digest',
|
|
945
|
-
));
|
|
946
|
-
}
|
|
947
|
-
return live;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
function observeTaskIdentity(repoRoot, identity, diagnostics, live) {
|
|
951
|
-
const read = readJsonFile(repoRoot, '.ultra/tasks.json');
|
|
952
|
-
if (read.readCode || read.jsonError) {
|
|
953
|
-
diagnostics.push(diagnostic(
|
|
954
|
-
'task_ledger_unreadable',
|
|
955
|
-
`the task ledger could not be stably read${read.readCode ? ` (${read.readCode})` : ''}: ${read.readMessage || read.jsonError}. Restore the ordinary ledger file, then re-run the validator`,
|
|
956
|
-
'live.task_identity',
|
|
957
|
-
));
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
if (read.missing) {
|
|
961
|
-
diagnostics.push(diagnostic(
|
|
962
|
-
'task_identity_missing',
|
|
963
|
-
`the task ledger is missing; the accepted task identity ${identity} cannot be confirmed`,
|
|
964
|
-
'live.task_identity',
|
|
965
|
-
));
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
live.task_identity_present = isPlainObject(read.parsed) && Array.isArray(read.parsed.tasks)
|
|
969
|
-
&& read.parsed.tasks.some((row) => isPlainObject(row) && row.id === identity);
|
|
970
|
-
if (!live.task_identity_present) {
|
|
971
|
-
diagnostics.push(diagnostic(
|
|
972
|
-
'task_identity_missing',
|
|
973
|
-
`the accepted task identity ${identity} is not present in .ultra/tasks.json`,
|
|
974
|
-
'live.task_identity',
|
|
975
|
-
));
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
function verifyFrozenInputFinalDigests(repoRoot, result, diagnostics, skipPaths = new Set()) {
|
|
980
|
-
const entries = Array.isArray(result.frozen_input_final_digests)
|
|
981
|
-
? result.frozen_input_final_digests.filter(isPlainObject)
|
|
982
|
-
: [];
|
|
983
|
-
for (const entry of entries) {
|
|
984
|
-
// An invalid or escaping path is rejected structurally and never accessed:
|
|
985
|
-
// no filesystem operation runs through it.
|
|
986
|
-
if (!normalizedRelativePath(entry.path)) continue;
|
|
987
|
-
if (skipPaths.has(entry.path)) continue;
|
|
988
|
-
if (entry.absent === true) {
|
|
989
|
-
let present = true;
|
|
990
|
-
try {
|
|
991
|
-
fs.lstatSync(path.join(repoRoot, entry.path));
|
|
992
|
-
} catch (error) {
|
|
993
|
-
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) present = false;
|
|
994
|
-
}
|
|
995
|
-
if (present) {
|
|
996
|
-
diagnostics.push(diagnostic(
|
|
997
|
-
'result_frozen_input_digest_mismatch',
|
|
998
|
-
`frozen_input_final_digests declares ${entry.path} absent, but the path exists`,
|
|
999
|
-
`live.frozen_inputs[${entry.path}]`,
|
|
1000
|
-
));
|
|
1001
|
-
}
|
|
1002
|
-
continue;
|
|
1003
|
-
}
|
|
1004
|
-
let read;
|
|
1005
|
-
try {
|
|
1006
|
-
read = stableReadBytes(repoRoot, entry.path, MAX_INPUT_BYTES);
|
|
1007
|
-
} catch (error) {
|
|
1008
|
-
diagnostics.push(diagnostic(
|
|
1009
|
-
'result_frozen_input_unreadable',
|
|
1010
|
-
`frozen input ${entry.path}: ${error.message}`,
|
|
1011
|
-
`live.frozen_inputs[${entry.path}]`,
|
|
1012
|
-
));
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
if (read.missing) {
|
|
1016
|
-
diagnostics.push(diagnostic(
|
|
1017
|
-
'result_frozen_input_digest_mismatch',
|
|
1018
|
-
`frozen_input_final_digests declares a digest for ${entry.path}, but the path no longer exists; declare it absent instead`,
|
|
1019
|
-
`live.frozen_inputs[${entry.path}]`,
|
|
1020
|
-
));
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
const sha = require('node:crypto').createHash('sha256').update(read.bytes).digest('hex');
|
|
1024
|
-
if (sha !== entry.sha256) {
|
|
1025
|
-
diagnostics.push(diagnostic(
|
|
1026
|
-
'result_frozen_input_digest_mismatch',
|
|
1027
|
-
`the recomputed final bytes of frozen input ${entry.path} differ from the declared SHA-256`,
|
|
1028
|
-
`live.frozen_inputs[${entry.path}]`,
|
|
1029
|
-
));
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// The versioned closeout-transition contract. A CLOSEOUT receipt separates the
|
|
1035
|
-
// immutable reviewed subject (the newest terminal v2 RESULT and the bytes it
|
|
1036
|
-
// froze) from exactly one uncommitted prescribed closeout: the post-review
|
|
1037
|
-
// ledger `completed` flip, the task context's closeout sections, and the final
|
|
1038
|
-
// evidence record. It never rewrites OFFER/ACK/RESULT bytes, never commits, and
|
|
1039
|
-
// starts no review and no handoff.
|
|
1040
|
-
function prescribedCloseoutPaths(identity) {
|
|
1041
|
-
return [
|
|
1042
|
-
'.ultra/tasks.json',
|
|
1043
|
-
`.ultra/contexts/task-${identity}.md`,
|
|
1044
|
-
`.ultra/evidence/${identity}/evidence.json`,
|
|
1045
|
-
];
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function closeoutBoundaryIndex(text) {
|
|
1049
|
-
let best = -1;
|
|
1050
|
-
for (const heading of CLOSEOUT_SECTION_HEADINGS) {
|
|
1051
|
-
const at = text.indexOf(`${heading}\n`);
|
|
1052
|
-
if (at !== -1 && (best === -1 || at < best)) best = at;
|
|
1053
|
-
}
|
|
1054
|
-
return best;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function closeoutSiblingsShape(value, identity) {
|
|
1058
|
-
if (!Array.isArray(value)) return false;
|
|
1059
|
-
const pattern = new RegExp(`^\\.ultra/evidence/${identity}/[A-Za-z0-9][A-Za-z0-9._-]*$`);
|
|
1060
|
-
const seen = new Set();
|
|
1061
|
-
for (const entry of value) {
|
|
1062
|
-
if (!isPlainObject(entry) || !pattern.test(String(entry.path || ''))
|
|
1063
|
-
|| entry.path === `.ultra/evidence/${identity}/evidence.json`
|
|
1064
|
-
|| !SHA256_PATTERN.test(String(entry.sha256 || ''))) return false;
|
|
1065
|
-
if (seen.has(entry.path)) return false;
|
|
1066
|
-
seen.add(entry.path);
|
|
1067
|
-
}
|
|
1068
|
-
return true;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
function stableShaOr(report, relative) {
|
|
1072
|
-
let read;
|
|
1073
|
-
try {
|
|
1074
|
-
read = stableReadBytes(report.root, relative, MAX_INPUT_BYTES);
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
return { error: error.message };
|
|
1077
|
-
}
|
|
1078
|
-
if (read.missing) return { missing: true };
|
|
1079
|
-
return { sha256: crypto.createHash('sha256').update(read.bytes).digest('hex'), text: read.text };
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Canonical structure-preserving JSON with sorted object keys, so a row digest
|
|
1083
|
-
// never depends on key order — only on the row's fields and their values.
|
|
1084
|
-
function canonicalJsonDigest(value) {
|
|
1085
|
-
const canonical = (item) => {
|
|
1086
|
-
if (Array.isArray(item)) return `[${item.map(canonical).join(',')}]`;
|
|
1087
|
-
if (isPlainObject(item)) {
|
|
1088
|
-
return `{${Object.keys(item).sort().map((key) => `${JSON.stringify(key)}:${canonical(item[key])}`).join(',')}}`;
|
|
1089
|
-
}
|
|
1090
|
-
return JSON.stringify(item);
|
|
1091
|
-
};
|
|
1092
|
-
return crypto.createHash('sha256').update(Buffer.from(canonical(value))).digest('hex');
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
function rowExStatusDigest(row) {
|
|
1096
|
-
const { status, ...rest } = row;
|
|
1097
|
-
return canonicalJsonDigest(rest);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// Exactly-one lookup of the closed task's ledger row.
|
|
1101
|
-
function ledgerTaskRows(repoRoot, identity) {
|
|
1102
|
-
const read = readJsonFile(repoRoot, '.ultra/tasks.json');
|
|
1103
|
-
if (read.missing) return { error: 'the task ledger is missing' };
|
|
1104
|
-
if (read.readCode || read.jsonError) {
|
|
1105
|
-
return { error: read.readMessage || read.jsonError || 'the task ledger is unreadable' };
|
|
1106
|
-
}
|
|
1107
|
-
if (!isPlainObject(read.parsed) || !Array.isArray(read.parsed.tasks)) {
|
|
1108
|
-
return { error: 'the task ledger is not a task-ledger object' };
|
|
1109
|
-
}
|
|
1110
|
-
return {
|
|
1111
|
-
rows: read.parsed.tasks.filter((row) => isPlainObject(row) && row.id === identity),
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// Bind the existing external-review receipt semantics: read-only reviewer,
|
|
1116
|
-
// exact task/change identity, reviewer authority and reviewed contract by
|
|
1117
|
-
// stable bytes, subject equal to the closeout start, approve verdict with no
|
|
1118
|
-
// P0/P1 finding. Anything else is a typed authorization stop.
|
|
1119
|
-
function bindExternalReviewReceipt(text, closeout, repoRoot, diagnostics) {
|
|
1120
|
-
const stop = (message, location = 'CLOSEOUT.json.authorized_by') => diagnostics.push(
|
|
1121
|
-
diagnostic('closeout_authorization', message, location),
|
|
1122
|
-
);
|
|
1123
|
-
let receipt = null;
|
|
1124
|
-
try {
|
|
1125
|
-
receipt = JSON.parse(text);
|
|
1126
|
-
} catch {
|
|
1127
|
-
return stop('the authorized_by receipt is not valid JSON');
|
|
1128
|
-
}
|
|
1129
|
-
if (!isPlainObject(receipt)) return stop('the authorized_by receipt must be a JSON object');
|
|
1130
|
-
if (receipt.$schema !== EXTERNAL_RECEIPT_SCHEMA) {
|
|
1131
|
-
stop(`the authorized_by receipt must use ${EXTERNAL_RECEIPT_SCHEMA}; a strict SUMMARY or invented schema never authorizes a closeout`);
|
|
1132
|
-
}
|
|
1133
|
-
const keys = Object.keys(receipt).sort();
|
|
1134
|
-
if (JSON.stringify(keys) !== JSON.stringify([...EXTERNAL_RECEIPT_KEYS].sort())) {
|
|
1135
|
-
stop('the authorized_by receipt must carry exactly the external-review receipt fields');
|
|
1136
|
-
}
|
|
1137
|
-
if (!nonemptyString(receipt.reviewer) || receipt.reviewer_role !== 'read-only') {
|
|
1138
|
-
stop('the external reviewer must be identified and read-only');
|
|
1139
|
-
}
|
|
1140
|
-
if (receipt.task_id !== closeout.task_identity) {
|
|
1141
|
-
stop('the external receipt task_id must equal the closeout task identity');
|
|
1142
|
-
}
|
|
1143
|
-
if (receipt.verdict !== 'approve') {
|
|
1144
|
-
stop('a prescribed closeout is authorized only by an approve verdict');
|
|
1145
|
-
}
|
|
1146
|
-
if (typeof receipt.timestamp !== 'string' || !Number.isFinite(Date.parse(receipt.timestamp))) {
|
|
1147
|
-
stop('the authorized_by receipt needs an RFC 3339 timestamp');
|
|
1148
|
-
}
|
|
1149
|
-
const findings = receipt.findings;
|
|
1150
|
-
if (!Array.isArray(findings)
|
|
1151
|
-
|| !findings.every((finding) => isPlainObject(finding)
|
|
1152
|
-
&& nonemptyString(finding.id)
|
|
1153
|
-
&& nonemptyString(finding.title)
|
|
1154
|
-
&& EXTERNAL_SEVERITIES.includes(finding.severity))) {
|
|
1155
|
-
stop('the authorized_by findings must be an exact array of {id, severity P0-P3, title}');
|
|
1156
|
-
} else {
|
|
1157
|
-
const ids = new Set(findings.map((finding) => finding.id));
|
|
1158
|
-
if (ids.size !== findings.length) stop('the authorized_by finding ids must be unique');
|
|
1159
|
-
if (findings.some((finding) => finding.severity === 'P0' || finding.severity === 'P1')) {
|
|
1160
|
-
stop('an approve verdict with unresolved P0/P1 findings cannot authorize a closeout');
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
const subject = receipt.subject;
|
|
1164
|
-
if (!isPlainObject(subject)
|
|
1165
|
-
|| !HEAD_PATTERN.test(String(subject.git_head || ''))
|
|
1166
|
-
|| !SHA256_PATTERN.test(String(subject.worktree_digest || ''))
|
|
1167
|
-
|| subject.git_head !== closeout.subject_before?.head
|
|
1168
|
-
|| subject.worktree_digest !== closeout.subject_before?.worktree_digest) {
|
|
1169
|
-
stop('the reviewed subject must be exactly the closeout-start HEAD and worktree digest');
|
|
1170
|
-
}
|
|
1171
|
-
for (const field of ['reviewed_contract', 'reviewer_authority']) {
|
|
1172
|
-
const ref = receipt[field];
|
|
1173
|
-
if (!isPlainObject(ref) || !normalizedRelativePath(String(ref.ref || ''))
|
|
1174
|
-
|| !SHA256_PATTERN.test(String(ref.sha256 || ''))) {
|
|
1175
|
-
stop(`the authorized_by ${field} must cite a repository ref and SHA-256`);
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
const observed = stableShaOr({ root: repoRoot }, ref.ref);
|
|
1179
|
-
if (observed.error || observed.missing) {
|
|
1180
|
-
stop(`the authorized_by ${field} ref cannot be stably read: ${ref.ref}`);
|
|
1181
|
-
} else if (observed.sha256 !== ref.sha256) {
|
|
1182
|
-
stop(`the recomputed bytes of the authorized_by ${field} differ from the cited SHA-256`);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
// The receipt must be planned in the task context's Planned Path Inventory
|
|
1186
|
-
// before the review: the repository's evidence audit and this contract
|
|
1187
|
-
// agree only when the receipt path is a planned entry of the closed task.
|
|
1188
|
-
const authPath = closeout.authorized_by?.path;
|
|
1189
|
-
const contextRelative = `.ultra/contexts/task-${closeout.task_identity}.md`;
|
|
1190
|
-
const contextObserved = stableShaOr({ root: repoRoot }, contextRelative);
|
|
1191
|
-
if (contextObserved.error || contextObserved.missing) {
|
|
1192
|
-
stop('the task context cannot be stably read to verify the review receipt is planned');
|
|
1193
|
-
} else if (typeof authPath === 'string') {
|
|
1194
|
-
const heading = '## Planned Path Inventory';
|
|
1195
|
-
const at = contextObserved.text.indexOf(heading);
|
|
1196
|
-
const after = at === -1 ? -1 : at + heading.length;
|
|
1197
|
-
const end = after === -1 ? -1 : contextObserved.text.indexOf('\n## ', after);
|
|
1198
|
-
const section = after === -1 || end === -1 ? null : contextObserved.text.slice(after, end);
|
|
1199
|
-
// The same exact bullet-path set the repository artifact audit accepts:
|
|
1200
|
-
// `- `path`` lines only, so a near-match line (for example a planned
|
|
1201
|
-
// `external-review.json.bak`) is not the planned receipt.
|
|
1202
|
-
const planned = section === null
|
|
1203
|
-
? new Set()
|
|
1204
|
-
: new Set([...section.matchAll(/^- `([^`\n]+)`$/gmu)].map((match) => match[1]));
|
|
1205
|
-
if (!planned.has(authPath)) {
|
|
1206
|
-
stop('the external review receipt must be a Planned Path Inventory entry of the task context before the review');
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return receipt;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
function validateCloseout(offer, result, payload, handoffId, repoRoot, diagnostics) {
|
|
1213
|
-
if (!isPlainObject(payload)) {
|
|
1214
|
-
diagnostics.push(diagnostic('closeout_schema', 'CLOSEOUT.json must be a JSON object', 'CLOSEOUT.json'));
|
|
1215
|
-
return null;
|
|
1216
|
-
}
|
|
1217
|
-
const bad = (message, location) => diagnostics.push(diagnostic('closeout_schema', message, location));
|
|
1218
|
-
if (payload.$schema !== CLOSEOUT_SCHEMA) {
|
|
1219
|
-
bad(`CLOSEOUT $schema must be ${CLOSEOUT_SCHEMA}`, 'CLOSEOUT.json.$schema');
|
|
1220
|
-
}
|
|
1221
|
-
if (payload.handoff_id !== handoffId) {
|
|
1222
|
-
bad('CLOSEOUT handoff_id must match the OFFER handoff id', 'CLOSEOUT.json.handoff_id');
|
|
1223
|
-
}
|
|
1224
|
-
const identity = offer?.accepted_scope?.new_task_identity;
|
|
1225
|
-
if (!nonemptyString(payload.task_identity) || payload.task_identity !== identity) {
|
|
1226
|
-
bad('CLOSEOUT task_identity must equal the OFFER accepted task identity', 'CLOSEOUT.json.task_identity');
|
|
1227
|
-
}
|
|
1228
|
-
const closes = payload.closes_result;
|
|
1229
|
-
if (!isPlainObject(closes) || closes.$schema !== RESULT_V2_SCHEMA
|
|
1230
|
-
|| closes.final_head !== result.final_head
|
|
1231
|
-
|| closes.final_worktree_digest !== result.final_worktree_digest) {
|
|
1232
|
-
bad('CLOSEOUT closes_result must cite this handoff RESULT schema, final HEAD, and final worktree digest exactly', 'CLOSEOUT.json.closes_result');
|
|
1233
|
-
}
|
|
1234
|
-
const expectedPaths = prescribedCloseoutPaths(String(identity || ''));
|
|
1235
|
-
const prescribedSet = new Set(expectedPaths);
|
|
1236
|
-
const prescribed = Array.isArray(payload.prescribed_paths) ? [...payload.prescribed_paths].sort() : null;
|
|
1237
|
-
if (prescribed === null || prescribed.length !== expectedPaths.length
|
|
1238
|
-
|| JSON.stringify(prescribed) !== JSON.stringify([...expectedPaths].sort())) {
|
|
1239
|
-
bad('CLOSEOUT prescribed_paths must be exactly the task ledger, the task context, and the final evidence record of the accepted task identity', 'CLOSEOUT.json.prescribed_paths');
|
|
1240
|
-
}
|
|
1241
|
-
const auth = payload.authorized_by;
|
|
1242
|
-
let authText = null;
|
|
1243
|
-
if (!isPlainObject(auth) || !normalizedRelativePath(String(auth.path || ''))
|
|
1244
|
-
|| !SHA256_PATTERN.test(String(auth.sha256 || ''))
|
|
1245
|
-
|| !String(auth.path || '').startsWith(`.ultra/evidence/${payload.task_identity}/`)) {
|
|
1246
|
-
bad('CLOSEOUT authorized_by must cite an evidence receipt of the accepted task identity by path and SHA-256', 'CLOSEOUT.json.authorized_by');
|
|
1247
|
-
} else {
|
|
1248
|
-
const observed = stableShaOr({ root: repoRoot }, auth.path);
|
|
1249
|
-
if (observed.error || observed.missing) {
|
|
1250
|
-
bad(`the CLOSEOUT authorized_by receipt cannot be stably read: ${auth.path}`, 'CLOSEOUT.json.authorized_by');
|
|
1251
|
-
} else if (observed.sha256 !== auth.sha256) {
|
|
1252
|
-
bad('the recomputed bytes of the CLOSEOUT authorized_by receipt differ from the cited SHA-256', 'CLOSEOUT.json.authorized_by');
|
|
1253
|
-
} else {
|
|
1254
|
-
authText = observed.text;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
const effects = payload.effects_declined;
|
|
1258
|
-
if (!isPlainObject(effects) || effects.commit !== false
|
|
1259
|
-
|| effects.review_started !== false || effects.handoff_started !== false) {
|
|
1260
|
-
bad('a prescribed closeout declines commit, review, and handoff effects exactly (all three false)', 'CLOSEOUT.json.effects_declined');
|
|
1261
|
-
}
|
|
1262
|
-
const shaFields = Object.freeze([
|
|
1263
|
-
'worktree_digest', 'ledger_sha256', 'context_sha256',
|
|
1264
|
-
'ledger_rows_ex_task_sha256', 'ledger_row_ex_status_sha256',
|
|
1265
|
-
'context_prefix_sha256',
|
|
1266
|
-
]);
|
|
1267
|
-
const before = payload.subject_before;
|
|
1268
|
-
const after = payload.subject_after;
|
|
1269
|
-
const shapeSubject = (subject, label) => {
|
|
1270
|
-
if (!isPlainObject(subject)) {
|
|
1271
|
-
bad(`CLOSEOUT ${label} must be an object`, `CLOSEOUT.json.${label}`);
|
|
1272
|
-
return false;
|
|
1273
|
-
}
|
|
1274
|
-
for (const field of shaFields) {
|
|
1275
|
-
if (!SHA256_PATTERN.test(String(subject[field] || ''))) {
|
|
1276
|
-
bad(`CLOSEOUT ${label}.${field} must be a 64-character lowercase SHA-256`, `CLOSEOUT.json.${label}.${field}`);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
if (!HEAD_PATTERN.test(String(subject.head || ''))) {
|
|
1280
|
-
bad(`CLOSEOUT ${label}.head must be a 40-character lowercase commit id`, `CLOSEOUT.json.${label}.head`);
|
|
1281
|
-
}
|
|
1282
|
-
if (!closeoutSiblingsShape(subject.evidence_siblings, String(payload.task_identity || ''))) {
|
|
1283
|
-
bad(`CLOSEOUT ${label}.evidence_siblings must list distinct sibling evidence receipts with their SHA-256`, `CLOSEOUT.json.${label}.evidence_siblings`);
|
|
1284
|
-
}
|
|
1285
|
-
return true;
|
|
1286
|
-
};
|
|
1287
|
-
const beforeOk = shapeSubject(before, 'subject_before');
|
|
1288
|
-
const afterOk = shapeSubject(after, 'subject_after');
|
|
1289
|
-
if (beforeOk && before.evidence_json_absent !== true) {
|
|
1290
|
-
bad('the final evidence record is written only at closeout; CLOSEOUT subject_before must record it absent', 'CLOSEOUT.json.subject_before.evidence_json_absent');
|
|
1291
|
-
}
|
|
1292
|
-
if (afterOk && !SHA256_PATTERN.test(String(after.evidence_json_sha256 || ''))) {
|
|
1293
|
-
bad('CLOSEOUT subject_after.evidence_json_sha256 must be a 64-character lowercase SHA-256', 'CLOSEOUT.json.subject_after.evidence_json_sha256');
|
|
1294
|
-
}
|
|
1295
|
-
const continuationEntries = Array.isArray(payload.continuation?.delta_paths)
|
|
1296
|
-
? payload.continuation.delta_paths.filter(isPlainObject)
|
|
1297
|
-
: [];
|
|
1298
|
-
const deltaByPath = new Map(continuationEntries.map((entry) => [entry.path, entry]));
|
|
1299
|
-
if (beforeOk && afterOk) {
|
|
1300
|
-
const rowStop = (message) => diagnostics.push(diagnostic(
|
|
1301
|
-
'closeout_task_row',
|
|
1302
|
-
message,
|
|
1303
|
-
'CLOSEOUT.json.task_row',
|
|
1304
|
-
));
|
|
1305
|
-
if (HEAD_PATTERN.test(String(before.head || '')) && before.head !== result.final_head) {
|
|
1306
|
-
bad('CLOSEOUT subject_before.head must be the frozen final HEAD; the closeout never starts from a moved history', 'CLOSEOUT.json.subject_before.head');
|
|
1307
|
-
}
|
|
1308
|
-
if (HEAD_PATTERN.test(String(after.head || '')) && after.head !== result.final_head) {
|
|
1309
|
-
bad('CLOSEOUT subject_after.head must be the frozen final HEAD; the closeout never commits', 'CLOSEOUT.json.subject_after.head');
|
|
1310
|
-
}
|
|
1311
|
-
if (before.ledger_row_status !== 'in_progress') {
|
|
1312
|
-
rowStop('the closed task row must start in_progress at closeout (subject_before.ledger_row_status)');
|
|
1313
|
-
}
|
|
1314
|
-
if (after.ledger_row_status !== 'completed') {
|
|
1315
|
-
rowStop('the closed task row must end completed (subject_after.ledger_row_status)');
|
|
1316
|
-
}
|
|
1317
|
-
if (SHA256_PATTERN.test(String(before.ledger_row_ex_status_sha256 || ''))
|
|
1318
|
-
&& SHA256_PATTERN.test(String(after.ledger_row_ex_status_sha256 || ''))
|
|
1319
|
-
&& before.ledger_row_ex_status_sha256 !== after.ledger_row_ex_status_sha256) {
|
|
1320
|
-
rowStop('the task row changed beyond its status flip across the closeout: the canonical ex-status digests disagree');
|
|
1321
|
-
}
|
|
1322
|
-
for (const field of ['ledger_rows_ex_task_sha256', 'context_prefix_sha256']) {
|
|
1323
|
-
if (SHA256_PATTERN.test(String(before[field] || '')) && before[field] !== after[field]) {
|
|
1324
|
-
bad(`CLOSEOUT subject_before.${field} must equal subject_after.${field}: the closeout never touches that scope`, `CLOSEOUT.json.subject_after.${field}`);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
if (JSON.stringify(before.evidence_siblings || []) !== JSON.stringify(after.evidence_siblings || [])) {
|
|
1328
|
-
bad('CLOSEOUT evidence siblings must be identical before and after the closeout; pre-review evidence stays untouched', 'CLOSEOUT.json.subject_after.evidence_siblings');
|
|
1329
|
-
}
|
|
1330
|
-
// A prescribed path that is also an OFFER frozen input must reach closeout
|
|
1331
|
-
// still on its frozen final bytes, or carry that delta in a declared
|
|
1332
|
-
// continuation. With no continuation at all, the live binding decides —
|
|
1333
|
-
// a moved subject without any recorded continuation is a binding stop.
|
|
1334
|
-
const continuationDeclared = payload.continuation !== undefined;
|
|
1335
|
-
const beforeShaFor = (relative) => {
|
|
1336
|
-
if (relative === '.ultra/tasks.json') return before.ledger_sha256;
|
|
1337
|
-
if (relative === `.ultra/contexts/task-${payload.task_identity}.md`) return before.context_sha256;
|
|
1338
|
-
return null;
|
|
1339
|
-
};
|
|
1340
|
-
if (continuationDeclared) {
|
|
1341
|
-
for (const entry of Array.isArray(result.frozen_input_final_digests)
|
|
1342
|
-
? result.frozen_input_final_digests.filter(isPlainObject)
|
|
1343
|
-
: []) {
|
|
1344
|
-
if (!prescribedSet.has(entry.path) || entry.absent === true) continue;
|
|
1345
|
-
const beforeSha = beforeShaFor(entry.path);
|
|
1346
|
-
if (beforeSha === null || beforeSha === entry.sha256) continue;
|
|
1347
|
-
const delta = deltaByPath.get(entry.path);
|
|
1348
|
-
if (!isPlainObject(delta) || delta.sha256 !== beforeSha) {
|
|
1349
|
-
bad(`the prescribed path ${entry.path} moved off its frozen final bytes before closeout; record that owner-authorized continuation delta or return to the owner`, `CLOSEOUT.json.continuation.delta_paths[${entry.path}]`);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
const continuation = payload.continuation;
|
|
1355
|
-
if (continuation !== undefined) {
|
|
1356
|
-
if (!isPlainObject(continuation)
|
|
1357
|
-
|| !SHA256_PATTERN.test(String(continuation.from_worktree_digest || ''))
|
|
1358
|
-
|| continuation.from_worktree_digest !== result.final_worktree_digest) {
|
|
1359
|
-
bad('CLOSEOUT continuation must anchor from_worktree_digest at the RESULT final_worktree_digest', 'CLOSEOUT.json.continuation.from_worktree_digest');
|
|
1360
|
-
}
|
|
1361
|
-
const deltaPaths = continuation?.delta_paths;
|
|
1362
|
-
if (!Array.isArray(deltaPaths) || deltaPaths.length === 0
|
|
1363
|
-
|| !deltaPaths.every((entry) => isPlainObject(entry)
|
|
1364
|
-
&& normalizedRelativePath(entry.path)
|
|
1365
|
-
&& SHA256_PATTERN.test(String(entry.sha256 || '')))) {
|
|
1366
|
-
bad('CLOSEOUT continuation.delta_paths must list normalized repository-relative paths with SHA-256 bytes', 'CLOSEOUT.json.continuation.delta_paths');
|
|
1367
|
-
} else {
|
|
1368
|
-
const seen = new Set();
|
|
1369
|
-
for (const entry of deltaPaths) {
|
|
1370
|
-
if (seen.has(entry.path)) {
|
|
1371
|
-
bad(`CLOSEOUT continuation.delta_paths repeats ${entry.path}`, `CLOSEOUT.json.continuation.delta_paths[${entry.path}]`);
|
|
1372
|
-
}
|
|
1373
|
-
seen.add(entry.path);
|
|
1374
|
-
if (!prescribedSet.has(entry.path)) continue;
|
|
1375
|
-
const beforeSha = entry.path === '.ultra/tasks.json' ? before?.ledger_sha256 : before?.context_sha256;
|
|
1376
|
-
if (beforeSha !== entry.sha256) {
|
|
1377
|
-
bad(`the continuation delta for the prescribed path ${entry.path} must equal the closeout-start bytes`, `CLOSEOUT.json.continuation.delta_paths[${entry.path}]`);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
const refs = continuation?.authority_refs;
|
|
1382
|
-
if (!Array.isArray(refs) || refs.length === 0
|
|
1383
|
-
|| !refs.every((ref) => isPlainObject(ref) && normalizedRelativePath(String(ref.path || ''))
|
|
1384
|
-
&& SHA256_PATTERN.test(String(ref.sha256 || '')))) {
|
|
1385
|
-
bad('CLOSEOUT continuation.authority_refs must cite readable canonical records by path and SHA-256', 'CLOSEOUT.json.continuation.authority_refs');
|
|
1386
|
-
} else {
|
|
1387
|
-
for (const ref of refs) {
|
|
1388
|
-
const observed = stableShaOr({ root: repoRoot }, ref.path);
|
|
1389
|
-
if (observed.error || observed.missing) {
|
|
1390
|
-
bad(`the continuation authority ref cannot be stably read: ${ref.path}`, `CLOSEOUT.json.continuation.authority_refs[${ref.path}]`);
|
|
1391
|
-
} else if (observed.sha256 !== ref.sha256) {
|
|
1392
|
-
bad(`the recomputed bytes of the continuation authority ref ${ref.path} differ from the cited SHA-256`, `CLOSEOUT.json.continuation.authority_refs[${ref.path}]`);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
// The closed task's ledger row is unique and present — missing or duplicate
|
|
1398
|
-
// rows are typed stops in every validation mode.
|
|
1399
|
-
const rows = ledgerTaskRows(repoRoot, String(payload.task_identity || ''));
|
|
1400
|
-
let singleRow = null;
|
|
1401
|
-
const rowStopS = (message) => diagnostics.push(diagnostic('closeout_task_row', message, 'CLOSEOUT.json.task_row'));
|
|
1402
|
-
if (rows.error) {
|
|
1403
|
-
rowStopS(`the task ledger could not be read (${rows.error})`);
|
|
1404
|
-
} else if (rows.rows.length === 0) {
|
|
1405
|
-
rowStopS(`the task ledger no longer carries the closed task row ${payload.task_identity}`);
|
|
1406
|
-
} else if (rows.rows.length > 1) {
|
|
1407
|
-
rowStopS(`the task ledger carries duplicate rows for ${payload.task_identity}`);
|
|
1408
|
-
} else {
|
|
1409
|
-
singleRow = rows.rows[0];
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Bind the authorization receipt to the existing external-review semantics,
|
|
1413
|
-
// and its change identity to the closed ledger row's own change_id.
|
|
1414
|
-
if (authText !== null) {
|
|
1415
|
-
const bound = bindExternalReviewReceipt(authText, payload, repoRoot, diagnostics);
|
|
1416
|
-
if (isPlainObject(bound) && singleRow && bound.change_id !== singleRow.change_id) {
|
|
1417
|
-
diagnostics.push(diagnostic(
|
|
1418
|
-
'closeout_authorization',
|
|
1419
|
-
'the external receipt change_id must equal the closed task ledger row change_id',
|
|
1420
|
-
'CLOSEOUT.json.authorized_by',
|
|
1421
|
-
));
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
if (diagnostics.some((item) => item.code === 'closeout_schema' && item.location?.startsWith('CLOSEOUT.json'))) {
|
|
1426
|
-
return null;
|
|
1427
|
-
}
|
|
1428
|
-
return payload;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Live binding of the newest terminal v2 RESULT with an attached CLOSEOUT
|
|
1432
|
-
// receipt: current reality must equal the recorded closeout end-state, the
|
|
1433
|
-
// reviewed implementation subject stays pinned (aggregate digest, inventory,
|
|
1434
|
-
// frozen inputs outside the prescribed paths), and the closeout-only scopes
|
|
1435
|
-
// (ledger rows outside the task, the context before its closeout sections, the
|
|
1436
|
-
// pre-review evidence siblings) stay byte-frozen. Any later write fails typed;
|
|
1437
|
-
// the contract offers no re-freeze, no second closeout, and no retry loop.
|
|
1438
|
-
function liveCloseoutObservations(offer, result, closeout, repoRoot, diagnostics, live) {
|
|
1439
|
-
const identity = closeout.task_identity;
|
|
1440
|
-
const prescribed = prescribedCloseoutPaths(identity);
|
|
1441
|
-
const prescribedSet = new Set(prescribed);
|
|
1442
|
-
const digest = digestObservation(repoRoot);
|
|
1443
|
-
if (digest.error) {
|
|
1444
|
-
diagnostics.push(diagnostic(
|
|
1445
|
-
'result_binding_unverifiable',
|
|
1446
|
-
`the closed-out subject could not be re-observed: ${digest.error}`,
|
|
1447
|
-
'live.final_worktree_digest',
|
|
1448
|
-
));
|
|
1449
|
-
return live;
|
|
1450
|
-
}
|
|
1451
|
-
let manifest = null;
|
|
1452
|
-
try {
|
|
1453
|
-
manifest = manifestAgainst(repoRoot, offer?.repository?.base_head, digest);
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
diagnostics.push(diagnostic(
|
|
1456
|
-
'result_binding_unverifiable',
|
|
1457
|
-
`the closed-out product path inventory could not be observed: ${error && error.message ? error.message.split('\n')[0] : 'git failed'}`,
|
|
1458
|
-
'live.final_path_inventory',
|
|
1459
|
-
));
|
|
1460
|
-
}
|
|
1461
|
-
if (manifest) {
|
|
1462
|
-
const expected = new Set([
|
|
1463
|
-
...(Array.isArray(result.changed_paths) ? result.changed_paths.filter(nonemptyString) : []),
|
|
1464
|
-
prescribed[0],
|
|
1465
|
-
prescribed[1],
|
|
1466
|
-
]);
|
|
1467
|
-
for (const entry of closeout.continuation?.delta_paths || []) {
|
|
1468
|
-
if (isPlainObject(entry) && normalizedRelativePath(entry.path)) expected.add(entry.path);
|
|
1469
|
-
}
|
|
1470
|
-
const manifestChanged = new Set(manifest.changed);
|
|
1471
|
-
const unpinned = manifest.changed.filter((file) => !expected.has(file));
|
|
1472
|
-
if (unpinned.length > 0) {
|
|
1473
|
-
diagnostics.push(diagnostic(
|
|
1474
|
-
'closeout_binding',
|
|
1475
|
-
`product paths outside the closed-out subject and its recorded continuation: ${unpinned.join(', ')}`,
|
|
1476
|
-
'live.final_path_inventory',
|
|
1477
|
-
));
|
|
1478
|
-
}
|
|
1479
|
-
const vanished = [...expected].filter((file) => file !== prescribed[2] && !manifestChanged.has(file));
|
|
1480
|
-
if (vanished.length > 0) {
|
|
1481
|
-
diagnostics.push(diagnostic(
|
|
1482
|
-
'closeout_binding',
|
|
1483
|
-
`pinned product paths disappeared from the closed-out subject: ${vanished.join(', ')}`,
|
|
1484
|
-
'live.final_path_inventory',
|
|
1485
|
-
));
|
|
1486
|
-
}
|
|
1487
|
-
const deleted = new Set((Array.isArray(result.deleted_paths) ? result.deleted_paths : []).filter(nonemptyString));
|
|
1488
|
-
const deletedMismatch = [
|
|
1489
|
-
...manifest.deleted.filter((file) => !deleted.has(file)),
|
|
1490
|
-
...[...deleted].filter((file) => !new Set(manifest.deleted).has(file)),
|
|
1491
|
-
];
|
|
1492
|
-
if (deletedMismatch.length > 0) {
|
|
1493
|
-
diagnostics.push(diagnostic(
|
|
1494
|
-
'closeout_binding',
|
|
1495
|
-
`deletions disagree with the closed-out subject: ${deletedMismatch.join(', ')}`,
|
|
1496
|
-
'live.final_path_inventory',
|
|
1497
|
-
));
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
for (const entry of closeout.continuation?.delta_paths || []) {
|
|
1501
|
-
if (!isPlainObject(entry) || !normalizedRelativePath(entry.path) || prescribedSet.has(entry.path)) continue;
|
|
1502
|
-
const observed = stableShaOr({ root: repoRoot }, entry.path);
|
|
1503
|
-
if (observed.error || observed.missing) {
|
|
1504
|
-
diagnostics.push(diagnostic(
|
|
1505
|
-
'closeout_binding',
|
|
1506
|
-
`the continuation path ${entry.path} can no longer be observed (${observed.missing ? 'missing' : 'unreadable'})`,
|
|
1507
|
-
`live.continuation[${entry.path}]`,
|
|
1508
|
-
));
|
|
1509
|
-
} else if (observed.sha256 !== entry.sha256) {
|
|
1510
|
-
diagnostics.push(diagnostic(
|
|
1511
|
-
'closeout_binding',
|
|
1512
|
-
`the current bytes of the continuation path ${entry.path} differ from the recorded continuation SHA-256`,
|
|
1513
|
-
`live.continuation[${entry.path}]`,
|
|
1514
|
-
));
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
verifyFrozenInputFinalDigests(repoRoot, result, diagnostics, prescribedSet);
|
|
1518
|
-
|
|
1519
|
-
const scope = (message, location) => diagnostics.push(diagnostic('closeout_scope_drift', message, location));
|
|
1520
|
-
const pinFile = (relative, expectedSha, label) => {
|
|
1521
|
-
let read;
|
|
1522
|
-
try {
|
|
1523
|
-
read = stableReadBytes(repoRoot, relative, MAX_INPUT_BYTES);
|
|
1524
|
-
} catch (error) {
|
|
1525
|
-
scope(`the ${label} ${relative} cannot be stably observed: ${error.message}`, `live.closeout.${label}`);
|
|
1526
|
-
return null;
|
|
1527
|
-
}
|
|
1528
|
-
if (read.missing) {
|
|
1529
|
-
scope(`the ${label} ${relative} is missing after closeout`, `live.closeout.${label}`);
|
|
1530
|
-
return null;
|
|
1531
|
-
}
|
|
1532
|
-
const sha = crypto.createHash('sha256').update(read.bytes).digest('hex');
|
|
1533
|
-
if (sha !== expectedSha) {
|
|
1534
|
-
scope(`the current bytes of the ${label} differ from the closeout end-state SHA-256`, `live.closeout.${label}`);
|
|
1535
|
-
}
|
|
1536
|
-
return read;
|
|
1537
|
-
};
|
|
1538
|
-
const ledgerRead = pinFile(prescribed[0], closeout.subject_after.ledger_sha256, 'ledger');
|
|
1539
|
-
const contextRead = pinFile(prescribed[1], closeout.subject_after.context_sha256, 'context');
|
|
1540
|
-
pinFile(prescribed[2], closeout.subject_after.evidence_json_sha256, 'evidence');
|
|
1541
|
-
|
|
1542
|
-
if (ledgerRead && !ledgerRead.missing) {
|
|
1543
|
-
try {
|
|
1544
|
-
const parsed = JSON.parse(ledgerRead.text);
|
|
1545
|
-
if (!isPlainObject(parsed) || !Array.isArray(parsed.tasks)) throw new Error('not a task ledger object');
|
|
1546
|
-
const rowsDigest = crypto.createHash('sha256').update(Buffer.from(JSON.stringify({
|
|
1547
|
-
...parsed,
|
|
1548
|
-
tasks: parsed.tasks.filter((row) => isPlainObject(row) && row.id !== identity),
|
|
1549
|
-
}))).digest('hex');
|
|
1550
|
-
if (rowsDigest !== closeout.subject_after.ledger_rows_ex_task_sha256) {
|
|
1551
|
-
scope('ledger rows outside the closed-out task changed; unrelated authority stays stale at closeout', 'live.closeout.ledger_rows');
|
|
1552
|
-
}
|
|
1553
|
-
// Live readback of the closed task's own row: unique, completed, and
|
|
1554
|
-
// byte/structure-equivalent beyond the status flip.
|
|
1555
|
-
const rowStop = (message) => diagnostics.push(diagnostic('closeout_task_row', message, 'live.closeout.task_row'));
|
|
1556
|
-
const currentRows = parsed.tasks.filter((row) => isPlainObject(row) && row.id === identity);
|
|
1557
|
-
if (currentRows.length === 0) {
|
|
1558
|
-
rowStop(`the task ledger no longer carries the closed task row ${identity}`);
|
|
1559
|
-
} else if (currentRows.length > 1) {
|
|
1560
|
-
rowStop(`the task ledger carries duplicate rows for ${identity}`);
|
|
1561
|
-
} else {
|
|
1562
|
-
if (currentRows[0].status !== 'completed') {
|
|
1563
|
-
rowStop('the closed task row did not end completed');
|
|
1564
|
-
}
|
|
1565
|
-
if (rowExStatusDigest(currentRows[0]) !== closeout.subject_after.ledger_row_ex_status_sha256) {
|
|
1566
|
-
rowStop('the current task row drifted beyond its status flip: canonical ex-status digest mismatch');
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
} catch (error) {
|
|
1570
|
-
scope(`the task ledger could not be parsed to verify the unrelated-row scope (${error.message})`, 'live.closeout.ledger_rows');
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
if (contextRead && !contextRead.missing) {
|
|
1574
|
-
const boundary = closeoutBoundaryIndex(contextRead.text);
|
|
1575
|
-
if (boundary === -1) {
|
|
1576
|
-
scope('the task context no longer carries a closeout-section boundary', 'live.closeout.context_prefix');
|
|
1577
|
-
} else {
|
|
1578
|
-
const prefixSha = crypto.createHash('sha256')
|
|
1579
|
-
.update(Buffer.from(contextRead.text.slice(0, boundary), 'utf8')).digest('hex');
|
|
1580
|
-
if (prefixSha !== closeout.subject_after.context_prefix_sha256) {
|
|
1581
|
-
scope('the task-context bytes before the closeout sections changed; implementation records, the PPI, and Acceptance stay stale at closeout', 'live.closeout.context_prefix');
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
const evidenceRelative = `.ultra/evidence/${identity}`;
|
|
1586
|
-
try {
|
|
1587
|
-
const observation = stableDirectoryObservation(repoRoot, evidenceRelative, { required: true });
|
|
1588
|
-
const current = [];
|
|
1589
|
-
for (const [name, entry] of observation.entries) {
|
|
1590
|
-
if (name === 'evidence.json') continue;
|
|
1591
|
-
if (entry.stat.isSymbolicLink() || !entry.stat.isFile()) {
|
|
1592
|
-
scope(`the evidence sibling ${name} is not an ordinary regular file`, 'live.closeout.evidence_siblings');
|
|
1593
|
-
continue;
|
|
1594
|
-
}
|
|
1595
|
-
const relative = `${evidenceRelative}/${name}`;
|
|
1596
|
-
const observed = stableShaOr({ root: repoRoot }, relative);
|
|
1597
|
-
if (observed.error || observed.missing) continue;
|
|
1598
|
-
current.push({ path: relative, sha256: observed.sha256 });
|
|
1599
|
-
}
|
|
1600
|
-
current.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1601
|
-
if (JSON.stringify(current) !== JSON.stringify(closeout.subject_after.evidence_siblings || [])) {
|
|
1602
|
-
scope('the evidence directory outside the final evidence record changed; pre-review evidence stays untouched at closeout', 'live.closeout.evidence_siblings');
|
|
1603
|
-
}
|
|
1604
|
-
} catch (error) {
|
|
1605
|
-
scope(`the task evidence directory could not be observed (${error.message})`, 'live.closeout.evidence_siblings');
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
const digestSecond = digestObservation(repoRoot);
|
|
1609
|
-
if (digestSecond.error) {
|
|
1610
|
-
diagnostics.push(diagnostic(
|
|
1611
|
-
'result_binding_unverifiable',
|
|
1612
|
-
`the closed-out subject could not be re-observed coherently: ${digestSecond.error}`,
|
|
1613
|
-
'live.final_worktree_digest',
|
|
1614
|
-
));
|
|
1615
|
-
return live;
|
|
1616
|
-
}
|
|
1617
|
-
if (digestSecond.diff_digest !== digest.diff_digest) {
|
|
1618
|
-
diagnostics.push(diagnostic(
|
|
1619
|
-
'subject_changed_during_observation',
|
|
1620
|
-
'the product worktree digest changed between the first and the closing observation of this validation; let the concurrent change finish, then re-run the validator once',
|
|
1621
|
-
'live.final_worktree_digest',
|
|
1622
|
-
));
|
|
1623
|
-
return live;
|
|
1624
|
-
}
|
|
1625
|
-
live.final_worktree_digest = digestSecond.diff_digest;
|
|
1626
|
-
live.closeout = 'applied';
|
|
1627
|
-
if (digestSecond.diff_digest !== closeout.subject_after.worktree_digest) {
|
|
1628
|
-
diagnostics.push(diagnostic(
|
|
1629
|
-
'closeout_binding',
|
|
1630
|
-
'the recomputed current product worktree digest differs from the closeout end-state digest; the subject moved after the closeout — a typed stop for the owner, never permission for a re-freeze or a new handoff',
|
|
1631
|
-
'live.final_worktree_digest',
|
|
1632
|
-
));
|
|
1633
|
-
}
|
|
1634
|
-
return live;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
function validateHandoffDir(handoffDir, options) {
|
|
1638
|
-
const anchor = handoffRootFor(handoffDir);
|
|
1639
|
-
const handoffId = path.basename(handoffDir);
|
|
1640
|
-
const diagnostics = [];
|
|
1641
|
-
const live = { observed: true };
|
|
1642
|
-
if (!HANDOFF_ID_PATTERN.test(handoffId) || anchor === null) {
|
|
1643
|
-
diagnostics.push(diagnostic(
|
|
1644
|
-
'handoff_id_invalid',
|
|
1645
|
-
'a handoff directory must be .ultra/.runtime/handoffs/<id> with a normalized id',
|
|
1646
|
-
handoffDir,
|
|
1647
|
-
));
|
|
1648
|
-
return {
|
|
1649
|
-
handoff_id: handoffId,
|
|
1650
|
-
state: 'unknown',
|
|
1651
|
-
valid: false,
|
|
1652
|
-
diagnostics,
|
|
1653
|
-
live: null,
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
// Anchor all stable reads on the real repository root so parent-chain walks
|
|
1657
|
-
// never depend on symlinked path components above the repository.
|
|
1658
|
-
let repoRoot = anchor.root;
|
|
1659
|
-
try {
|
|
1660
|
-
repoRoot = fs.realpathSync(anchor.root);
|
|
1661
|
-
} catch {
|
|
1662
|
-
repoRoot = anchor.root;
|
|
1663
|
-
}
|
|
1664
|
-
const handoffRelative = path.relative(anchor.root, handoffDir).split(path.sep).join('/');
|
|
1665
|
-
|
|
1666
|
-
let observation;
|
|
1667
|
-
try {
|
|
1668
|
-
observation = stableDirectoryObservation(repoRoot, handoffRelative, { required: true });
|
|
1669
|
-
} catch (error) {
|
|
1670
|
-
diagnostics.push(diagnostic(error.code, error.message, handoffDir));
|
|
1671
|
-
return { handoff_id: handoffId, state: 'unknown', valid: false, diagnostics, live: null };
|
|
1672
|
-
}
|
|
1673
|
-
const entries = observation.entries;
|
|
1674
|
-
|
|
1675
|
-
// A handoff directory holds exactly OFFER.json, optional ACK.json, optional
|
|
1676
|
-
// RESULT.json, and — only beside a completed v2 RESULT — the optional
|
|
1677
|
-
// CLOSEOUT receipt of the prescribed closeout transition; every other entry
|
|
1678
|
-
// of any type is a typed stop.
|
|
1679
|
-
for (const [name] of entries) {
|
|
1680
|
-
if (RECEIPT_FILES.includes(name)) continue;
|
|
1681
|
-
let kind = 'unknown_receipt_file';
|
|
1682
|
-
if (name.endsWith('.json')) {
|
|
1683
|
-
const extraRead = readJsonFile(repoRoot, `${handoffRelative}/${name}`);
|
|
1684
|
-
const parsed = extraRead.parsed;
|
|
1685
|
-
if (isPlainObject(parsed) && typeof parsed.$schema === 'string' && parsed.$schema.startsWith('ultra-delegation-')) {
|
|
1686
|
-
kind = 'delegation_receipt_in_handoff';
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
diagnostics.push(diagnostic(
|
|
1690
|
-
kind,
|
|
1691
|
-
'a handoff directory holds exactly OFFER.json, optional ACK.json, optional RESULT.json, and — only beside a completed v2 RESULT — optional CLOSEOUT.json; use a fresh handoff id instead of extra receipts',
|
|
1692
|
-
name,
|
|
1693
|
-
));
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
const offerRead = readReceipt(repoRoot, `${handoffRelative}/OFFER.json`, 'OFFER', diagnostics);
|
|
1697
|
-
const ackRead = readReceipt(repoRoot, `${handoffRelative}/ACK.json`, 'ACK', diagnostics);
|
|
1698
|
-
const resultRead = readReceipt(repoRoot, `${handoffRelative}/RESULT.json`, 'RESULT', diagnostics);
|
|
1699
|
-
const closeoutRead = readReceipt(repoRoot, `${handoffRelative}/CLOSEOUT.json`, 'CLOSEOUT', diagnostics);
|
|
1700
|
-
|
|
1701
|
-
const offer = offerRead.payload || null;
|
|
1702
|
-
if (offerRead.read && offer) {
|
|
1703
|
-
validateOffer(handoffId, repoRoot, offer, diagnostics);
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
let ackState = null;
|
|
1707
|
-
if (ackRead.read) {
|
|
1708
|
-
if (offer) {
|
|
1709
|
-
ackState = validateAck(offer, ackRead.payload, handoffId, diagnostics);
|
|
1710
|
-
} else {
|
|
1711
|
-
diagnostics.push(diagnostic('ack_schema', 'an ACK cannot be validated without its OFFER', 'ACK.json'));
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
let terminal = null;
|
|
1716
|
-
if (resultRead.read) {
|
|
1717
|
-
if (offer) {
|
|
1718
|
-
terminal = validateResult(offer, ackState, resultRead.payload, handoffId, repoRoot, diagnostics);
|
|
1719
|
-
} else {
|
|
1720
|
-
diagnostics.push(diagnostic('result_schema', 'a RESULT cannot be validated without its OFFER', 'RESULT.json'));
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
let closeout = null;
|
|
1725
|
-
if (closeoutRead.read) {
|
|
1726
|
-
if (resultRead.read && offer && terminal === 'completed'
|
|
1727
|
-
&& resultRead.payload?.$schema === RESULT_V2_SCHEMA) {
|
|
1728
|
-
closeout = validateCloseout(offer, resultRead.payload, closeoutRead.payload, handoffId, repoRoot, diagnostics);
|
|
1729
|
-
} else {
|
|
1730
|
-
diagnostics.push(diagnostic(
|
|
1731
|
-
'closeout_without_result',
|
|
1732
|
-
'a CLOSEOUT receipt attaches only to a completed v2 RESULT in the same handoff; a fresh handoff or the owner boundary — never an invented receipt — is the other route',
|
|
1733
|
-
'CLOSEOUT.json',
|
|
1734
|
-
));
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
let state = 'offered';
|
|
1739
|
-
if (!offerRead.read) state = 'unknown';
|
|
1740
|
-
if (ackState === 'blocked') state = 'blocked';
|
|
1741
|
-
if (ackState === 'ready') state = 'active';
|
|
1742
|
-
if (TERMINAL_STATES.includes(terminal)) state = terminal;
|
|
1743
|
-
|
|
1744
|
-
const entry = {
|
|
1745
|
-
handoff_id: handoffId,
|
|
1746
|
-
state,
|
|
1747
|
-
valid: false,
|
|
1748
|
-
diagnostics,
|
|
1749
|
-
live: null,
|
|
1750
|
-
time: latestReceiptTime([offerRead.payload, ackRead.payload, resultRead.payload, closeoutRead.payload]),
|
|
1751
|
-
};
|
|
1752
|
-
|
|
1753
|
-
if (options.live) {
|
|
1754
|
-
const superseded = options.superseded !== undefined
|
|
1755
|
-
? options.superseded
|
|
1756
|
-
: supersededByNewerHandoff(repoRoot, handoffId, entry.time, offer);
|
|
1757
|
-
entry.live = liveObservations(
|
|
1758
|
-
offer,
|
|
1759
|
-
ackRead.payload,
|
|
1760
|
-
ackState,
|
|
1761
|
-
terminal,
|
|
1762
|
-
resultRead.payload,
|
|
1763
|
-
repoRoot,
|
|
1764
|
-
diagnostics,
|
|
1765
|
-
live,
|
|
1766
|
-
superseded,
|
|
1767
|
-
closeout,
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
entry.valid = diagnostics.every((item) => item.severity !== 'error');
|
|
1771
|
-
return entry;
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
function readReceipt(repoRoot, relative, label, diagnostics) {
|
|
1775
|
-
const read = readJsonFile(repoRoot, relative);
|
|
1776
|
-
if (read.missing) return { read: false };
|
|
1777
|
-
if (read.readCode) {
|
|
1778
|
-
diagnostics.push(diagnostic(read.readCode, `${label}.json: ${read.readMessage}`, `${label}.json`));
|
|
1779
|
-
return { read: true, payload: null };
|
|
1780
|
-
}
|
|
1781
|
-
if (read.jsonError) {
|
|
1782
|
-
diagnostics.push(diagnostic(`${label.toLowerCase()}_invalid_json`, `${label}.json is not valid JSON: ${read.jsonError}`, `${label}.json`));
|
|
1783
|
-
return { read: true, payload: null };
|
|
1784
|
-
}
|
|
1785
|
-
return { read: true, payload: read.parsed };
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function latestReceiptTime(payloads) {
|
|
1789
|
-
let latest = null;
|
|
1790
|
-
for (const payload of payloads) {
|
|
1791
|
-
const time = receiptTime(payload);
|
|
1792
|
-
if (time !== null && (latest === null || time > latest)) latest = time;
|
|
1793
|
-
}
|
|
1794
|
-
return latest;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// A newer handoff supersedes an older terminal receipt only when it continues
|
|
1798
|
-
// the same transfer subject and authority: the same repository root, the same
|
|
1799
|
-
// accepted task identity, and the same owner-grant decision bytes.
|
|
1800
|
-
function subjectKey(offer) {
|
|
1801
|
-
if (!isPlainObject(offer)) return null;
|
|
1802
|
-
const grants = Array.isArray(offer.frozen_inputs)
|
|
1803
|
-
? offer.frozen_inputs
|
|
1804
|
-
.filter(isPlainObject)
|
|
1805
|
-
.filter((input) => typeof input.path === 'string' && input.path.startsWith('.ultra/decisions/'))
|
|
1806
|
-
.map((input) => `${input.path}:${input.sha256}`)
|
|
1807
|
-
.sort()
|
|
1808
|
-
: [];
|
|
1809
|
-
return JSON.stringify([
|
|
1810
|
-
offer.repository && typeof offer.repository.root === 'string' ? offer.repository.root : null,
|
|
1811
|
-
offer.accepted_scope && typeof offer.accepted_scope.new_task_identity === 'string'
|
|
1812
|
-
? offer.accepted_scope.new_task_identity
|
|
1813
|
-
: null,
|
|
1814
|
-
grants,
|
|
1815
|
-
]);
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
function supersededByNewerHandoff(repoRoot, ownId, ownTime, ownOffer) {
|
|
1819
|
-
const ownKey = subjectKey(ownOffer);
|
|
1820
|
-
if (ownKey === null) return false;
|
|
1821
|
-
let siblings;
|
|
1822
|
-
try {
|
|
1823
|
-
siblings = listHandoffDirs(repoRoot);
|
|
1824
|
-
} catch {
|
|
1825
|
-
return false;
|
|
1826
|
-
}
|
|
1827
|
-
for (const dir of siblings) {
|
|
1828
|
-
if (path.basename(dir) === ownId) continue;
|
|
1829
|
-
const siblingRelative = path.relative(repoRoot, dir).split(path.sep).join('/');
|
|
1830
|
-
const offerRead = readJsonFile(repoRoot, `${siblingRelative}/OFFER.json`);
|
|
1831
|
-
const ackRead = readJsonFile(repoRoot, `${siblingRelative}/ACK.json`);
|
|
1832
|
-
const resultRead = readJsonFile(repoRoot, `${siblingRelative}/RESULT.json`);
|
|
1833
|
-
const other = Math.max(
|
|
1834
|
-
receiptTime(offerRead.parsed),
|
|
1835
|
-
receiptTime(ackRead.parsed),
|
|
1836
|
-
receiptTime(resultRead.parsed),
|
|
1837
|
-
);
|
|
1838
|
-
if (other === null) continue;
|
|
1839
|
-
if (ownTime !== null && other <= ownTime) continue;
|
|
1840
|
-
if (subjectKey(offerRead.parsed) === ownKey) return true;
|
|
1841
|
-
}
|
|
1842
|
-
return false;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
function validateRepo(repoRoot, options) {
|
|
1846
|
-
const diagnostics = [];
|
|
1847
|
-
let handoffs = [];
|
|
1848
|
-
try {
|
|
1849
|
-
handoffs = listHandoffDirs(repoRoot).map((dir) => validateHandoffDir(dir, options));
|
|
1850
|
-
} catch (error) {
|
|
1851
|
-
diagnostics.push(diagnostic(
|
|
1852
|
-
error && error.code ? error.code : 'handoff_scan_limit',
|
|
1853
|
-
`${error && error.message ? error.message : error}`,
|
|
1854
|
-
HANDOFFS_ROOT_RELATIVE,
|
|
1855
|
-
));
|
|
1856
|
-
}
|
|
1857
|
-
const active = handoffs.filter((entry) => entry.state === 'active');
|
|
1858
|
-
if (active.length > 1) {
|
|
1859
|
-
diagnostics.push(diagnostic(
|
|
1860
|
-
'multiple_active_transfers',
|
|
1861
|
-
`more than one ACK-ready handoff is active (${active.map((entry) => entry.handoff_id).join(', ')}); a work package has one canonical primary writer`,
|
|
1862
|
-
'.ultra/.runtime/handoffs',
|
|
1863
|
-
));
|
|
1864
|
-
}
|
|
1865
|
-
return {
|
|
1866
|
-
$schema: REPORT_SCHEMA,
|
|
1867
|
-
path: repoRoot,
|
|
1868
|
-
handoffs,
|
|
1869
|
-
diagnostics,
|
|
1870
|
-
valid: diagnostics.every((item) => item.severity !== 'error')
|
|
1871
|
-
&& handoffs.every((entry) => entry.valid),
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
function usage() {
|
|
1876
|
-
process.stderr.write(
|
|
1877
|
-
'usage: validate_primary_transfer.cjs <handoff-dir | repo-root> [--live]\n'
|
|
1878
|
-
+ 'Validates OFFER/ACK/RESULT primary-transfer receipts structurally.\n'
|
|
1879
|
-
+ '--live additionally observes the active-transfer HEAD and ledger row, whether\n'
|
|
1880
|
-
+ 'the ACK-start worktree digest still holds, and — for the newest v2 terminal\n'
|
|
1881
|
-
+ 'RESULT — the recomputed final HEAD, worktree digest, exact product path\n'
|
|
1882
|
-
+ 'inventory, and final frozen-input digests. A CLOSEOUT receipt beside a\n'
|
|
1883
|
-
+ 'completed v2 RESULT binds the one prescribed post-review closeout instead:\n'
|
|
1884
|
-
+ 'the closeout end-state digest, the pinned reviewed subject, and the frozen\n'
|
|
1885
|
-
+ 'ledger-row, context-prefix, and pre-review-evidence scopes.\n',
|
|
1886
|
-
);
|
|
1887
|
-
process.exitCode = 2;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
function main(argv) {
|
|
1891
|
-
const live = argv.includes('--live');
|
|
1892
|
-
const targets = argv.filter((arg) => arg !== '--live');
|
|
1893
|
-
if (targets.length !== 1 || !targets[0]) {
|
|
1894
|
-
usage();
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
const target = path.resolve(targets[0]);
|
|
1898
|
-
if (fs.existsSync(target) && fs.statSync(target).isFile()) {
|
|
1899
|
-
usage();
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
const anchor = fs.existsSync(path.join(target, 'OFFER.json'))
|
|
1903
|
-
|| (path.basename(target) !== 'handoffs'
|
|
1904
|
-
&& path.basename(path.dirname(target)) === 'handoffs');
|
|
1905
|
-
let report;
|
|
1906
|
-
if (anchor) {
|
|
1907
|
-
const single = validateHandoffDir(target, { live });
|
|
1908
|
-
report = {
|
|
1909
|
-
$schema: REPORT_SCHEMA,
|
|
1910
|
-
path: target,
|
|
1911
|
-
handoffs: [single],
|
|
1912
|
-
diagnostics: [],
|
|
1913
|
-
valid: single.valid,
|
|
1914
|
-
};
|
|
1915
|
-
} else {
|
|
1916
|
-
report = validateRepo(target, { live });
|
|
1917
|
-
}
|
|
1918
|
-
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
1919
|
-
if (!report.valid) process.exitCode = 1;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
if (require.main === module) main(process.argv.slice(2));
|
|
1923
|
-
|
|
1924
|
-
module.exports = {
|
|
1925
|
-
TERMINAL_STATES,
|
|
1926
|
-
validateHandoffDir,
|
|
1927
|
-
validateRepo,
|
|
1928
|
-
};
|