@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { enumerateArtifacts } from '../task/artifacts.js';
4
4
  import { parseTaskFrontmatter } from '../task/frontmatter.js';
5
+ import { renderHumanOverrideAudit } from '../task/human-override.js';
5
6
  import { resolveTaskRef } from '../task/resolve-ref.js';
6
7
  import { resolvePlatformContext } from './context.js';
7
8
  import { createGitHubClient } from './github-client.js';
@@ -10,8 +11,9 @@ import { platformResult } from './types.js';
10
11
  function summaryMarker(taskId) {
11
12
  return `<!-- sync-pr:${taskId}:summary -->`;
12
13
  }
13
- function buildPullRequestSummary(taskId, body, headSha) {
14
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
14
+ function buildPullRequestSummary(taskId, body, headSha, humanOverrideAudit = '') {
15
+ const sections = [body.replace(/\s+$/, ''), humanOverrideAudit.trim()].filter(Boolean).join('\n\n');
16
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
15
17
  }
16
18
  function reconcileSummaryComment(comments, taskId, desired) {
17
19
  const marker = summaryMarker(taskId);
@@ -67,7 +69,7 @@ function syncPullRequestSummary(taskRef, options) {
67
69
  catch (error) {
68
70
  return platformResult('failed', { platform: context.platform, capabilities: context.capabilities, error: { code: 'GIT_HEAD_UNRESOLVED', message: error instanceof Error ? error.message : String(error), retryable: false } });
69
71
  }
70
- const desired = buildPullRequestSummary(resolved.taskId, options.body, headSha);
72
+ const desired = buildPullRequestSummary(resolved.taskId, options.body, headSha, renderHumanOverrideAudit(fs.readFileSync(resolved.taskMdPath, 'utf8')));
71
73
  const listed = listRemoteComments(client, context.platform.repository, prNumber, resolved.repoRoot);
72
74
  if (!listed.ok)
73
75
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', { platform: context.platform, capabilities: context.capabilities, resource: { kind: 'pull-request', number: prNumber }, error: listed.error });
@@ -0,0 +1,319 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { verifySnapshot } from './store.js';
6
+ function repositoryKey(repository) {
7
+ return crypto.createHash('sha256').update(repository).digest('hex').slice(0, 32);
8
+ }
9
+ function checkpointPaths(root, repository) {
10
+ const directory = path.join(root, 'checkpoints', 'github');
11
+ const base = path.join(directory, repositoryKey(repository));
12
+ return { directory, checkpoint: `${base}.json`, lock: `${base}.lock` };
13
+ }
14
+ function processStartToken(pid) {
15
+ if (process.platform === 'linux') {
16
+ try {
17
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
18
+ const closing = stat.lastIndexOf(')');
19
+ const fields = stat.slice(closing + 2).split(' ');
20
+ return fields[19] ?? null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ return pid === process.pid ? `self:${process.pid}` : null;
27
+ }
28
+ function fsyncDirectory(directory) {
29
+ if (process.platform === 'win32')
30
+ return;
31
+ const fd = fs.openSync(directory, 'r');
32
+ try {
33
+ fs.fsyncSync(fd);
34
+ }
35
+ finally {
36
+ fs.closeSync(fd);
37
+ }
38
+ }
39
+ function ensurePrivateDirectory(directory) {
40
+ const missing = [];
41
+ let current = path.resolve(directory);
42
+ while (!fs.existsSync(current)) {
43
+ missing.push(current);
44
+ const parent = path.dirname(current);
45
+ if (parent === current)
46
+ throw new Error(`cannot create directory tree: ${directory}`);
47
+ current = parent;
48
+ }
49
+ for (const target of missing.reverse()) {
50
+ fs.mkdirSync(target, { mode: 0o700 });
51
+ if (process.platform !== 'win32')
52
+ fs.chmodSync(target, 0o700);
53
+ fsyncDirectory(path.dirname(target));
54
+ }
55
+ }
56
+ function writeTemporary(filePath, value) {
57
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
58
+ const temporary = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
59
+ const fd = fs.openSync(temporary, 'wx', 0o600);
60
+ try {
61
+ fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
62
+ fs.fsyncSync(fd);
63
+ }
64
+ finally {
65
+ fs.closeSync(fd);
66
+ }
67
+ return temporary;
68
+ }
69
+ function validCheckpoint(value, repository) {
70
+ if (!value || typeof value !== 'object')
71
+ return false;
72
+ const checkpoint = value;
73
+ return checkpoint.schema === 'github-checkpoint/v1'
74
+ && checkpoint.repository === repository
75
+ && Boolean(checkpoint.snapshotId)
76
+ && Boolean(checkpoint.watermark)
77
+ && Boolean(checkpoint.committedAt)
78
+ && /^[a-f0-9]{64}$/.test(checkpoint.manifestSha256);
79
+ }
80
+ function checkpointMatchesSnapshot(root, value) {
81
+ const verified = verifySnapshot(root, value.snapshotId);
82
+ return verified.ok
83
+ && verified.manifest?.schema === 'raw-manifest/v2'
84
+ && verified.manifest.snapshotId === value.snapshotId
85
+ && verified.manifest.repository === value.repository
86
+ && verified.manifest.manifestSha256 === value.manifestSha256
87
+ && verified.manifest.watermark === value.watermark;
88
+ }
89
+ function checkpointState(value) {
90
+ return { snapshotId: value?.snapshotId ?? null, manifestSha256: value?.manifestSha256 ?? null };
91
+ }
92
+ function readGitHubCheckpoint(root, repository) {
93
+ const target = checkpointPaths(root, repository).checkpoint;
94
+ if (!fs.existsSync(target))
95
+ return { ok: true, value: null };
96
+ try {
97
+ const parsed = JSON.parse(fs.readFileSync(target, 'utf8'));
98
+ const value = {
99
+ schema: parsed.schema,
100
+ repository: parsed.repository,
101
+ snapshotId: parsed.snapshotId,
102
+ watermark: parsed.watermark,
103
+ manifestSha256: parsed.manifestSha256,
104
+ committedAt: parsed.committedAt
105
+ };
106
+ if (!validCheckpoint(value, repository)) {
107
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'GitHub checkpoint schema or identity is invalid' } };
108
+ }
109
+ return { ok: true, value };
110
+ }
111
+ catch {
112
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'GitHub checkpoint is not valid JSON' } };
113
+ }
114
+ }
115
+ function quarantine(lockPath) {
116
+ const target = `${lockPath}.orphaned-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
117
+ fs.renameSync(lockPath, target);
118
+ return target;
119
+ }
120
+ function acquireGitHubCheckpoint(root, repository) {
121
+ const paths = checkpointPaths(root, repository);
122
+ ensurePrivateDirectory(paths.directory);
123
+ const recovery = { recovered: false, quarantined: [] };
124
+ const owner = {
125
+ schema: 'checkpoint-owner/v1',
126
+ ownerId: crypto.randomUUID(),
127
+ pid: process.pid,
128
+ host: os.hostname(),
129
+ processStartToken: processStartToken(process.pid) ?? '',
130
+ createdAt: new Date().toISOString()
131
+ };
132
+ if (!owner.processStartToken) {
133
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Process liveness cannot be verified on this host' } };
134
+ }
135
+ let recoveredOwnerId = null;
136
+ if (fs.existsSync(paths.lock)) {
137
+ let previous;
138
+ try {
139
+ previous = JSON.parse(fs.readFileSync(paths.lock, 'utf8'));
140
+ }
141
+ catch {
142
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint lock is unreadable' } };
143
+ }
144
+ if (previous.host !== owner.host || !previous.ownerId || !previous.processStartToken) {
145
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint lock owner topology is unknown' } };
146
+ }
147
+ const currentToken = processStartToken(previous.pid);
148
+ if (currentToken && currentToken === previous.processStartToken) {
149
+ return { ok: false, error: { code: 'CHECKPOINT_BUSY', message: 'Checkpoint is held by a live process' } };
150
+ }
151
+ if (process.platform !== 'linux') {
152
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint owner liveness cannot be proven on this host' } };
153
+ }
154
+ try {
155
+ recoveredOwnerId = previous.ownerId;
156
+ recovery.quarantined.push(quarantine(paths.lock));
157
+ recovery.recovered = true;
158
+ }
159
+ catch {
160
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Unable to quarantine an orphaned checkpoint lock' } };
161
+ }
162
+ }
163
+ try {
164
+ const fd = fs.openSync(paths.lock, 'wx', 0o600);
165
+ try {
166
+ fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`);
167
+ fs.fsyncSync(fd);
168
+ }
169
+ finally {
170
+ fs.closeSync(fd);
171
+ }
172
+ fsyncDirectory(paths.directory);
173
+ }
174
+ catch (error) {
175
+ if (error.code === 'EEXIST') {
176
+ return { ok: false, error: { code: 'CHECKPOINT_BUSY', message: 'Checkpoint lock was acquired by another process' } };
177
+ }
178
+ return { ok: false, error: { code: 'CHECKPOINT_LOCK_FAILED', message: String(error) } };
179
+ }
180
+ const releaseOwnedLock = () => {
181
+ try {
182
+ fs.unlinkSync(paths.lock);
183
+ fsyncDirectory(paths.directory);
184
+ }
185
+ catch {
186
+ // Preserve the original recovery error; a remaining lock will fail closed next time.
187
+ }
188
+ };
189
+ const beforeResult = readGitHubCheckpoint(root, repository);
190
+ if (!beforeResult.ok) {
191
+ releaseOwnedLock();
192
+ return beforeResult;
193
+ }
194
+ let checkpointBefore = beforeResult.value;
195
+ const temporaryCandidates = fs.readdirSync(paths.directory)
196
+ .filter((entry) => entry.startsWith(path.basename(paths.checkpoint) + '.') && entry.endsWith('.tmp'))
197
+ .map((entry) => path.join(paths.directory, entry));
198
+ if (temporaryCandidates.length > 1) {
199
+ releaseOwnedLock();
200
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Multiple checkpoint recovery candidates exist' } };
201
+ }
202
+ if (temporaryCandidates.length === 1) {
203
+ const candidate = temporaryCandidates[0];
204
+ try {
205
+ const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
206
+ const isCandidate = parsed && typeof parsed === 'object' && 'candidateSchema' in parsed
207
+ && parsed.candidateSchema === 'github-checkpoint-candidate/v1';
208
+ if (isCandidate) {
209
+ const candidateValue = parsed;
210
+ const value = candidateValue;
211
+ const expected = checkpointState(checkpointBefore);
212
+ const candidateExpected = candidateValue.expectedOld;
213
+ if (!recoveredOwnerId || candidateValue.ownerId !== recoveredOwnerId) {
214
+ recovery.quarantined.push(quarantine(candidate));
215
+ releaseOwnedLock();
216
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate owner proof does not match the recovered lock owner' } };
217
+ }
218
+ if (!validCheckpoint(value, repository)
219
+ || candidateExpected?.snapshotId !== expected.snapshotId
220
+ || candidateExpected?.manifestSha256 !== expected.manifestSha256) {
221
+ releaseOwnedLock();
222
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate does not match the current head' } };
223
+ }
224
+ if (!checkpointMatchesSnapshot(root, value)) {
225
+ recovery.quarantined.push(quarantine(candidate));
226
+ releaseOwnedLock();
227
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate snapshot failed full verification' } };
228
+ }
229
+ if (process.platform === 'win32' && checkpointBefore) {
230
+ releaseOwnedLock();
231
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Checkpoint replacement durability cannot be proven on this host' } };
232
+ }
233
+ fs.renameSync(candidate, paths.checkpoint);
234
+ recovery.recovered = true;
235
+ fsyncDirectory(paths.directory);
236
+ const recovered = readGitHubCheckpoint(root, repository);
237
+ if (!recovered.ok) {
238
+ releaseOwnedLock();
239
+ return recovered;
240
+ }
241
+ checkpointBefore = recovered.value;
242
+ }
243
+ else {
244
+ recovery.quarantined.push(quarantine(candidate));
245
+ releaseOwnedLock();
246
+ return { ok: false, error: { code: 'CHECKPOINT_RECOVERY_REQUIRED', message: 'Checkpoint candidate lacks owner proof' } };
247
+ }
248
+ }
249
+ catch {
250
+ recovery.quarantined.push(quarantine(candidate));
251
+ }
252
+ }
253
+ return { ok: true, value: { paths, repository, owner, recovery, expectedOld: checkpointBefore } };
254
+ }
255
+ function commitGitHubCheckpoint(lease, value) {
256
+ try {
257
+ const current = JSON.parse(fs.readFileSync(lease.paths.lock, 'utf8'));
258
+ if (current.ownerId !== lease.owner.ownerId) {
259
+ return { ok: false, error: { code: 'CHECKPOINT_OWNER_MISMATCH', message: 'Checkpoint lock ownership changed' } };
260
+ }
261
+ if (value.repository !== lease.repository || !validCheckpoint(value, lease.repository)) {
262
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'Checkpoint value is invalid' } };
263
+ }
264
+ const root = path.dirname(path.dirname(lease.paths.directory));
265
+ if (!checkpointMatchesSnapshot(root, value)) {
266
+ return { ok: false, error: { code: 'CHECKPOINT_INVALID', message: 'Checkpoint snapshot does not match a verified v2 manifest' } };
267
+ }
268
+ const currentCheckpoint = readGitHubCheckpoint(root, lease.repository);
269
+ if (!currentCheckpoint.ok)
270
+ return currentCheckpoint;
271
+ if (checkpointState(currentCheckpoint.value).snapshotId !== checkpointState(lease.expectedOld).snapshotId
272
+ || checkpointState(currentCheckpoint.value).manifestSha256 !== checkpointState(lease.expectedOld).manifestSha256) {
273
+ return { ok: false, error: { code: 'CHECKPOINT_CONFLICT', message: 'Checkpoint head changed before commit' } };
274
+ }
275
+ const candidate = {
276
+ ...value,
277
+ candidateSchema: 'github-checkpoint-candidate/v1',
278
+ ownerId: lease.owner.ownerId,
279
+ expectedOld: checkpointState(lease.expectedOld)
280
+ };
281
+ const temporary = writeTemporary(lease.paths.checkpoint, candidate);
282
+ const rechecked = readGitHubCheckpoint(root, lease.repository);
283
+ if (!rechecked.ok) {
284
+ fs.unlinkSync(temporary);
285
+ return rechecked;
286
+ }
287
+ if (checkpointState(rechecked.value).snapshotId !== checkpointState(lease.expectedOld).snapshotId
288
+ || checkpointState(rechecked.value).manifestSha256 !== checkpointState(lease.expectedOld).manifestSha256) {
289
+ fs.unlinkSync(temporary);
290
+ return { ok: false, error: { code: 'CHECKPOINT_CONFLICT', message: 'Checkpoint head changed during commit' } };
291
+ }
292
+ if (process.platform === 'win32' && fs.existsSync(lease.paths.checkpoint)) {
293
+ fs.unlinkSync(temporary);
294
+ return { ok: false, error: { code: 'CHECKPOINT_TOPOLOGY_UNSUPPORTED', message: 'Checkpoint replacement durability cannot be proven on this host' } };
295
+ }
296
+ fs.renameSync(temporary, lease.paths.checkpoint);
297
+ if (process.platform !== 'win32')
298
+ fs.chmodSync(lease.paths.checkpoint, 0o600);
299
+ fsyncDirectory(lease.paths.directory);
300
+ return { ok: true, value: undefined };
301
+ }
302
+ catch (error) {
303
+ return { ok: false, error: { code: 'CHECKPOINT_COMMIT_FAILED', message: String(error) } };
304
+ }
305
+ }
306
+ function releaseGitHubCheckpoint(lease) {
307
+ try {
308
+ const current = JSON.parse(fs.readFileSync(lease.paths.lock, 'utf8'));
309
+ if (current.ownerId === lease.owner.ownerId) {
310
+ fs.unlinkSync(lease.paths.lock);
311
+ fsyncDirectory(lease.paths.directory);
312
+ }
313
+ }
314
+ catch {
315
+ // The next invocation will fail closed and report recovery if the lock remains.
316
+ }
317
+ }
318
+ export { acquireGitHubCheckpoint, checkpointPaths, commitGitHubCheckpoint, processStartToken, readGitHubCheckpoint, releaseGitHubCheckpoint };
319
+ //# sourceMappingURL=checkpoint.js.map
@@ -1,20 +1,22 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { execFileSync } from 'node:child_process';
4
3
  import { detectRepoRoot } from '../task/resolve-ref.js';
5
- import { normalizeObjects } from './normalize.js';
6
- import { reconcileRecords } from './reconcile.js';
4
+ import { acquireGitHubCheckpoint, commitGitHubCheckpoint, readGitHubCheckpoint, releaseGitHubCheckpoint } from './checkpoint.js';
5
+ import { materializeJson, materializeSnapshot } from './materialize.js';
6
+ import { normalizeResources } from './normalize.js';
7
+ import { reconcileRecords, reconcileResourceRecords } from './reconcile.js';
7
8
  import { readAppliedOverlays, repairSnapshot } from './repair.js';
8
- import { collectGitHubObjects, collectLocalObjects } from './sources.js';
9
- import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.js';
9
+ import { collectGitHubBoundary, resolveGitHubRepository } from './sources.js';
10
+ import { canonicalJsonBytes, createObjectStore, findSnapshot, publishSnapshotV2, verifySnapshot } from './store.js';
11
+ import { createGitHubClient } from '../platform/github-client.js';
10
12
  const USAGE = `Usage: ai data <command> [options]
11
13
 
12
14
  Commands:
13
- capture [--source all|local|github] [--root <dir>] [--include-excerpts]
15
+ capture [--source github] [--root <dir>] [--full-reconcile]
14
16
  verify <snapshot-id> [--root <dir>]
15
17
  audit <snapshot-id> [--root <dir>] [--format json|text]
16
18
  repair <snapshot-id> [--root <dir>] [--apply]
17
- export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
19
+ export <snapshot-id> [--root <dir>] [--repairs none|applied] [--as-of <ISO>] [--output <file|->]
18
20
  `;
19
21
  function parseArgs(args, valueOptions, booleanOptions) {
20
22
  const positionals = [];
@@ -61,14 +63,6 @@ function dataRoot(repoRoot, requested) {
61
63
  }
62
64
  return resolved;
63
65
  }
64
- function repositoryIdentity(repoRoot) {
65
- try {
66
- return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
67
- }
68
- catch {
69
- return path.basename(repoRoot);
70
- }
71
- }
72
66
  function writeJson(value) {
73
67
  process.stdout.write(`${JSON.stringify(value)}\n`);
74
68
  }
@@ -77,61 +71,127 @@ function optionString(parsed, name) {
77
71
  return typeof value === 'string' ? value : undefined;
78
72
  }
79
73
  function capture(args) {
80
- const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
74
+ const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--full-reconcile', '--include-excerpts']));
81
75
  if (parsed.positionals.length > 0)
82
76
  throw new Error('capture does not accept positional arguments');
83
- const scope = optionString(parsed, '--source') ?? 'all';
84
- if (!['all', 'local', 'github'].includes(scope))
85
- throw new Error('--source must be all, local, or github');
77
+ const scope = optionString(parsed, '--source') ?? 'github';
78
+ if (scope !== 'github')
79
+ throw new Error('new process-data capture only supports --source github; local data is already normalized and --source all is retired');
80
+ if (parsed.options.has('--include-excerpts'))
81
+ throw new Error('--include-excerpts is not supported for GitHub-only capture');
86
82
  const repoRoot = detectRepoRoot();
87
83
  const root = dataRoot(repoRoot, optionString(parsed, '--root'));
88
- const observedFrom = new Date().toISOString();
89
- const objects = [];
90
- const endpoints = [];
91
- let repository = repositoryIdentity(repoRoot);
92
- if (scope === 'all' || scope === 'local') {
93
- const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
94
- if (!local.ok) {
95
- writeJson({ ok: false, error: local.error });
84
+ const client = createGitHubClient();
85
+ const repositoryResult = resolveGitHubRepository(repoRoot, client);
86
+ if (!repositoryResult.ok) {
87
+ writeJson({ ok: false, error: repositoryResult.error });
88
+ return 2;
89
+ }
90
+ const repository = repositoryResult.value;
91
+ const leaseResult = acquireGitHubCheckpoint(root, repository);
92
+ if (!leaseResult.ok) {
93
+ writeJson({ ok: false, error: leaseResult.error });
94
+ return 2;
95
+ }
96
+ const lease = leaseResult.value;
97
+ try {
98
+ const checkpointResult = readGitHubCheckpoint(root, repository);
99
+ if (!checkpointResult.ok) {
100
+ writeJson({ ok: false, error: checkpointResult.error });
96
101
  return 2;
97
102
  }
98
- objects.push(...local.value);
99
- }
100
- if (scope === 'all' || scope === 'github') {
101
- const github = collectGitHubObjects(repoRoot);
103
+ const checkpoint = checkpointResult.value;
104
+ const full = parsed.options.has('--full-reconcile');
105
+ const observedFrom = new Date().toISOString();
106
+ const github = collectGitHubBoundary(repoRoot, {
107
+ client,
108
+ fromInclusive: full ? null : checkpoint?.watermark ?? null,
109
+ reconciliation: full ? 'full' : 'incremental'
110
+ });
102
111
  if (!github.ok) {
103
112
  writeJson({ ok: false, error: github.error });
104
113
  return 2;
105
114
  }
106
- objects.push(...github.value.objects);
107
- endpoints.push(...github.value.endpoints);
108
- repository = github.value.repository;
109
- }
110
- const store = createObjectStore(root);
111
- for (const object of objects) {
112
- if (object.content !== undefined && object.disposition?.state === 'included') {
113
- const stored = store.put(Buffer.from(object.content, 'utf8'));
114
- if (stored.sha256 !== object.sha256)
115
- throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
115
+ const store = createObjectStore(root);
116
+ for (const object of github.value.objects) {
117
+ if (object.content !== undefined && object.disposition?.state === 'included') {
118
+ const stored = store.put(Buffer.from(object.content, 'utf8'));
119
+ if (stored.sha256 !== object.sha256)
120
+ throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
121
+ }
122
+ }
123
+ let parent = [];
124
+ if (checkpoint) {
125
+ const parentPath = findSnapshot(root, checkpoint.snapshotId);
126
+ if (!parentPath)
127
+ throw new Error(`checkpoint head snapshot is missing: ${checkpoint.snapshotId}`);
128
+ const materialized = materializeSnapshot(root, checkpoint.snapshotId);
129
+ if (!materialized.manifest || materialized.manifest.manifestSha256 !== checkpoint.manifestSha256 || materialized.manifest.watermark !== checkpoint.watermark) {
130
+ throw new Error('checkpoint does not match its verified snapshot head');
131
+ }
132
+ parent = materialized.records;
116
133
  }
134
+ const current = normalizeResources(github.value.objects);
135
+ const reconciliation = reconcileResourceRecords(current, {
136
+ parent,
137
+ full,
138
+ deferred: github.value.deferred,
139
+ unavailable: github.value.unavailable
140
+ });
141
+ const quality = reconcileRecords(reconciliation.records, 'github');
142
+ const fromInclusive = checkpoint?.watermark ?? null;
143
+ const hasStrictSinceEndpoint = github.value.endpoints.some((endpoint) => endpoint.queryMode === 'strict-since');
144
+ const queryAfter = !full && fromInclusive && hasStrictSinceEndpoint
145
+ ? new Date(new Date(fromInclusive).getTime() - 1000).toISOString()
146
+ : null;
147
+ const responseDates = github.value.responseDates;
148
+ const published = publishSnapshotV2(root, {
149
+ scope: 'github',
150
+ repository,
151
+ observedFrom,
152
+ observedTo: github.value.watermark,
153
+ objects: github.value.objects.map(({ content: _content, ...object }) => object),
154
+ endpoints: github.value.endpoints,
155
+ excerptsEnabled: false,
156
+ records: reconciliation.records,
157
+ quality: quality.findings,
158
+ repairs: quality.repairs,
159
+ snapshotKind: checkpoint ? 'delta' : 'base',
160
+ parentSnapshotId: checkpoint?.snapshotId ?? null,
161
+ checkpointBefore: checkpoint?.snapshotId ?? null,
162
+ watermark: github.value.watermark,
163
+ window: { fromInclusive, queryAfter, toExclusive: github.value.watermark, precision: 'second' },
164
+ observation: {
165
+ cutoffSource: 'github-response-date',
166
+ preflightDate: github.value.preflightDate,
167
+ responseDates
168
+ },
169
+ coverage: { mode: 'boundary-reread-with-full-reconcile', absoluteCompleteness: false },
170
+ reconciliation: full ? 'full' : 'incremental',
171
+ operations: reconciliation.operations
172
+ });
173
+ const verified = verifySnapshot(root, published.snapshotId);
174
+ if (!verified.ok || !verified.manifest || verified.manifest.schema !== 'raw-manifest/v2') {
175
+ throw new Error(`published snapshot failed verification: ${verified.errors.join('; ')}`);
176
+ }
177
+ const committed = commitGitHubCheckpoint(lease, {
178
+ schema: 'github-checkpoint/v1',
179
+ repository,
180
+ snapshotId: published.snapshotId,
181
+ watermark: github.value.watermark,
182
+ manifestSha256: verified.manifest.manifestSha256,
183
+ committedAt: new Date().toISOString()
184
+ });
185
+ if (!committed.ok) {
186
+ writeJson({ ok: false, error: committed.error });
187
+ return 2;
188
+ }
189
+ writeJson({ ok: true, snapshotId: published.snapshotId, scope, snapshotKind: checkpoint ? 'delta' : 'base', reconciliation: full ? 'full' : 'incremental', created: published.created, objects: github.value.objects.length, records: reconciliation.records.length, watermark: github.value.watermark, deferred: github.value.deferred.length });
190
+ return 0;
191
+ }
192
+ finally {
193
+ releaseGitHubCheckpoint(lease);
117
194
  }
118
- const records = normalizeObjects(objects);
119
- const reconciliation = reconcileRecords(records, scope);
120
- const observedTo = new Date().toISOString();
121
- const published = publishSnapshot(root, {
122
- scope: scope,
123
- repository,
124
- observedFrom,
125
- observedTo,
126
- objects: objects.map(({ content: _content, ...object }) => object),
127
- endpoints,
128
- excerptsEnabled: parsed.options.has('--include-excerpts'),
129
- records,
130
- quality: reconciliation.findings,
131
- repairs: reconciliation.repairs
132
- });
133
- writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
134
- return 0;
135
195
  }
136
196
  function verify(args) {
137
197
  const parsed = parseArgs(args, new Set(['--root']), new Set());
@@ -182,7 +242,7 @@ function repair(args) {
182
242
  return 0;
183
243
  }
184
244
  function exportSnapshot(args) {
185
- const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
245
+ const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output', '--as-of']), new Set());
186
246
  if (parsed.positionals.length !== 1)
187
247
  throw new Error('export requires one snapshot id');
188
248
  const repairMode = optionString(parsed, '--repairs') ?? 'none';
@@ -196,7 +256,15 @@ function exportSnapshot(args) {
196
256
  writeJson(verified);
197
257
  return 1;
198
258
  }
199
- let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
259
+ let content;
260
+ if (verified.manifest?.schema === 'raw-manifest/v2') {
261
+ content = materializeJson(root, parsed.positionals[0], { asOf: optionString(parsed, '--as-of') });
262
+ }
263
+ else {
264
+ if (optionString(parsed, '--as-of'))
265
+ throw new Error('--as-of is only supported for v2 GitHub snapshots');
266
+ content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
267
+ }
200
268
  if (repairMode === 'applied')
201
269
  content += readAppliedOverlays(root, parsed.positionals[0]);
202
270
  if (output === '-')