chati-dev 4.5.28 → 4.5.29

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.
Files changed (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -257,12 +257,60 @@ function extractArchiveIntoPinnedSubject(archivePath, subjectDir) {
257
257
  }
258
258
  }
259
259
 
260
- /** Materialize exactly one immutable Git commit without linking its .git dir. */
261
- export function createRailReadOnlyCommitWorkspace({ projectDir, baseCommitRef, resultCommitRef } = {}) {
260
+ function archiveVerificationCommit({ projectDir, root, resultCommitRef, archivePath }) {
261
+ // A separate empty metadata repository prevents local/info/global attributes,
262
+ // export-ignore and export-subst from changing the committed verification
263
+ // subject. Only the object database is read, never checked-out metadata.
264
+ const metadata = join(root, 'export.git');
265
+ const attributes = join(root, 'empty-attributes');
266
+ mkdirSync(attributes, { mode: 0o700 });
267
+ const environment = trustedGitEnvironment();
268
+ execFileSync('git', ['init', '--quiet', '--bare', '--template=',
269
+ `--object-format=${resultCommitRef.length === 64 ? 'sha256' : 'sha1'}`, metadata], { env: environment, stdio: 'pipe' });
270
+ const objects = git(projectDir, ['rev-parse', '--git-path', 'objects'], { encoding: 'utf8' }).trim();
271
+ execFileSync('git', ['--git-dir', metadata, '--work-tree', attributes,
272
+ '-c', 'core.attributesFile=/dev/null', 'archive', '--worktree-attributes',
273
+ '--format=tar', '--output', archivePath, resultCommitRef], {
274
+ env: { ...environment, GIT_OBJECT_DIRECTORY: realpathSync(resolve(projectDir, objects)) }, stdio: 'pipe',
275
+ });
276
+ }
277
+
278
+ export const RAIL_READ_ONLY_WORKSPACE_PREFIX = 'chati-rail-review-subject-';
279
+ export const RETAINED_WORKSPACE_SWEEP_AFTER_MS = 24 * 60 * 60 * 1000;
280
+
281
+ /** Darwin retains finished review/verification workspaces because scoped group
282
+ * completion cannot prove that every detached reader died. Without a reaper
283
+ * they accumulate on every ordinary run. Reclaim retained roots that are older
284
+ * than a generous bound: any reader still alive that long is not a legitimate
285
+ * review process. Best effort, never throws, never touches recent roots. */
286
+ export function sweepRetainedReadOnlyWorkspaces({
287
+ directory = tmpdir(), prefixes = [RAIL_READ_ONLY_WORKSPACE_PREFIX],
288
+ olderThanMs = RETAINED_WORKSPACE_SWEEP_AFTER_MS, now = Date.now(),
289
+ } = {}) {
290
+ const removed = [];
291
+ let entries;
292
+ try { entries = readdirSync(directory); } catch { return removed; }
293
+ for (const name of entries) {
294
+ if (!prefixes.some((prefix) => name.startsWith(prefix))) continue;
295
+ const path = join(directory, name);
296
+ try {
297
+ const stat = lstatSync(path);
298
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.()) continue;
299
+ if (now - stat.mtimeMs < olderThanMs) continue;
300
+ rmSync(path, { recursive: true, force: true });
301
+ removed.push(path);
302
+ } catch { /* best effort: a concurrent owner or a permission boundary keeps it */ }
303
+ }
304
+ return removed;
305
+ }
306
+
307
+ /** Materialize one immutable commit without linking its .git dir to the child. */
308
+ export function createRailReadOnlyCommitWorkspace({ projectDir, baseCommitRef, resultCommitRef, verificationSubject = false } = {}) {
262
309
  const canonicalProject = realpathSync(resolve(projectDir));
263
310
  const resolvedBase = resolveExactCommit(canonicalProject, baseCommitRef, 'RAIL_REVIEW_BASE_COMMIT_INVALID', 'base');
264
311
  const resolvedCommit = resolveExactCommit(canonicalProject, resultCommitRef, 'RAIL_REVIEW_RESULT_COMMIT_INVALID', 'result');
265
- const root = mkdtempSync(join(tmpdir(), 'chati-rail-review-subject-'));
312
+ if (process.platform === 'darwin') sweepRetainedReadOnlyWorkspaces();
313
+ const root = mkdtempSync(join(tmpdir(), RAIL_READ_ONLY_WORKSPACE_PREFIX));
266
314
  const rootStat = lstatSync(root);
267
315
  const rootIdentity = {
268
316
  dev: rootStat.dev,
@@ -275,10 +323,11 @@ export function createRailReadOnlyCommitWorkspace({ projectDir, baseCommitRef, r
275
323
  try {
276
324
  mkdirSync(subjectDir, { mode: 0o700 });
277
325
  runReadOnlyWorkspaceTestHook('afterSubjectDirectoryCreate', { root, subjectDir });
278
- git(canonicalProject, ['archive', '--format=tar', '--output', archivePath, resultCommitRef], { stdio: 'ignore' });
326
+ if (verificationSubject) archiveVerificationCommit({ projectDir: canonicalProject, root, resultCommitRef, archivePath });
327
+ else git(canonicalProject, ['archive', '--format=tar', '--output', archivePath, resultCommitRef], { stdio: 'ignore' });
279
328
  extractArchiveIntoPinnedSubject(archivePath, subjectDir);
280
329
  rmSync(archivePath);
281
- materializeReviewDelta({
330
+ if (!verificationSubject) materializeReviewDelta({
282
331
  projectDir: canonicalProject, baseCommitRef: resolvedBase, resultCommitRef: resolvedCommit, subjectDir,
283
332
  });
284
333
  const baseline = captureCommitSubject(subjectDir, resolvedCommit, resolvedBase);
@@ -0,0 +1,315 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
3
+ import { basename, join, parse, resolve, sep } from 'node:path';
4
+ import { canonicalize, canonicalSerialize, sha256, openAtSync, lstatAtSync, readDirectorySync, readlinkAtSync } from '@chati/core';
5
+ import { persistImmutableInstallationFile, readSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
6
+ import { signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
7
+ import { railRepositoryIdentity, readRailGitState as git } from './rail-execution-worktree.js';
8
+ import { verifyTerminalAuthorityReceipt } from './spawner.js';
9
+
10
+ const DOMAIN = 'chati.rail-worktree-checkpoint.v1';
11
+ const MAX_BYTES = 128 * 1024 * 1024;
12
+ const MAX_ENTRIES = 10_000;
13
+ const MANIFEST_BYTES = 4 * 1024 * 1024;
14
+ const O_DIRECTORY = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
15
+ const HASH = /^[a-f0-9]{64}$/;
16
+ const OID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
17
+ const hooks = Symbol.for('chati.checkpoint.security-test-hooks');
18
+ const rawHash = (bytes) => createHash('sha256').update(bytes).digest('hex');
19
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
20
+ function hook(name, detail) { globalThis[hooks]?.[name]?.(detail); }
21
+ function decode(bytes) {
22
+ try { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
23
+ catch { return fail('CHECKPOINT_PATH_ENCODING_UNSUPPORTED', 'checkpoint paths must be losslessly representable as UTF-8'); }
24
+ }
25
+ function excluded(path) { return ['.chati', '.chati.dev'].includes(path.split('/')[0]); }
26
+ function components(path) {
27
+ const parts = path.split('/');
28
+ if (!path || parts.some((part) => !part || ['.', '..', '.git'].includes(part) || /[\\\0]/.test(part) || Buffer.byteLength(part) > 255)) {
29
+ fail('CHECKPOINT_UNSAFE_PATH', 'checkpoint path must stay inside the worktree without Git control entries');
30
+ }
31
+ return parts;
32
+ }
33
+ function sameSnapshot(a, b) {
34
+ return ['dev', 'ino', 'mode', 'nlink', 'size', 'mtimeNs', 'ctimeNs'].every((key) => a[key] === b[key]);
35
+ }
36
+ function openDirectory(path) {
37
+ const absolute = resolve(path);
38
+ const root = parse(absolute).root;
39
+ let fd = openSync(root, O_DIRECTORY);
40
+ try {
41
+ for (const part of absolute.slice(root.length).split(sep).filter(Boolean)) {
42
+ const next = openAtSync(fd, part, O_DIRECTORY);
43
+ closeSync(fd); fd = next;
44
+ }
45
+ return fd;
46
+ } catch (error) { closeSync(fd); throw error; }
47
+ }
48
+ function readFileAt(fd, name, { allowMissing = false } = {}) {
49
+ let source;
50
+ try { source = openAtSync(fd, name, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW); }
51
+ catch (error) { if (allowMissing && error.code === 'ENOENT') return null; throw error; }
52
+ try {
53
+ const before = fstatSync(source, { bigint: true });
54
+ if (!before.isFile() || before.nlink !== 1n) fail('CHECKPOINT_UNSAFE_FILE', 'checkpoint input must be a regular single-link file');
55
+ if (before.size > BigInt(MAX_BYTES)) fail('CHECKPOINT_SIZE_LIMIT', 'checkpoint input exceeds 128 MiB');
56
+ const chunks = []; let total = 0;
57
+ while (total <= MAX_BYTES) {
58
+ const bytes = Buffer.allocUnsafe(Math.min(65536, MAX_BYTES + 1 - total));
59
+ const count = readSync(source, bytes, 0, bytes.length, null);
60
+ if (!count) break;
61
+ chunks.push(bytes.subarray(0, count)); total += count;
62
+ }
63
+ if (total > MAX_BYTES) fail('CHECKPOINT_SIZE_LIMIT', 'checkpoint input grew beyond 128 MiB');
64
+ if (!sameSnapshot(before, fstatSync(source, { bigint: true }))) fail('CHECKPOINT_SOURCE_CHANGED', 'file changed during checkpoint capture');
65
+ return { bytes: Buffer.concat(chunks, total), mode: Number(before.mode & 0o777n) };
66
+ } finally { closeSync(source); }
67
+ }
68
+ function readGitFile(repository, path, options) {
69
+ const parent = resolve(path, '..');
70
+ if (![repository.git_dir.path, repository.common_git_dir.path].includes(parent)) {
71
+ fail('CHECKPOINT_GIT_STORAGE_MISMATCH', 'Git metadata must belong to the signed repository storage');
72
+ }
73
+ const fd = openDirectory(parent);
74
+ try { return readFileAt(fd, basename(path), options); } finally { closeSync(fd); }
75
+ }
76
+ function nulEntries(bytes) {
77
+ const value = decode(bytes);
78
+ if (value && !value.endsWith('\0')) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'Git inventory must be NUL terminated');
79
+ return value ? value.slice(0, -1).split('\0') : [];
80
+ }
81
+ function checkpointId(dispatch) {
82
+ return sha256({ attempt_id: dispatch.payload.attempt_id, dispatch_event_id: dispatch.event_id,
83
+ launch_nonce: dispatch.payload.invocation.launch_nonce });
84
+ }
85
+ function storage(projectDir, id) {
86
+ if (!HASH.test(id)) fail('CHECKPOINT_REFERENCE_INVALID', 'checkpoint ID must be a SHA-256 digest');
87
+ return join(resolve(projectDir), '.chati/v2/rail/worktree-checkpoints', id);
88
+ }
89
+ function blobReader(directory) {
90
+ return (ref) => {
91
+ if (!ref || !HASH.test(ref.sha256) || !Number.isSafeInteger(ref.size) || ref.size < 0 || ref.size > MAX_BYTES) {
92
+ fail('CHECKPOINT_BLOB_INVALID', 'checkpoint blob reference is invalid');
93
+ }
94
+ const bytes = readSafeInstallationStorageFile(join(directory, 'blobs', ref.sha256), { encoding: null, maxBytes: MAX_BYTES }).content;
95
+ if (bytes.length !== ref.size || rawHash(bytes) !== ref.sha256) fail('CHECKPOINT_BLOB_MISMATCH', 'checkpoint blob is missing or changed');
96
+ return bytes;
97
+ };
98
+ }
99
+
100
+ function checkpointConfiguration(projectDir) {
101
+ // Inspect the same effective configuration Git loads, including includes and
102
+ // enabled worktree config. Do not disclose values in receipts or diagnostics.
103
+ const bytes = git(projectDir, ['config', '--null', '--list', '--includes'], { encoding: null });
104
+ if (bytes.length && bytes.at(-1) !== 0) fail('CHECKPOINT_GIT_CONFIG_INVALID', 'Git configuration inventory is incomplete');
105
+ let offset = 0;
106
+ while (offset < bytes.length) {
107
+ const end = bytes.indexOf(0, offset);
108
+ const entry = bytes.subarray(offset, end);
109
+ const newline = entry.indexOf(10);
110
+ const key = entry.subarray(0, newline < 0 ? entry.length : newline).toString('latin1');
111
+ if (/^(?:extensions\.partialclone|remote\..*\.(?:promisor|partialclonefilter))$/i.test(key)) {
112
+ fail('CHECKPOINT_PARTIAL_CLONE_UNSUPPORTED', 'checkpoint does not support partial-clone/promisor configuration; Git and worktree were not changed');
113
+ }
114
+ offset = end + 1;
115
+ }
116
+ return rawHash(bytes);
117
+ }
118
+
119
+ function captureState(projectDir, repository, base, blob, configurationDigest) {
120
+ if (checkpointConfiguration(projectDir) !== configurationDigest) fail('CHECKPOINT_SOURCE_CHANGED', 'Git configuration changed before capture');
121
+ const head = git(projectDir, ['rev-parse', '--verify', 'HEAD^{commit}']).trim();
122
+ if (!OID.test(head)) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'checkpoint requires an exact partial HEAD');
123
+ const headFile = readGitFile(repository, join(repository.git_dir.path, 'HEAD'));
124
+ const rawHead = decode(headFile.bytes).trim();
125
+ const headRef = rawHead.startsWith('ref: ') ? rawHead.slice(5) : null;
126
+ if ((headRef !== null && !/^refs\/[^\s\0]+$/.test(headRef)) || (headRef === null && rawHead !== head)) {
127
+ fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'HEAD must have a recoverable symbolic or detached identity');
128
+ }
129
+ const indexPath = resolve(projectDir, git(projectDir, ['rev-parse', '--git-path', 'index']).trim());
130
+ const index = readGitFile(repository, indexPath, { allowMissing: true });
131
+ const sharedPath = git(projectDir, ['rev-parse', '--shared-index-path']).trim();
132
+ let shared = null;
133
+ if (sharedPath) {
134
+ if (!/^sharedindex\.(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(basename(sharedPath))) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'split index dependency is invalid');
135
+ shared = { name: basename(sharedPath), blob: blob(readGitFile(repository, resolve(projectDir, sharedPath)).bytes) };
136
+ }
137
+ const shallowPath = resolve(projectDir, git(projectDir, ['rev-parse', '--git-path', 'shallow']).trim());
138
+ const shallow = readGitFile(repository, shallowPath, { allowMissing: true });
139
+ const stageBytes = git(projectDir, ['ls-files', '--stage', '-z'], { encoding: null });
140
+ const paths = new Set(); const objectIds = new Set([base, head]);
141
+ const addPath = (path) => { if (!excluded(path)) { components(path); paths.add(path); } };
142
+ for (const entry of nulEntries(stageBytes)) {
143
+ const match = /^(\d{6}) ([a-f0-9]{40,64}) [0-3]\t([\s\S]+)$/.exec(entry);
144
+ if (!match || !OID.test(match[2])) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'Git index inventory is malformed');
145
+ if (match[1] === '160000') fail('CHECKPOINT_GITLINK_UNSUPPORTED', 'submodule storage requires an explicit recursive checkpoint before restoration');
146
+ if (!/^0+$/.test(match[2])) objectIds.add(match[2]);
147
+ addPath(match[3]);
148
+ }
149
+ for (const commit of [base, head]) {
150
+ for (const entry of nulEntries(git(projectDir, ['ls-tree', '-r', '-z', '--full-tree', commit], { encoding: null }))) {
151
+ const match = /^(\d{6}) (?:blob|commit) [a-f0-9]{40,64}\t([\s\S]+)$/.exec(entry);
152
+ if (!match) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'Git tree inventory is malformed');
153
+ if (match[1] === '160000') fail('CHECKPOINT_GITLINK_UNSUPPORTED', 'submodule storage requires an explicit recursive checkpoint before restoration');
154
+ addPath(match[2]);
155
+ }
156
+ }
157
+ for (const path of nulEntries(git(projectDir, ['ls-files', '--others', '--exclude-standard', '-z'], { encoding: null }))) addPath(path);
158
+ // Include cache-tree and resolve-undo dependencies, not only visible staged
159
+ // blobs. Restrict enumeration to this index, never other linked worktrees.
160
+ const reachable = git(projectDir, ['rev-list', '--objects', '--no-object-names', '--single-worktree', '--indexed-objects', base, head]);
161
+ for (const oid of reachable.trim().split('\n').filter(Boolean)) {
162
+ if (!OID.test(oid)) fail('CHECKPOINT_GIT_OUTPUT_INVALID', 'Git object closure is malformed');
163
+ objectIds.add(oid);
164
+ }
165
+ if (paths.size > MAX_ENTRIES) fail('CHECKPOINT_ENTRY_LIMIT', 'checkpoint exceeds 10,000 paths');
166
+ const trie = { children: new Map(), full: false };
167
+ for (const path of paths) {
168
+ let node = trie;
169
+ for (const part of components(path)) {
170
+ if (!node.children.has(part)) node.children.set(part, { children: new Map(), full: false });
171
+ node = node.children.get(part);
172
+ }
173
+ node.full = true;
174
+ }
175
+ const files = [];
176
+ const walk = (parent, name, path, node, inheritedFull = false) => {
177
+ if (files.length >= MAX_ENTRIES) fail('CHECKPOINT_ENTRY_LIMIT', 'checkpoint exceeds 10,000 entries');
178
+ components(path);
179
+ let before;
180
+ try { before = lstatAtSync(parent, name, { bigint: true }); }
181
+ catch (error) {
182
+ if (error.code !== 'ENOENT') throw error;
183
+ files.push({ path, type: 'absent' }); return;
184
+ }
185
+ if (before.isSymbolicLink()) {
186
+ const bytes = readlinkAtSync(parent, name);
187
+ if (!sameSnapshot(before, lstatAtSync(parent, name, { bigint: true }))) fail('CHECKPOINT_SOURCE_CHANGED', 'symlink changed while captured');
188
+ files.push({ path, type: 'symlink', blob: blob(bytes) });
189
+ } else if (before.isFile()) {
190
+ const content = readFileAt(parent, name);
191
+ if (!sameSnapshot(before, lstatAtSync(parent, name, { bigint: true }))) fail('CHECKPOINT_SOURCE_CHANGED', 'file path changed while captured');
192
+ files.push({ path, type: 'file', mode: content.mode, blob: blob(content.bytes) });
193
+ } else if (before.isDirectory()) {
194
+ const fd = openAtSync(parent, name, O_DIRECTORY);
195
+ try {
196
+ const pinned = fstatSync(fd, { bigint: true });
197
+ if (pinned.dev !== before.dev || pinned.ino !== before.ino) fail('CHECKPOINT_SOURCE_CHANGED', 'directory changed before capture');
198
+ const full = inheritedFull || node.full;
199
+ const names = full ? readDirectorySync(fd, { encoding: 'buffer' }).map(decode).sort() : [...node.children.keys()].sort();
200
+ files.push({ path, type: 'directory', mode: Number(before.mode & 0o777n), complete: full });
201
+ for (const child of names) walk(fd, child, `${path}/${child}`, node.children.get(child) ?? { children: new Map(), full: true }, full);
202
+ if (!sameSnapshot(before, fstatSync(fd, { bigint: true }))
203
+ || !sameSnapshot(before, lstatAtSync(parent, name, { bigint: true }))) fail('CHECKPOINT_SOURCE_CHANGED', 'directory changed while captured');
204
+ } finally { closeSync(fd); }
205
+ } else fail('CHECKPOINT_UNSAFE_FILE', 'special files cannot be silently omitted from a checkpoint');
206
+ };
207
+ const rootFd = openDirectory(projectDir);
208
+ try { for (const name of [...trie.children.keys()].sort()) walk(rootFd, name, name, trie.children.get(name)); }
209
+ finally { closeSync(rootFd); }
210
+ if (sha256(railRepositoryIdentity(projectDir)) !== sha256(repository)) fail('CHECKPOINT_REPOSITORY_CHANGED', 'repository identity changed during capture');
211
+ if (checkpointConfiguration(projectDir) !== configurationDigest) fail('CHECKPOINT_SOURCE_CHANGED', 'Git configuration changed during capture');
212
+ return canonicalize({ git: { head, head_ref: headRef, head_file: blob(headFile.bytes), index: index ? blob(index.bytes) : null,
213
+ shared_index: shared, shallow: shallow ? blob(shallow.bytes) : null, index_listing: blob(stageBytes),
214
+ configuration_digest: configurationDigest,
215
+ object_format: git(projectDir, ['rev-parse', '--show-object-format']).trim(), object_ids: [...objectIds].sort() }, files });
216
+ }
217
+
218
+ export function readRailWorktreeCheckpoint({ projectDir, receipt, expected = {} }) {
219
+ const { authority_id, authority_signature, ...payload } = receipt ?? {};
220
+ if (payload.schema_version !== 1 || payload.purpose !== 'rail_execution_checkpoint'
221
+ || !verifyHostAuthorityPayload(payload, { domain: DOMAIN, authorityId: authority_id, signature: authority_signature })
222
+ || payload.project_dir !== resolve(projectDir)
223
+ || Object.entries(expected).some(([key, value]) => sha256(payload[key] ?? null) !== sha256(value))) {
224
+ fail('CHECKPOINT_AUTHORITY_MISMATCH', 'checkpoint receipt must authenticate the exact canonical execution');
225
+ }
226
+ const directory = storage(projectDir, payload.checkpoint_id);
227
+ const bytes = readSafeInstallationStorageFile(join(directory, 'manifest.json'), { encoding: null, maxBytes: MANIFEST_BYTES }).content;
228
+ if (rawHash(bytes) !== payload.manifest_sha256) fail('CHECKPOINT_MANIFEST_MISMATCH', 'checkpoint manifest changed');
229
+ const manifest = JSON.parse(decode(bytes));
230
+ if (!manifest.state || !Array.isArray(manifest.state.files) || manifest.state.files.length > MAX_ENTRIES
231
+ || sha256(manifest.binding) !== sha256(payload.binding) || sha256(manifest.state) !== payload.source_state_digest) {
232
+ fail('CHECKPOINT_MANIFEST_MISMATCH', 'checkpoint state or binding does not match its receipt');
233
+ }
234
+ const readBlob = blobReader(directory);
235
+ const refs = [manifest.pack, manifest.state.git.head_file, manifest.state.git.index_listing,
236
+ manifest.state.git.index, manifest.state.git.shared_index?.blob, manifest.state.git.shallow,
237
+ ...manifest.state.files.map((file) => file.blob)].filter(Boolean);
238
+ const seen = new Set(); let size = 0;
239
+ for (const ref of refs) {
240
+ if (seen.has(ref.sha256)) continue;
241
+ seen.add(ref.sha256); size += ref.size;
242
+ if (size > MAX_BYTES) fail('CHECKPOINT_SIZE_LIMIT', 'checkpoint exceeds 128 MiB');
243
+ readBlob(ref);
244
+ }
245
+ return Object.freeze({ receipt, manifest: Object.freeze(manifest), readBlob });
246
+ }
247
+
248
+ /** Capture only. No reset, clean, stash, index write, ref write or provider launch. */
249
+ export function captureRailWorktreeCheckpoint({ projectDir, dispatch, interruption }) {
250
+ const launch = dispatch?.payload?.launch_receipt;
251
+ const nonce = dispatch?.payload?.invocation?.launch_nonce;
252
+ if (!verifyTerminalAuthorityReceipt(launch, { purpose: 'terminal_launch', authority_kind: 'rail_execution',
253
+ working_dir: resolve(projectDir), launch_nonce: nonce })
254
+ || interruption?.payload?.dispatch_event_id !== dispatch.event_id
255
+ || interruption.payload.launch_receipt_digest !== sha256(launch)
256
+ || interruption.payload.process?.status !== 'exited') {
257
+ fail('CHECKPOINT_DISPATCH_MISMATCH', 'checkpoint requires the canonical launch and its observed interruption');
258
+ }
259
+ // Must precede every object-resolution command. Transport denial alone is
260
+ // insufficient on old Git: lazy fetch can modify config before it is denied.
261
+ const configurationDigest = checkpointConfiguration(projectDir);
262
+ const repository = railRepositoryIdentity(projectDir);
263
+ if (sha256(repository) !== launch.execution_repository_identity_digest) fail('CHECKPOINT_REPOSITORY_CHANGED', 'repository differs from its signed launch identity');
264
+ const binding = canonicalize({ attempt_id: dispatch.payload.attempt_id, dispatch_event_id: dispatch.event_id,
265
+ interruption_event_id: interruption.event_id, launch_nonce: nonce, claim_id: dispatch.payload.claim_id,
266
+ launch_receipt_digest: sha256(launch), base_commit_ref: launch.execution_base_commit_ref,
267
+ repository_identity_digest: sha256(repository) });
268
+ const id = checkpointId(dispatch); const directory = storage(projectDir, id);
269
+ const existing = readSafeInstallationStorageFile(join(directory, 'receipt.json'), { allowMissing: true });
270
+ if (existing) return readRailWorktreeCheckpoint({ projectDir, receipt: JSON.parse(existing.content), expected: { binding } }).receipt;
271
+ const stored = new Map(); let total = 0;
272
+ const blob = (bytes, persist = true) => {
273
+ const digest = rawHash(bytes);
274
+ if (!stored.has(digest)) {
275
+ total += bytes.length;
276
+ if (total > MAX_BYTES) fail('CHECKPOINT_SIZE_LIMIT', 'checkpoint exceeds its 128 MiB total byte budget');
277
+ stored.set(digest, bytes.length);
278
+ if (persist) persistImmutableInstallationFile(join(directory, 'blobs', digest), bytes, { maxBytes: MAX_BYTES, label: 'worktree checkpoint blob' });
279
+ }
280
+ return { sha256: digest, size: bytes.length };
281
+ };
282
+ const state = captureState(projectDir, repository, binding.base_commit_ref, blob, configurationDigest);
283
+ const pack = blob(git(projectDir, ['pack-objects', '--stdout', '--threads=1', '--window=0', '--no-reuse-delta', '--no-reuse-object'], {
284
+ encoding: null, input: `${state.git.object_ids.join('\n')}\n`,
285
+ }));
286
+ hook('afterCheckpointCapture', { projectDir });
287
+ const after = captureState(projectDir, repository, binding.base_commit_ref, (bytes) => ({ sha256: rawHash(bytes), size: bytes.length }), configurationDigest);
288
+ if (sha256(state) !== sha256(after)) fail('CHECKPOINT_SOURCE_CHANGED', 'worktree or index changed before checkpoint commit');
289
+ const manifest = canonicalize({ schema_version: 1, binding, state, pack });
290
+ const bytes = Buffer.from(canonicalSerialize(manifest));
291
+ const material = { schema_version: 1, purpose: 'rail_execution_checkpoint', project_dir: resolve(projectDir),
292
+ checkpoint_id: id, binding, manifest_sha256: rawHash(bytes), source_state_digest: sha256(state) };
293
+ const signed = signHostAuthorityPayload(material, { domain: DOMAIN });
294
+ const receipt = canonicalize({ ...material, authority_id: signed.authority_id, authority_signature: signed.signature });
295
+ persistImmutableInstallationFile(join(directory, 'manifest.json'), bytes, { maxBytes: MANIFEST_BYTES, label: 'worktree checkpoint manifest' });
296
+ readRailWorktreeCheckpoint({ projectDir, receipt, expected: { binding } });
297
+ persistImmutableInstallationFile(join(directory, 'receipt.json'), canonicalSerialize(receipt));
298
+ return Object.freeze(receipt);
299
+ }
300
+
301
+ /** Used only by the host restoration adapter, before committing its intent. */
302
+ export function assertRailCheckpointSourceUnchanged(projectDir, checkpoint) {
303
+ const repository = railRepositoryIdentity(projectDir);
304
+ const binding = checkpoint.manifest.binding;
305
+ if (sha256(repository) !== binding.repository_identity_digest) fail('CHECKPOINT_REPOSITORY_CHANGED', 'checkpoint repository identity changed');
306
+ const configuration = checkpointConfiguration(projectDir);
307
+ const state = captureState(projectDir, repository, binding.base_commit_ref,
308
+ (bytes) => ({ sha256: rawHash(bytes), size: bytes.length }), configuration);
309
+ if (sha256(state) !== checkpoint.receipt.source_state_digest) fail('CHECKPOINT_SOURCE_CHANGED', 'current source differs from the original checkpoint');
310
+ return repository;
311
+ }
312
+
313
+ // Internal shared I/O, not a new public recovery entrypoint.
314
+ export { openDirectory as openCheckpointDirectory, readFileAt as readCheckpointFileAt,
315
+ checkpointConfiguration as readCheckpointConfiguration, components as checkpointPathComponents };