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
@@ -0,0 +1,485 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+ import { canonicalSerialize, canonicalize, sha256, lstatAtSync, mkdirAtSync, openAtSync,
6
+ readDirectorySync, readlinkAtSync, renameNoReplaceAtSync, symlinkAtSync } from '@chati/core';
7
+ import { tryExclusiveFileLockSync, unlinkAtSync } from '@chati/core';
8
+ import { persistImmutableInstallationFile, readSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
9
+ import { signHostAuthorityPayload, verifyHostAuthorityPayload } from '../installer-v2/installation-authority.js';
10
+ import { readRailGitState as git, railRepositoryIdentity, railRepositoryStorageIdentity, captureRailExecutionWorktree } from './rail-execution-worktree.js';
11
+ import { readRailWorktreeCheckpoint, assertRailCheckpointSourceUnchanged, openCheckpointDirectory,
12
+ readCheckpointFileAt, readCheckpointConfiguration, checkpointPathComponents } from './rail-worktree-checkpoint.js';
13
+
14
+ const DOMAIN = 'chati.rail-worktree-restoration.v1';
15
+ const MAX_BYTES = 128 * 1024 * 1024;
16
+ const MAX_PLAN_BYTES = 16 * 1024 * 1024;
17
+ const O_DIRECTORY = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
18
+ const ABSENT = Object.freeze({ type: 'absent' });
19
+ const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
20
+ const ref = (bytes) => ({ sha256: hash(bytes), size: bytes.length });
21
+ const hooks = Symbol.for('chati.restoration.security-test-hooks');
22
+ const hook = (name, detail) => globalThis[hooks]?.[name]?.(detail);
23
+ function fail(code, message, details) { throw Object.assign(new Error(message), { code, details }); }
24
+ function decode(bytes) { return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
25
+ function excluded(path) { return ['.chati', '.chati.dev'].includes(path.split('/')[0]); }
26
+ function signed(payload) {
27
+ const result = signHostAuthorityPayload(payload, { domain: DOMAIN });
28
+ return canonicalize({ ...payload, authority_id: result.authority_id, authority_signature: result.signature });
29
+ }
30
+ function verify(value, expected) {
31
+ const { authority_id, authority_signature, ...payload } = value ?? {};
32
+ if (!verifyHostAuthorityPayload(payload, { domain: DOMAIN, authorityId: authority_id, signature: authority_signature })
33
+ || Object.entries(expected).some(([key, item]) => sha256(payload[key] ?? null) !== sha256(item))) {
34
+ fail('RESTORATION_AUTHORITY_MISMATCH', 'restoration evidence must authenticate this exact checkpoint and step');
35
+ }
36
+ return payload;
37
+ }
38
+ function baseDirectory(projectDir, checkpoint) {
39
+ return join(resolve(projectDir), '.chati/v2/rail/worktree-checkpoints', checkpoint.receipt.checkpoint_id, 'restoration');
40
+ }
41
+ function comparable(state) {
42
+ const value = { ...state }; delete value.complete;
43
+ return value;
44
+ }
45
+ const same = (left, right) => sha256(comparable(left)) === sha256(comparable(right));
46
+ function entryKey(scope, path) { checkpointPathComponents(path); return `${scope}:${path}`; }
47
+ function entryPath(roots, entry) {
48
+ if (!Object.hasOwn(roots, entry.scope)) fail('RESTORATION_PATH_INVALID', 'unknown restoration storage scope');
49
+ checkpointPathComponents(entry.path);
50
+ if (entry.scope === 'worktree' && excluded(entry.path)) fail('RESTORATION_PATH_INVALID', 'restoration cannot modify the CHATI control plane');
51
+ return join(roots[entry.scope], entry.path);
52
+ }
53
+ function openParent(roots, entry, { allowMissing = false } = {}) {
54
+ entryPath(roots, entry);
55
+ let fd = openCheckpointDirectory(roots[entry.scope]);
56
+ const parts = checkpointPathComponents(entry.path); const name = parts.pop();
57
+ try {
58
+ for (const part of parts) {
59
+ let next;
60
+ try { next = openAtSync(fd, part, O_DIRECTORY); }
61
+ catch (error) {
62
+ if (allowMissing && ['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code)) { closeSync(fd); return null; }
63
+ throw error;
64
+ }
65
+ closeSync(fd); fd = next;
66
+ }
67
+ return { fd, name };
68
+ } catch (error) { closeSync(fd); throw error; }
69
+ }
70
+ function readEntryAt(fd, name) {
71
+ let stat;
72
+ try { stat = lstatAtSync(fd, name); }
73
+ catch (error) { if (error.code === 'ENOENT') return ABSENT; throw error; }
74
+ if (stat.isSymbolicLink()) return { type: 'symlink', blob: ref(readlinkAtSync(fd, name)) };
75
+ if (stat.isDirectory()) return { type: 'directory', mode: stat.mode & 0o777 };
76
+ if (stat.isFile()) {
77
+ const saved = readCheckpointFileAt(fd, name);
78
+ return { type: 'file', mode: saved.mode, blob: ref(saved.bytes) };
79
+ }
80
+ return fail('RESTORATION_UNSAFE_FILE', 'special entries cannot be overwritten during restoration');
81
+ }
82
+ function readEntry(roots, entry) {
83
+ const parent = openParent(roots, entry, { allowMissing: true });
84
+ if (!parent) return ABSENT;
85
+ try { return readEntryAt(parent.fd, parent.name); } finally { closeSync(parent.fd); }
86
+ }
87
+ function persistJson(path, value, maxBytes = MAX_PLAN_BYTES) {
88
+ persistImmutableInstallationFile(path, canonicalSerialize(value), { maxBytes });
89
+ }
90
+ function privateDirectory(path) {
91
+ // Reuse the protected immutable writer to create/validate the ancestor chain.
92
+ persistImmutableInstallationFile(join(path, '.storage'), 'restoration-v1');
93
+ return openCheckpointDirectory(path);
94
+ }
95
+
96
+ function checkpointBase(checkpoint, storeBlob) {
97
+ // Every Git write is confined to this owned disposable repository. No Git
98
+ // mutating subprocess operates on the source, even if its parent is killed.
99
+ const scratch = mkdtempSync(join(tmpdir(), 'chati-restoration-base-'));
100
+ try {
101
+ git(scratch, ['init', '--bare', '-q', `--object-format=${checkpoint.manifest.state.git.object_format}`]);
102
+ git(scratch, ['index-pack', '--stdin'], { encoding: null, input: checkpoint.readBlob(checkpoint.manifest.pack) });
103
+ const base = checkpoint.manifest.binding.base_commit_ref;
104
+ git(scratch, ['-c', 'core.splitIndex=false', '-c', 'index.version=2', 'read-tree', base]);
105
+ const index = storeBlob(readFileSync(join(scratch, 'index')));
106
+ const bytes = git(scratch, ['ls-tree', '-r', '-z', '--full-tree', base], { encoding: null });
107
+ if (bytes.length && bytes.at(-1) !== 0) fail('RESTORATION_BASE_INVALID', 'base inventory is incomplete');
108
+ const files = [];
109
+ for (const line of decode(bytes).split('\0').filter(Boolean)) {
110
+ const match = /^(100644|100755|120000) blob ([a-f0-9]{40,64})\t([\s\S]+)$/.exec(line);
111
+ if (!match) fail('RESTORATION_BASE_UNSUPPORTED', 'base must contain ordinary files and symlinks only');
112
+ if (excluded(match[3])) continue;
113
+ checkpointPathComponents(match[3]);
114
+ const blob = storeBlob(git(scratch, ['cat-file', 'blob', match[2]], { encoding: null }));
115
+ files.push({ path: match[3], state: match[1] === '120000' ? { type: 'symlink', blob }
116
+ : { type: 'file', mode: match[1] === '100755' ? 0o755 : 0o644, blob } });
117
+ }
118
+ return { files, index };
119
+ } finally { rmSync(scratch, { recursive: true, force: true }); }
120
+ }
121
+
122
+ function createPlan(projectDir, checkpoint, directory) {
123
+ const repository = assertRailCheckpointSourceUnchanged(projectDir, checkpoint);
124
+ const roots = { worktree: resolve(projectDir), git: repository.git_dir.path, common: repository.common_git_dir.path };
125
+ const rootDevice = repository.root.dev;
126
+ const entries = new Map(); const desired = new Map();
127
+ let blobBytes = 0; const blobs = new Map();
128
+ const storeBlob = (bytes) => {
129
+ const value = ref(bytes);
130
+ if (!blobs.has(value.sha256)) {
131
+ blobBytes += bytes.length;
132
+ if (blobBytes > MAX_BYTES) fail('RESTORATION_SIZE_LIMIT', 'restoration base exceeds 128 MiB');
133
+ persistImmutableInstallationFile(join(directory, 'blobs', value.sha256), bytes, { maxBytes: MAX_BYTES });
134
+ blobs.set(value.sha256, value);
135
+ }
136
+ return value;
137
+ };
138
+ for (const file of checkpoint.manifest.state.files) {
139
+ const { path, ...before } = file;
140
+ entries.set(entryKey('worktree', path), { scope: 'worktree', path, before });
141
+ }
142
+ const add = (scope, path, after) => {
143
+ const key = entryKey(scope, path);
144
+ if (!entries.has(key)) entries.set(key, { scope, path, before: readEntry(roots, { scope, path }) });
145
+ desired.set(key, after);
146
+ for (let parent = dirname(path); parent !== '.'; parent = dirname(parent)) {
147
+ const parentKey = entryKey(scope, parent);
148
+ if (!entries.has(parentKey)) entries.set(parentKey, { scope, path: parent, before: readEntry(roots, { scope, path: parent }) });
149
+ const original = entries.get(parentKey).before;
150
+ desired.set(parentKey, original.type === 'directory' ? original : { type: 'directory', mode: 0o755 });
151
+ }
152
+ };
153
+ const base = checkpointBase(checkpoint, storeBlob);
154
+ for (const file of base.files) add('worktree', file.path, file.state);
155
+ add('git', 'index', { type: 'file', mode: 0o600, blob: base.index });
156
+ const saved = checkpoint.manifest.state.git;
157
+ // Preserve symbolic HEAD spelling; only its exact ref value returns to base.
158
+ let refScope = 'git'; let refPath = 'HEAD';
159
+ if (saved.head_ref) {
160
+ const absolute = resolve(projectDir, git(projectDir, ['rev-parse', '--git-path', saved.head_ref]).trim());
161
+ refScope = absolute.startsWith(roots.git + '/') ? 'git' : 'common';
162
+ refPath = relative(roots[refScope], absolute).split('\\').join('/');
163
+ if (!refPath.startsWith('refs/')) fail('RESTORATION_REF_STORAGE_UNSUPPORTED', 'restoration requires descriptor-relative files ref storage');
164
+ add('git', 'HEAD', { type: 'file', mode: readEntry(roots, { scope: 'git', path: 'HEAD' }).mode, blob: saved.head_file });
165
+ }
166
+ add(refScope, refPath, { type: 'file', mode: 0o644, blob: storeBlob(Buffer.from(checkpoint.manifest.binding.base_commit_ref + '\n')) });
167
+ // Packed refs are a guard, never rewritten. This catches movement of a ref
168
+ // that had no loose file when the restoration intent was committed.
169
+ const packed = { scope: 'common', path: 'packed-refs' };
170
+ const guards = [{ ...packed, before: readEntry(roots, packed) }];
171
+ const lockPaths = new Map([['git', 'index.lock'], ['git', 'HEAD.lock'], [refScope, refPath + '.lock'], ['common', 'packed-refs.lock']]
172
+ .map(([scope, path]) => [join(roots[scope], path), { scope, path }]));
173
+ for (const lock of lockPaths.values()) {
174
+ if (readEntry(roots, lock).type !== 'absent') fail('RESTORATION_GIT_LOCK_PRESENT', 'an unowned Git transaction lock must not be removed or bypassed', { path: entryPath(roots, lock) });
175
+ }
176
+ const operations = [];
177
+ const sorted = [...entries.entries()].sort((a, b) => a[1].path.localeCompare(b[1].path, 'en'));
178
+ const willRemove = [];
179
+ for (const [key, entry] of sorted) {
180
+ let after = desired.get(key) ?? ABSENT;
181
+ if (after.type === 'absent' && entry.before.type === 'directory' && entry.before.complete !== true) after = entry.before;
182
+ desired.set(key, after);
183
+ if (entry.before.type !== 'absent' && !same(entry.before, after)) willRemove.push({ ...entry, after: ABSENT, kind: 'retire' });
184
+ }
185
+ // Children leave first, so retiring a directory can only move an empty one.
186
+ willRemove.sort((a, b) => b.path.split('/').length - a.path.split('/').length || a.path.localeCompare(b.path, 'en'));
187
+ const gitRemovals = willRemove.filter((entry) => entry.scope !== 'worktree');
188
+ operations.push(...willRemove.filter((entry) => entry.scope === 'worktree'));
189
+ const additions = sorted.filter(([key, entry]) => desired.get(key).type !== 'absent' && !same(entry.before, desired.get(key)))
190
+ .map(([key, entry]) => ({ scope: entry.scope, path: entry.path, before: ABSENT, after: desired.get(key), kind: 'publish' }));
191
+ additions.sort((a, b) => a.path.split('/').length - b.path.split('/').length || a.path.localeCompare(b.path, 'en'));
192
+ operations.push(...additions.filter((entry) => entry.scope === 'worktree'));
193
+ // Index first, then ref/HEAD. All source mutations are synchronous syscalls,
194
+ // not detached Git children; the caller retains journal exclusion throughout.
195
+ for (const path of ['index', null]) {
196
+ operations.push(...gitRemovals.filter((entry) => path === 'index' ? entry.path === path : entry.path !== 'index'));
197
+ operations.push(...additions.filter((entry) => entry.scope !== 'worktree' && (path === 'index' ? entry.path === path : entry.path !== 'index')));
198
+ }
199
+ if (entries.size > 20_000 || operations.length > 40_000) fail('RESTORATION_ENTRY_LIMIT', 'restoration exceeds its bounded path inventory');
200
+ for (const entry of entries.values()) {
201
+ const parent = openParent(roots, entry, { allowMissing: true });
202
+ if (parent) {
203
+ try {
204
+ if (fstatSync(parent.fd).dev !== rootDevice) fail('RESTORATION_CROSS_DEVICE_UNSUPPORTED', 'restoration requires one filesystem for atomic preservation');
205
+ if (readEntryAt(parent.fd, parent.name).type !== 'absent' && lstatAtSync(parent.fd, parent.name).dev !== rootDevice) {
206
+ fail('RESTORATION_CROSS_DEVICE_UNSUPPORTED', 'restoration cannot relocate a mounted entry');
207
+ }
208
+ }
209
+ finally { closeSync(parent.fd); }
210
+ }
211
+ }
212
+ const storage = openCheckpointDirectory(directory);
213
+ try { if (fstatSync(storage).dev !== rootDevice) fail('RESTORATION_CROSS_DEVICE_UNSUPPORTED', 'checkpoint storage must share the worktree filesystem'); }
214
+ finally { closeSync(storage); }
215
+ assertRailCheckpointSourceUnchanged(projectDir, checkpoint);
216
+ return canonicalize({ schema_version: 1, checkpoint_receipt_digest: sha256(checkpoint.receipt),
217
+ binding: checkpoint.manifest.binding, roots, configuration_digest: saved.configuration_digest,
218
+ entries: [...entries.values()], guards, operations, blobs: [...blobs.values()],
219
+ locks: [...lockPaths.values()].map((lock) => ({ ...lock, nonce: randomUUID() })) });
220
+ }
221
+
222
+ function loadIntent(projectDir, checkpoint, directory) {
223
+ const stored = readSafeInstallationStorageFile(join(directory, 'intent.json'), { allowMissing: true, maxBytes: MAX_PLAN_BYTES });
224
+ if (stored) {
225
+ const intent = JSON.parse(stored.content);
226
+ verify(intent, { purpose: 'rail_restoration_intent', checkpoint_receipt_digest: sha256(checkpoint.receipt) });
227
+ return intent;
228
+ }
229
+ const plan = createPlan(projectDir, checkpoint, directory);
230
+ const intent = signed({ purpose: 'rail_restoration_intent', checkpoint_receipt_digest: sha256(checkpoint.receipt), plan });
231
+ persistJson(join(directory, 'intent.json'), intent);
232
+ return intent;
233
+ }
234
+ function expectedEntries(plan, cursor) {
235
+ const values = new Map(plan.entries.map((entry) => [entryKey(entry.scope, entry.path), { ...entry, state: entry.before }]));
236
+ for (const operation of plan.operations.slice(0, cursor)) values.get(entryKey(operation.scope, operation.path)).state = operation.after;
237
+ return [...values.values()];
238
+ }
239
+ function assertState(plan, cursor) {
240
+ const physical = railRepositoryStorageIdentity(plan.roots.worktree, { gitDir: plan.roots.git, commonGitDir: plan.roots.common });
241
+ if (sha256(physical) !== plan.binding.repository_identity_digest) fail('RESTORATION_SOURCE_CHANGED', 'repository storage identity changed');
242
+ const entries = expectedEntries(plan, cursor);
243
+ for (const entry of [...entries, ...plan.guards.map((guard) => ({ ...guard, state: guard.before }))]) {
244
+ if (!same(readEntry(plan.roots, entry), entry.state)) fail('RESTORATION_SOURCE_CHANGED', 'path differs from the durable restoration position', { scope: entry.scope, path: entry.path });
245
+ if (entry.state.type === 'directory' && entry.state.complete) {
246
+ const fd = openCheckpointDirectory(entryPath(plan.roots, entry));
247
+ try {
248
+ const actual = readDirectorySync(fd, { encoding: 'buffer' }).map(decode).sort();
249
+ const expected = entries.filter((child) => child.scope === entry.scope && dirname(child.path) === entry.path && child.state.type !== 'absent').map((child) => child.path.slice(entry.path.length + 1)).sort();
250
+ if (sha256(actual) !== sha256(expected)) fail('RESTORATION_SOURCE_CHANGED', 'complete directory has uninventoried children', { path: entry.path });
251
+ } finally { closeSync(fd); }
252
+ }
253
+ }
254
+ const head = entries.find((entry) => entry.scope === 'git' && entry.path === 'HEAD');
255
+ if (head?.state.type === 'absent') {
256
+ const next = plan.operations[cursor];
257
+ if (next?.scope !== 'git' || next.path !== 'HEAD' || next.kind !== 'publish') {
258
+ fail('RESTORATION_SOURCE_CHANGED', 'HEAD absence is not the authenticated retirement/publication interval');
259
+ }
260
+ // Every physical entry above matched the signed prefix. Only the following
261
+ // HEAD publication can proceed; Git/config/inventory are checked again as
262
+ // soon as the repository can be discovered, before any final receipt.
263
+ return;
264
+ }
265
+ if (sha256(railRepositoryIdentity(plan.roots.worktree)) !== plan.binding.repository_identity_digest
266
+ || readCheckpointConfiguration(plan.roots.worktree) !== plan.configuration_digest) fail('RESTORATION_SOURCE_CHANGED', 'repository or effective configuration changed');
267
+ const known = new Set(entries.filter((entry) => entry.scope === 'worktree').map((entry) => entry.path));
268
+ const untracked = decode(git(plan.roots.worktree, ['ls-files', '--others', '--exclude-standard', '-z'], { encoding: null }));
269
+ for (const path of untracked.split('\0').filter(Boolean)) {
270
+ if (!excluded(path) && !known.has(path)) fail('RESTORATION_SOURCE_CHANGED', 'new untracked work is outside the original checkpoint', { path });
271
+ }
272
+ }
273
+ function readBaseBlob(directory, checkpoint, blob) {
274
+ const stored = readSafeInstallationStorageFile(join(directory, 'blobs', blob.sha256), { allowMissing: true, encoding: null, maxBytes: MAX_BYTES });
275
+ const bytes = stored ? stored.content : checkpoint.readBlob(blob);
276
+ if (sha256(ref(bytes)) !== sha256(blob)) fail('RESTORATION_BLOB_CHANGED', 'restoration blob changed');
277
+ return bytes;
278
+ }
279
+ function prepareEntry(directory, checkpoint, state) {
280
+ const stage = privateDirectory(join(directory, 'staging', randomUUID()));
281
+ try {
282
+ if (state.type === 'symlink') symlinkAtSync(readBaseBlob(directory, checkpoint, state.blob), stage, 'entry');
283
+ else if (state.type === 'directory') {
284
+ mkdirAtSync(stage, 'entry');
285
+ const fd = openAtSync(stage, 'entry', O_DIRECTORY);
286
+ try { fchmodSync(fd, state.mode); fsyncSync(fd); } finally { closeSync(fd); }
287
+ } else {
288
+ const fd = openAtSync(stage, 'entry', constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
289
+ try { writeFileSync(fd, readBaseBlob(directory, checkpoint, state.blob)); fchmodSync(fd, state.mode); fsyncSync(fd); }
290
+ finally { closeSync(fd); }
291
+ }
292
+ fsyncSync(stage);
293
+ return stage;
294
+ } catch (error) { closeSync(stage); throw error; }
295
+ }
296
+ function verifyRetired(fd, index, operation, directory) {
297
+ const preservedPath = join(directory, 'retired', String(index));
298
+ if (!same(readEntryAt(fd, String(index)), operation.before)) fail('RESTORATION_SOURCE_CHANGED', 'entry changed during retirement; its bytes remain preserved', { preserved_path: preservedPath });
299
+ if (operation.before.type === 'directory') {
300
+ const child = openAtSync(fd, String(index), O_DIRECTORY);
301
+ try {
302
+ if (readDirectorySync(child, { encoding: 'buffer' }).length) fail('RESTORATION_SOURCE_CHANGED', 'concurrent directory contents remain preserved', { preserved_path: preservedPath });
303
+ } finally { closeSync(child); }
304
+ }
305
+ }
306
+
307
+ function acquireGitLocks(plan, intentDigest, directory) {
308
+ const held = [];
309
+ const assertHeld = () => {
310
+ for (const lock of held) {
311
+ const current = lstatAtSync(lock.parent.fd, lock.parent.name);
312
+ const pinned = fstatSync(lock.fd);
313
+ if (current.dev !== pinned.dev || current.ino !== pinned.ino || current.nlink !== 1
314
+ || !readCheckpointFileAt(lock.parent.fd, lock.parent.name).bytes.equals(lock.bytes)) {
315
+ fail('RESTORATION_GIT_LOCK_CHANGED', 'restoration no longer owns its exact Git lock');
316
+ }
317
+ }
318
+ };
319
+ const release = () => {
320
+ let failure;
321
+ for (const lock of held.reverse()) {
322
+ try {
323
+ const current = lstatAtSync(lock.parent.fd, lock.parent.name);
324
+ const pinned = fstatSync(lock.fd);
325
+ if (current.dev !== pinned.dev || current.ino !== pinned.ino || current.nlink !== 1
326
+ || !readCheckpointFileAt(lock.parent.fd, lock.parent.name).bytes.equals(lock.bytes)) {
327
+ fail('RESTORATION_GIT_LOCK_CHANGED', 'changed Git lock was not removed');
328
+ }
329
+ unlinkAtSync(lock.parent.fd, lock.parent.name); fsyncSync(lock.parent.fd);
330
+ } catch (error) { failure ??= error; }
331
+ finally { closeSync(lock.fd); closeSync(lock.parent.fd); }
332
+ }
333
+ held.length = 0;
334
+ if (failure) throw failure;
335
+ };
336
+ try {
337
+ for (const lock of plan.locks) {
338
+ const parent = openParent(plan.roots, lock);
339
+ const bytes = Buffer.from(canonicalSerialize({ purpose: 'rail_restoration_lock', intent_digest: intentDigest, nonce: lock.nonce }));
340
+ let fd;
341
+ try {
342
+ if (readEntryAt(parent.fd, parent.name).type === 'absent') {
343
+ const stage = privateDirectory(join(directory, 'lock-staging', randomUUID()));
344
+ try {
345
+ const staged = openAtSync(stage, 'entry', constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
346
+ try { writeFileSync(staged, bytes); fsyncSync(staged); } finally { closeSync(staged); }
347
+ try { renameNoReplaceAtSync(stage, 'entry', parent.fd, parent.name); fsyncSync(stage); fsyncSync(parent.fd); }
348
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
349
+ } finally { closeSync(stage); }
350
+ }
351
+ if (!readCheckpointFileAt(parent.fd, parent.name).bytes.equals(bytes)) {
352
+ fail('RESTORATION_GIT_LOCK_PRESENT', 'another Git transaction owns this lock', { path: entryPath(plan.roots, lock) });
353
+ }
354
+ fd = openAtSync(parent.fd, parent.name, constants.O_RDWR | constants.O_NONBLOCK);
355
+ if (!tryExclusiveFileLockSync(fd)) fail('RESTORATION_ALREADY_RUNNING', 'another restoration process still holds the Git lock');
356
+ held.push({ parent, fd, bytes }); fd = undefined;
357
+ } catch (error) { if (fd !== undefined) closeSync(fd); closeSync(parent.fd); throw error; }
358
+ }
359
+ assertHeld();
360
+ return { assertHeld, release };
361
+ } catch (error) { release(); throw error; }
362
+ }
363
+
364
+ function applyOperation(plan, operation, index, directory, checkpoint) {
365
+ hook('beforeOperation', { projectDir: plan.roots.worktree, operation, index });
366
+ const parent = openParent(plan.roots, operation);
367
+ try {
368
+ if (!same(readEntryAt(parent.fd, parent.name), operation.before)) fail('RESTORATION_SOURCE_CHANGED', 'entry changed before its restoration step', { path: operation.path });
369
+ if (operation.kind === 'retire') {
370
+ if (operation.before.type === 'directory') {
371
+ const fd = openAtSync(parent.fd, parent.name, O_DIRECTORY);
372
+ try { if (readDirectorySync(fd, { encoding: 'buffer' }).length) fail('RESTORATION_SOURCE_CHANGED', 'directory gained unpreserved contents'); }
373
+ finally { closeSync(fd); }
374
+ }
375
+ const retired = privateDirectory(join(directory, 'retired'));
376
+ try {
377
+ renameNoReplaceAtSync(parent.fd, parent.name, retired, String(index));
378
+ fsyncSync(parent.fd); fsyncSync(retired);
379
+ hook('afterRetire', { operation, preservedPath: join(directory, 'retired', String(index)) });
380
+ verifyRetired(retired, index, operation, directory);
381
+ } finally { closeSync(retired); }
382
+ } else {
383
+ const stage = prepareEntry(directory, checkpoint, operation.after);
384
+ try {
385
+ hook('beforePublish', { operation, projectDir: plan.roots.worktree });
386
+ try { renameNoReplaceAtSync(stage, 'entry', parent.fd, parent.name); }
387
+ catch (error) {
388
+ if (error.code === 'EEXIST') fail('RESTORATION_SOURCE_CHANGED', 'a concurrently created entry was preserved', { path: operation.path });
389
+ throw error;
390
+ }
391
+ fsyncSync(stage); fsyncSync(parent.fd);
392
+ }
393
+ finally { closeSync(stage); }
394
+ }
395
+ if (!same(readEntryAt(parent.fd, parent.name), operation.after)) fail('RESTORATION_SOURCE_CHANGED', 'entry changed after restoration step', { path: operation.path });
396
+ hook('afterMutation', { projectDir: plan.roots.worktree, operation, index });
397
+ } finally { closeSync(parent.fd); }
398
+ }
399
+
400
+ /** Host adapter only: caller must hold the canonical journal exclusion and
401
+ * revalidate stopped owner, lease, admission and effects before invoking it. */
402
+ export function restoreRailWorktreeCheckpoint({ projectDir, receipt }) {
403
+ const checkpoint = readRailWorktreeCheckpoint({ projectDir, receipt });
404
+ const directory = baseDirectory(projectDir, checkpoint);
405
+ const fd = privateDirectory(directory); closeSync(fd);
406
+ const intent = loadIntent(projectDir, checkpoint, directory);
407
+ const plan = intent.plan; const intentDigest = sha256(intent);
408
+ for (const blob of plan.blobs) readBaseBlob(directory, checkpoint, blob);
409
+ const locks = acquireGitLocks(plan, intentDigest, directory);
410
+ try {
411
+ let cursor = 0;
412
+ for (; cursor < plan.operations.length; cursor += 1) {
413
+ const existing = readSafeInstallationStorageFile(join(directory, 'steps', `${cursor}.json`), { allowMissing: true });
414
+ if (!existing) break;
415
+ verify(JSON.parse(existing.content), { purpose: 'rail_restoration_step', intent_digest: intentDigest, step_index: cursor });
416
+ }
417
+ // One syscall may be durable while its progress receipt is not. Recognize
418
+ // only that exact next state, never an arbitrary later prefix or new snapshot.
419
+ try { assertState(plan, cursor); }
420
+ catch (error) {
421
+ if (error.code !== 'RESTORATION_SOURCE_CHANGED' || cursor === plan.operations.length) throw error;
422
+ assertState(plan, cursor + 1);
423
+ const operation = plan.operations[cursor];
424
+ if (operation.kind === 'retire') {
425
+ const retired = openCheckpointDirectory(join(directory, 'retired'));
426
+ try { verifyRetired(retired, cursor, operation, directory); }
427
+ finally { closeSync(retired); }
428
+ }
429
+ persistJson(join(directory, 'steps', `${cursor}.json`), signed({ purpose: 'rail_restoration_step', intent_digest: intentDigest, step_index: cursor }));
430
+ hook('afterProgress', { projectDir, operation: plan.operations[cursor], index: cursor });
431
+ cursor += 1;
432
+ }
433
+ for (; cursor < plan.operations.length; cursor += 1) {
434
+ locks.assertHeld();
435
+ applyOperation(plan, plan.operations[cursor], cursor, directory, checkpoint);
436
+ persistJson(join(directory, 'steps', `${cursor}.json`), signed({ purpose: 'rail_restoration_step', intent_digest: intentDigest, step_index: cursor }));
437
+ hook('afterProgress', { projectDir, operation: plan.operations[cursor], index: cursor });
438
+ }
439
+ assertState(plan, cursor);
440
+ locks.assertHeld();
441
+ captureRailExecutionWorktree({ projectDir, baseCommitRef: plan.binding.base_commit_ref });
442
+ const result = signed({ purpose: 'rail_execution_restoration', schema_version: 1, project_dir: resolve(projectDir),
443
+ checkpoint_receipt_digest: sha256(receipt), intent_digest: intentDigest,
444
+ binding: plan.binding, restored_base_commit_ref: plan.binding.base_commit_ref, step_count: cursor });
445
+ const existing = readSafeInstallationStorageFile(join(directory, 'receipt.json'), { allowMissing: true });
446
+ if (existing) {
447
+ const value = JSON.parse(existing.content);
448
+ verify(value, { purpose: result.purpose, checkpoint_receipt_digest: result.checkpoint_receipt_digest, intent_digest: intentDigest });
449
+ return value;
450
+ }
451
+ persistJson(join(directory, 'receipt.json'), result);
452
+ return result;
453
+ } finally { locks.release(); }
454
+ }
455
+
456
+ /** Authenticate durable history without requiring later executions to remain at
457
+ * this old base. Current-state validation is mandatory at claim recovery only. */
458
+ export function readRailWorktreeRestoration({ projectDir, receipt, checkpointReceipt, expected = {}, current = false }) {
459
+ const checkpoint = readRailWorktreeCheckpoint({ projectDir, receipt: checkpointReceipt });
460
+ const directory = baseDirectory(projectDir, checkpoint);
461
+ const readJson = (path) => JSON.parse(readSafeInstallationStorageFile(path, { maxBytes: MAX_PLAN_BYTES }).content);
462
+ const intent = readJson(join(directory, 'intent.json'));
463
+ verify(intent, { purpose: 'rail_restoration_intent', checkpoint_receipt_digest: sha256(checkpointReceipt) });
464
+ const plan = intent.plan;
465
+ const stored = readJson(join(directory, 'receipt.json'));
466
+ if (sha256(stored) !== sha256(receipt)) fail('RESTORATION_AUTHORITY_MISMATCH', 'restoration receipt differs from durable storage');
467
+ verify(stored, { ...expected, purpose: 'rail_execution_restoration', schema_version: 1,
468
+ project_dir: resolve(projectDir), checkpoint_receipt_digest: sha256(checkpointReceipt),
469
+ intent_digest: sha256(intent), binding: checkpoint.manifest.binding,
470
+ restored_base_commit_ref: checkpoint.manifest.binding.base_commit_ref, step_count: plan.operations.length });
471
+ if (sha256(plan.binding) !== sha256(checkpoint.manifest.binding)
472
+ || plan.checkpoint_receipt_digest !== sha256(checkpointReceipt)) {
473
+ fail('RESTORATION_AUTHORITY_MISMATCH', 'restoration plan is not bound to this checkpoint');
474
+ }
475
+ for (const blob of plan.blobs) readBaseBlob(directory, checkpoint, blob);
476
+ for (let index = 0; index < plan.operations.length; index += 1) {
477
+ verify(readJson(join(directory, 'steps', `${index}.json`)), {
478
+ purpose: 'rail_restoration_step', intent_digest: sha256(intent), step_index: index });
479
+ }
480
+ if (current) {
481
+ assertState(plan, plan.operations.length);
482
+ captureRailExecutionWorktree({ projectDir, baseCommitRef: plan.binding.base_commit_ref });
483
+ }
484
+ return stored;
485
+ }