@fitlab-ai/agent-infra 0.9.3 → 0.9.4

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 (175) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -123
  6. package/dist/lib/agent-clients/adapter.js +92 -2
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  12. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  13. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  14. package/dist/lib/agent-clients/registry.js +9 -1
  15. package/dist/lib/internal/sandbox-control.js +24 -0
  16. package/dist/lib/internal/task-activity.js +45 -13
  17. package/dist/lib/internal/task-ledger.js +12 -3
  18. package/dist/lib/internal/task-lifecycle.js +19 -1
  19. package/dist/lib/internal/task-orchestration.js +122 -18
  20. package/dist/lib/platform/github-client.js +19 -2
  21. package/dist/lib/platform/pr-checks.js +1 -1
  22. package/dist/lib/platform/pull-requests.js +47 -1
  23. package/dist/lib/platform/releases.js +13 -5
  24. package/dist/lib/process-data/index.js +239 -0
  25. package/dist/lib/process-data/normalize.js +198 -0
  26. package/dist/lib/process-data/privacy.js +33 -0
  27. package/dist/lib/process-data/reconcile.js +95 -0
  28. package/dist/lib/process-data/repair.js +58 -0
  29. package/dist/lib/process-data/sources.js +345 -0
  30. package/dist/lib/process-data/store.js +282 -0
  31. package/dist/lib/process-data/types.js +2 -0
  32. package/dist/lib/run/index.js +4 -3
  33. package/dist/lib/run/tui.js +7 -8
  34. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  35. package/dist/lib/sandbox/capture.js +7 -2
  36. package/dist/lib/sandbox/commands/create.js +109 -228
  37. package/dist/lib/sandbox/commands/enter.js +15 -47
  38. package/dist/lib/sandbox/commands/list-running.js +16 -6
  39. package/dist/lib/sandbox/commands/ls.js +10 -6
  40. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  41. package/dist/lib/sandbox/commands/refresh.js +4 -84
  42. package/dist/lib/sandbox/commands/rm.js +24 -3
  43. package/dist/lib/sandbox/commands/show.js +12 -3
  44. package/dist/lib/sandbox/commands/start.js +7 -4
  45. package/dist/lib/sandbox/config.js +2 -0
  46. package/dist/lib/sandbox/constants.js +6 -0
  47. package/dist/lib/sandbox/control/client.js +44 -0
  48. package/dist/lib/sandbox/control/protocol.js +35 -0
  49. package/dist/lib/sandbox/control/server.js +109 -0
  50. package/dist/lib/sandbox/dockerfile.js +36 -13
  51. package/dist/lib/sandbox/mounts.js +17 -4
  52. package/dist/lib/sandbox/recovery.js +128 -5
  53. package/dist/lib/sandbox/redaction.js +12 -0
  54. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  55. package/dist/lib/sandbox/workspace-identity.js +95 -0
  56. package/dist/lib/sandbox/workspace-view.js +103 -0
  57. package/dist/lib/task/activity-intent.js +257 -51
  58. package/dist/lib/task/commit-finalization.js +267 -0
  59. package/dist/lib/task/commit-intent.js +165 -0
  60. package/dist/lib/task/decision-intents.js +31 -17
  61. package/dist/lib/task/delegation-receipts.js +2 -2
  62. package/dist/lib/task/implementation-inputs.js +54 -7
  63. package/dist/lib/task/ledger-intents.js +35 -1
  64. package/dist/lib/task/orchestration.js +363 -28
  65. package/dist/lib/task/resolve-ref.js +41 -1
  66. package/lib/agent-clients/adapter.ts +126 -2
  67. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  68. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  69. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  71. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  72. package/lib/agent-clients/adapters/opencode.ts +18 -3
  73. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  74. package/lib/agent-clients/registry.ts +9 -1
  75. package/lib/internal/sandbox-control.ts +24 -0
  76. package/lib/internal/task-activity.ts +50 -14
  77. package/lib/internal/task-ledger.ts +12 -3
  78. package/lib/internal/task-lifecycle.ts +22 -1
  79. package/lib/internal/task-orchestration.ts +81 -11
  80. package/lib/platform/github-client.ts +20 -2
  81. package/lib/platform/pr-checks.ts +1 -1
  82. package/lib/platform/pull-requests.ts +49 -1
  83. package/lib/platform/releases.ts +16 -4
  84. package/lib/process-data/index.ts +224 -0
  85. package/lib/process-data/normalize.ts +202 -0
  86. package/lib/process-data/privacy.ts +41 -0
  87. package/lib/process-data/reconcile.ts +110 -0
  88. package/lib/process-data/repair.ts +64 -0
  89. package/lib/process-data/sources.ts +354 -0
  90. package/lib/process-data/store.ts +315 -0
  91. package/lib/process-data/types.ts +135 -0
  92. package/lib/run/index.ts +5 -3
  93. package/lib/run/tui.ts +11 -8
  94. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  95. package/lib/sandbox/capture.ts +9 -2
  96. package/lib/sandbox/commands/create.ts +126 -260
  97. package/lib/sandbox/commands/enter.ts +13 -59
  98. package/lib/sandbox/commands/list-running.ts +23 -6
  99. package/lib/sandbox/commands/ls.ts +22 -6
  100. package/lib/sandbox/commands/rebuild.ts +4 -3
  101. package/lib/sandbox/commands/refresh.ts +4 -126
  102. package/lib/sandbox/commands/rm.ts +24 -3
  103. package/lib/sandbox/commands/show.ts +29 -3
  104. package/lib/sandbox/commands/start.ts +6 -4
  105. package/lib/sandbox/config.ts +4 -0
  106. package/lib/sandbox/constants.ts +8 -0
  107. package/lib/sandbox/control/client.ts +55 -0
  108. package/lib/sandbox/control/protocol.ts +75 -0
  109. package/lib/sandbox/control/server.ts +117 -0
  110. package/lib/sandbox/dockerfile.ts +66 -13
  111. package/lib/sandbox/mounts.ts +24 -4
  112. package/lib/sandbox/recovery.ts +163 -6
  113. package/lib/sandbox/redaction.ts +14 -0
  114. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  115. package/lib/sandbox/tool-types.ts +16 -0
  116. package/lib/sandbox/workspace-identity.ts +123 -0
  117. package/lib/sandbox/workspace-view.ts +143 -0
  118. package/lib/task/activity-intent.ts +350 -61
  119. package/lib/task/commit-finalization.ts +356 -0
  120. package/lib/task/commit-intent.ts +230 -0
  121. package/lib/task/decision-intents.ts +32 -12
  122. package/lib/task/delegation-receipts.ts +2 -2
  123. package/lib/task/implementation-inputs.ts +62 -8
  124. package/lib/task/ledger-intents.ts +43 -3
  125. package/lib/task/orchestration.ts +501 -33
  126. package/lib/task/resolve-ref.ts +46 -1
  127. package/package.json +1 -1
  128. package/templates/.agents/rules/README.en.md +1 -1
  129. package/templates/.agents/rules/README.zh-CN.md +1 -1
  130. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  131. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  132. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  133. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  134. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  135. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  136. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  137. package/templates/.agents/rules/task-management.en.md +2 -0
  138. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  139. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.en.md +21 -11
  142. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
  143. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  144. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  149. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  150. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  151. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  152. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  153. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  156. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  157. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  158. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  159. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  161. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  162. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  163. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  164. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  165. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  166. package/templates/.agents/templates/task.en.md +1 -1
  167. package/templates/.agents/templates/task.zh-CN.md +1 -1
  168. package/templates/.claude/commands/watch-pr.en.md +1 -1
  169. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  170. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  171. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  172. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  173. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  174. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  175. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -0,0 +1,315 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import type {
6
+ JsonValue,
7
+ NormalizedRecord,
8
+ QualityFinding,
9
+ RepairAction,
10
+ RestCollectionEvidence,
11
+ SnapshotManifest,
12
+ StoredObjectEvidence
13
+ } from './types.ts';
14
+
15
+ type ObjectWrite = { sha256: string; path: string; created: boolean };
16
+ type ObjectStore = { put(bytes: Buffer): ObjectWrite; pathFor(sha256: string): string };
17
+
18
+ type PublishInput = {
19
+ scope: 'all' | 'local' | 'github';
20
+ repository: string;
21
+ observedFrom: string;
22
+ observedTo: string;
23
+ objects: StoredObjectEvidence[];
24
+ endpoints: RestCollectionEvidence[];
25
+ excerptsEnabled: boolean;
26
+ records: NormalizedRecord[];
27
+ quality: QualityFinding[];
28
+ repairs: RepairAction[];
29
+ };
30
+
31
+ type VerificationResult = {
32
+ ok: boolean;
33
+ snapshotId: string;
34
+ snapshotPath: string | null;
35
+ errors: string[];
36
+ manifest?: SnapshotManifest;
37
+ };
38
+
39
+ function sha256(bytes: Buffer | string): string {
40
+ return crypto.createHash('sha256').update(bytes).digest('hex');
41
+ }
42
+
43
+ function compareCodePoints(left: string, right: string): number {
44
+ const a = Array.from(left, (char) => char.codePointAt(0)!);
45
+ const b = Array.from(right, (char) => char.codePointAt(0)!);
46
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
47
+ if (a[index] !== b[index]) return a[index]! - b[index]!;
48
+ }
49
+ return a.length - b.length;
50
+ }
51
+
52
+ function canonicalString(value: unknown, seen: Set<object>): string {
53
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') {
54
+ return JSON.stringify(value);
55
+ }
56
+ if (typeof value === 'number') {
57
+ if (!Number.isFinite(value)) throw new Error('canonical JSON accepts only finite JSON values');
58
+ return JSON.stringify(value);
59
+ }
60
+ if (Array.isArray(value)) {
61
+ if (seen.has(value)) throw new Error('canonical JSON accepts only acyclic JSON values');
62
+ seen.add(value);
63
+ const output = `[${value.map((entry) => canonicalString(entry, seen)).join(',')}]`;
64
+ seen.delete(value);
65
+ return output;
66
+ }
67
+ if (typeof value === 'object') {
68
+ if (seen.has(value)) throw new Error('canonical JSON accepts only acyclic JSON values');
69
+ seen.add(value);
70
+ const record = value as Record<string, unknown>;
71
+ const fields = Object.keys(record)
72
+ .sort(compareCodePoints)
73
+ .map((key) => `${JSON.stringify(key)}:${canonicalString(record[key], seen)}`);
74
+ seen.delete(value);
75
+ return `{${fields.join(',')}}`;
76
+ }
77
+ throw new Error('canonical JSON accepts only JSON values');
78
+ }
79
+
80
+ function canonicalJsonBytes(value: JsonValue | unknown): Buffer {
81
+ return Buffer.from(canonicalString(value, new Set()), 'utf8');
82
+ }
83
+
84
+ function ensurePrivateDirectory(directory: string): void {
85
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
86
+ if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
87
+ }
88
+
89
+ function writeExclusive(filePath: string, bytes: Buffer): boolean {
90
+ ensurePrivateDirectory(path.dirname(filePath));
91
+ try {
92
+ const fd = fs.openSync(filePath, 'wx', 0o600);
93
+ try {
94
+ fs.writeFileSync(fd, bytes);
95
+ fs.fsyncSync(fd);
96
+ } finally {
97
+ fs.closeSync(fd);
98
+ }
99
+ return true;
100
+ } catch (error) {
101
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function createObjectStore(root: string): ObjectStore {
107
+ const objectRoot = path.join(root, 'objects', 'sha256');
108
+ ensurePrivateDirectory(objectRoot);
109
+ return {
110
+ pathFor(digest: string) {
111
+ if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error(`invalid SHA-256: ${digest}`);
112
+ return path.join(objectRoot, digest.slice(0, 2), digest);
113
+ },
114
+ put(bytes: Buffer) {
115
+ const digest = sha256(bytes);
116
+ const objectPath = path.join(objectRoot, digest.slice(0, 2), digest);
117
+ const created = writeExclusive(objectPath, bytes);
118
+ const stored = fs.readFileSync(objectPath);
119
+ if (sha256(stored) !== digest) throw new Error(`CAS hash mismatch for ${digest}`);
120
+ return { sha256: digest, path: objectPath, created };
121
+ }
122
+ };
123
+ }
124
+
125
+ function dispositionCounts(objects: StoredObjectEvidence[]): SnapshotManifest['dispositionCounts'] {
126
+ const counts: SnapshotManifest['dispositionCounts'] = { included: 0, 'excluded-sensitive': 0, unavailable: 0 };
127
+ for (const object of objects) counts[object.disposition?.state ?? 'included'] += 1;
128
+ return counts;
129
+ }
130
+
131
+ function jsonLine(value: JsonValue | unknown): string {
132
+ return `${canonicalJsonBytes(value).toString('utf8')}\n`;
133
+ }
134
+
135
+ function writePrivateFile(filePath: string, content: string | Buffer): void {
136
+ fs.writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' });
137
+ if (process.platform !== 'win32') fs.chmodSync(filePath, 0o600);
138
+ }
139
+
140
+ function manifestDigest(manifest: Omit<SnapshotManifest, 'manifestSha256'>): string {
141
+ return sha256(canonicalJsonBytes(manifest));
142
+ }
143
+
144
+ function publishSnapshot(root: string, input: PublishInput): { snapshotId: string; path: string; created: boolean } {
145
+ ensurePrivateDirectory(root);
146
+ ensurePrivateDirectory(path.join(root, 'repairs'));
147
+ const date = new Date(input.observedTo);
148
+ if (!Number.isFinite(date.getTime())) throw new Error('observedTo must be an ISO timestamp');
149
+
150
+ const identity = {
151
+ scope: input.scope,
152
+ repository: input.repository,
153
+ observedFrom: input.observedFrom,
154
+ observedTo: input.observedTo,
155
+ objects: input.objects,
156
+ endpoints: input.endpoints,
157
+ excerptsEnabled: input.excerptsEnabled,
158
+ dispositionCounts: dispositionCounts(input.objects),
159
+ recordCount: input.records.length,
160
+ findingCount: input.quality.length,
161
+ repairCount: input.repairs.length
162
+ };
163
+ const identityDigest = sha256(canonicalJsonBytes(identity));
164
+ const stamp = input.observedTo.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
165
+ const snapshotId = `${stamp}-${identityDigest.slice(0, 12)}`;
166
+ const partialManifest: Omit<SnapshotManifest, 'manifestSha256'> = {
167
+ schema: 'raw-manifest/v1',
168
+ snapshotId,
169
+ ...identity,
170
+ privacyPolicyVersion: 'process-data-privacy/v1'
171
+ };
172
+ const manifest: SnapshotManifest = { ...partialManifest, manifestSha256: manifestDigest(partialManifest) };
173
+
174
+ const stagingRoot = path.join(root, '.staging');
175
+ ensurePrivateDirectory(stagingRoot);
176
+ const staging = fs.mkdtempSync(path.join(stagingRoot, `${snapshotId}-`));
177
+ if (process.platform !== 'win32') fs.chmodSync(staging, 0o700);
178
+ try {
179
+ writePrivateFile(path.join(staging, 'manifest.json'), jsonLine(manifest));
180
+ writePrivateFile(
181
+ path.join(staging, 'normalized.jsonl'),
182
+ input.records.map((record) => jsonLine({ schema: 'normalized/v1', ...record })).join('')
183
+ );
184
+ writePrivateFile(path.join(staging, 'quality.json'), jsonLine({ schema: 'quality/v1', findings: input.quality }));
185
+ writePrivateFile(
186
+ path.join(staging, 'repair-manifest.jsonl'),
187
+ input.repairs.map((repair) => jsonLine({ schema: 'repair/v1', ...repair })).join('')
188
+ );
189
+
190
+ const destination = path.join(
191
+ root,
192
+ 'snapshots',
193
+ String(date.getUTCFullYear()),
194
+ String(date.getUTCMonth() + 1).padStart(2, '0'),
195
+ String(date.getUTCDate()).padStart(2, '0'),
196
+ snapshotId
197
+ );
198
+ ensurePrivateDirectory(path.dirname(destination));
199
+ if (fs.existsSync(destination)) {
200
+ const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
201
+ if (existing !== jsonLine(manifest)) throw new Error(`snapshot collision: ${snapshotId}`);
202
+ fs.rmSync(staging, { recursive: true, force: true });
203
+ return { snapshotId, path: destination, created: false };
204
+ }
205
+ fs.renameSync(staging, destination);
206
+ return { snapshotId, path: destination, created: true };
207
+ } catch (error) {
208
+ fs.rmSync(staging, { recursive: true, force: true });
209
+ throw error;
210
+ }
211
+ }
212
+
213
+ function findSnapshot(root: string, snapshotId: string): string | null {
214
+ const snapshotRoot = path.join(root, 'snapshots');
215
+ if (!fs.existsSync(snapshotRoot)) return null;
216
+ const stack = [snapshotRoot];
217
+ while (stack.length > 0) {
218
+ const current = stack.pop()!;
219
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
220
+ if (!entry.isDirectory()) continue;
221
+ const candidate = path.join(current, entry.name);
222
+ if (entry.name === snapshotId && fs.existsSync(path.join(candidate, 'manifest.json'))) return candidate;
223
+ stack.push(candidate);
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+
229
+ function verifySnapshot(root: string, snapshotId: string): VerificationResult {
230
+ const snapshotPath = findSnapshot(root, snapshotId);
231
+ if (!snapshotPath) return { ok: false, snapshotId, snapshotPath: null, errors: ['snapshot not found'] };
232
+ const errors: string[] = [];
233
+ const checkMode = (target: string, expected: number, label: string) => {
234
+ if (process.platform === 'win32' || !fs.existsSync(target)) return;
235
+ const actual = fs.statSync(target).mode & 0o777;
236
+ if (actual !== expected) errors.push(`${label} permission mismatch: expected ${expected.toString(8)}, got ${actual.toString(8)}`);
237
+ };
238
+ checkMode(snapshotPath, 0o700, 'snapshot directory');
239
+ for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) {
240
+ checkMode(path.join(snapshotPath, file), 0o600, file);
241
+ }
242
+ let manifest: SnapshotManifest;
243
+ try {
244
+ manifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8')) as SnapshotManifest;
245
+ } catch {
246
+ return { ok: false, snapshotId, snapshotPath, errors: ['manifest is invalid JSON'] };
247
+ }
248
+ if (manifest.schema !== 'raw-manifest/v1') errors.push(`unsupported manifest schema: ${manifest.schema}`);
249
+ if (manifest.snapshotId !== snapshotId) errors.push('snapshot identity mismatch');
250
+ if (typeof manifest.excerptsEnabled !== 'boolean') errors.push('manifest excerpts opt-in is invalid');
251
+ if (!manifest.dispositionCounts || canonicalJsonBytes(manifest.dispositionCounts).toString('utf8') !== canonicalJsonBytes(dispositionCounts(manifest.objects)).toString('utf8')) {
252
+ errors.push('manifest disposition counts mismatch');
253
+ }
254
+ const { manifestSha256, ...partial } = manifest;
255
+ if (manifestDigest(partial) !== manifestSha256) errors.push('manifest hash mismatch');
256
+ const store = createObjectStore(root);
257
+ try {
258
+ const lines = fs.readFileSync(path.join(snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean);
259
+ const ids = new Set<string>();
260
+ for (const line of lines) {
261
+ const record = JSON.parse(line) as { schema?: string; recordId?: string };
262
+ if (record.schema !== 'normalized/v1' || typeof record.recordId !== 'string') {
263
+ errors.push('normalized record schema is invalid');
264
+ continue;
265
+ }
266
+ if (ids.has(record.recordId)) errors.push(`duplicate normalized record: ${record.recordId}`);
267
+ ids.add(record.recordId);
268
+ }
269
+ if (lines.length !== manifest.recordCount) errors.push('normalized record count mismatch');
270
+ const quality = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'quality.json'), 'utf8')) as { schema?: string; findings?: unknown[] };
271
+ if (quality.schema !== 'quality/v1' || !Array.isArray(quality.findings)) errors.push('quality schema is invalid');
272
+ else if (quality.findings.length !== manifest.findingCount) errors.push('quality finding count mismatch');
273
+ const repairLines = fs.readFileSync(path.join(snapshotPath, 'repair-manifest.jsonl'), 'utf8').split('\n').filter(Boolean);
274
+ for (const line of repairLines) {
275
+ const repair = JSON.parse(line) as { schema?: string; repairId?: string };
276
+ if (repair.schema !== 'repair/v1' || typeof repair.repairId !== 'string') errors.push('repair manifest schema is invalid');
277
+ }
278
+ if (repairLines.length !== manifest.repairCount) errors.push('repair manifest count mismatch');
279
+ } catch {
280
+ errors.push('snapshot derived files are missing or invalid');
281
+ }
282
+ for (const object of manifest.objects) {
283
+ if (object.disposition?.state && object.disposition.state !== 'included') continue;
284
+ try {
285
+ const objectPath = store.pathFor(object.sha256);
286
+ checkMode(path.dirname(objectPath), 0o700, `object directory ${object.sha256.slice(0, 2)}`);
287
+ checkMode(objectPath, 0o600, `object ${object.sourceIdentity}`);
288
+ const bytes = fs.readFileSync(objectPath);
289
+ if (sha256(bytes) !== object.sha256) errors.push(`object hash mismatch: ${object.sourceIdentity}`);
290
+ if (bytes.length !== object.bytes) errors.push(`object byte count mismatch: ${object.sourceIdentity}`);
291
+ } catch {
292
+ errors.push(`object missing: ${object.sourceIdentity}`);
293
+ }
294
+ }
295
+ for (const page of manifest.endpoints.flatMap((endpoint) => endpoint.pages)) {
296
+ try {
297
+ if (sha256(fs.readFileSync(store.pathFor(page.canonicalSha256))) !== page.canonicalSha256) {
298
+ errors.push(`page hash mismatch: ${page.index}`);
299
+ }
300
+ } catch {
301
+ errors.push(`page object missing: ${page.canonicalSha256}`);
302
+ }
303
+ }
304
+ return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
305
+ }
306
+
307
+ export {
308
+ canonicalJsonBytes,
309
+ createObjectStore,
310
+ findSnapshot,
311
+ publishSnapshot,
312
+ sha256,
313
+ verifySnapshot
314
+ };
315
+ export type { ObjectStore, ObjectWrite, PublishInput, VerificationResult };
@@ -0,0 +1,135 @@
1
+ type JsonPrimitive = null | boolean | number | string;
2
+ type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
3
+
4
+ type Availability<T> =
5
+ | { state: 'known'; value: T }
6
+ | { state: 'unknown' | 'unavailable'; reason: string };
7
+
8
+ type SourceKind = 'local-file' | 'github-rest' | 'operational-report' | 'structured-telemetry';
9
+
10
+ type PrivacyDisposition =
11
+ | { state: 'included' }
12
+ | { state: 'excluded-sensitive'; ruleId: string }
13
+ | { state: 'unavailable'; reason: string };
14
+
15
+ type CapturedObject = {
16
+ sourceKind: SourceKind;
17
+ sourceIdentity: string;
18
+ sha256: string;
19
+ bytes: number;
20
+ content?: string;
21
+ disposition?: PrivacyDisposition;
22
+ };
23
+
24
+ type StoredObjectEvidence = Omit<CapturedObject, 'content'>;
25
+
26
+ type RestPageEvidence = {
27
+ index: number;
28
+ itemCount: number;
29
+ canonicalSha256: string;
30
+ };
31
+
32
+ type RestCollectionEvidence = {
33
+ endpoint: string;
34
+ requestCount: number;
35
+ dataPageCount: number;
36
+ itemCount: number;
37
+ termination: 'short-page';
38
+ pages: RestPageEvidence[];
39
+ };
40
+
41
+ type NormalizedKind =
42
+ | 'task'
43
+ | 'artifact'
44
+ | 'platform-resource'
45
+ | 'lifecycle-event'
46
+ | 'review-finding'
47
+ | 'human-ruling'
48
+ | 'telemetry'
49
+ | 'operational-report'
50
+ | 'missing'
51
+ | 'conflict';
52
+
53
+ type NormalizedRecord = {
54
+ recordId: string;
55
+ kind: NormalizedKind;
56
+ sourceIdentity: string;
57
+ sourceSha256: string;
58
+ binding?: string;
59
+ observedAt?: Availability<string>;
60
+ data?: JsonValue;
61
+ };
62
+
63
+ type QualityCategory =
64
+ | 'missing-local'
65
+ | 'missing-remote'
66
+ | 'duplicate-identity'
67
+ | 'binding-conflict'
68
+ | 'content-mismatch'
69
+ | 'schema-difference'
70
+ | 'mutable-remote'
71
+ | 'unrecoverable'
72
+ | 'privacy-excluded';
73
+
74
+ type QualityFinding = {
75
+ findingId: string;
76
+ category: QualityCategory;
77
+ severity: 'info' | 'warning' | 'error';
78
+ identities: string[];
79
+ repairable: boolean;
80
+ reason: string;
81
+ };
82
+
83
+ type RepairAction = {
84
+ repairId: string;
85
+ operation: 'add' | 'supersede' | 'link';
86
+ sourceRecordId: string;
87
+ targetRecordId: string;
88
+ preconditionSha256: string;
89
+ };
90
+
91
+ type SnapshotManifest = {
92
+ schema: 'raw-manifest/v1';
93
+ snapshotId: string;
94
+ scope: 'all' | 'local' | 'github';
95
+ repository: string;
96
+ observedFrom: string;
97
+ observedTo: string;
98
+ objects: StoredObjectEvidence[];
99
+ endpoints: RestCollectionEvidence[];
100
+ recordCount: number;
101
+ findingCount: number;
102
+ repairCount: number;
103
+ privacyPolicyVersion: 'process-data-privacy/v1';
104
+ excerptsEnabled: boolean;
105
+ dispositionCounts: Record<'included' | 'excluded-sensitive' | 'unavailable', number>;
106
+ manifestSha256: string;
107
+ };
108
+
109
+ type ProcessDataError = {
110
+ code: string;
111
+ message: string;
112
+ retryable?: boolean;
113
+ };
114
+
115
+ type ProcessResult<T> = { ok: true; value: T } | { ok: false; error: ProcessDataError };
116
+
117
+ export type {
118
+ Availability,
119
+ CapturedObject,
120
+ JsonPrimitive,
121
+ JsonValue,
122
+ NormalizedKind,
123
+ NormalizedRecord,
124
+ PrivacyDisposition,
125
+ ProcessDataError,
126
+ ProcessResult,
127
+ QualityCategory,
128
+ QualityFinding,
129
+ RepairAction,
130
+ RestCollectionEvidence,
131
+ RestPageEvidence,
132
+ SnapshotManifest,
133
+ SourceKind,
134
+ StoredObjectEvidence
135
+ };
package/lib/run/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { loadServerConfig } from '../server/config.ts';
4
- import { resolveTaskBranch } from '../sandbox/task-resolver.ts';
4
+ import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from '../sandbox/workspace-identity.ts';
5
5
  import { createRunId, runInSandbox, type SandboxRunMetadata } from '../sandbox/capture.ts';
6
6
  import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.ts';
7
7
  import { buildTuiCommand, renderPrompt, selectTui } from './tui.ts';
@@ -19,6 +19,7 @@ export type ParsedRunArgs = {
19
19
  export type SandboxRunRequest = {
20
20
  taskRef: string;
21
21
  branch: string;
22
+ workspace?: SandboxWorkspaceIdentity;
22
23
  command: string[];
23
24
  runId?: string;
24
25
  recreate?: boolean;
@@ -136,7 +137,6 @@ function resolveActiveTaskIdentity(taskRef: string, repoRoot: string): ActiveTas
136
137
 
137
138
  if (TASK_ID_RE.test(taskRef)) {
138
139
  taskId = taskRef;
139
- resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
140
140
  } else {
141
141
  const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
142
142
  if (normalized.kind !== 'shortId') return null;
@@ -211,7 +211,8 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
211
211
  }
212
212
 
213
213
  const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
214
- const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
214
+ const target = resolveSandboxTarget(parsed.taskRef, repoRoot);
215
+ const branch = target.branch;
215
216
  const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
216
217
  const runId = identity ? createRunId() : undefined;
217
218
  const writeStdout = options.writeStdout ?? ((chunk: string) => process.stdout.write(chunk));
@@ -222,6 +223,7 @@ export async function runSkill(args: string[], options: RunSkillOptions = {}): P
222
223
  const result = await runSandbox({
223
224
  taskRef: parsed.taskRef,
224
225
  branch,
226
+ workspace: target.workspace,
225
227
  command,
226
228
  runId,
227
229
  recreate: parsed.recreate
package/lib/run/tui.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import { renderAgentClientInvocation } from '../agent-clients/invocation.ts';
2
2
  import { getAgentClientAdapter } from '../agent-clients/registry.ts';
3
+ import type { AgentClientId } from '../agent-clients/types.ts';
3
4
 
4
5
  export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode';
5
6
 
6
7
  const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode']);
8
+ const TUI_AGENT_CLIENT_IDS: Readonly<Record<TuiName, AgentClientId>> = Object.freeze({
9
+ claude: 'claude-code',
10
+ codex: 'codex',
11
+ antigravity: 'antigravity-cli',
12
+ opencode: 'opencode'
13
+ });
7
14
 
8
15
  export type CommandConfig = {
9
16
  defaultTui?: unknown;
@@ -29,14 +36,10 @@ export function selectTui(
29
36
  }
30
37
 
31
38
  export function renderPrompt(params: { tui: TuiName; skill: string; args: string[] }): string {
32
- const suffix = [params.skill, ...params.args].join(' ').trim();
33
- if (params.tui === 'codex') {
34
- return renderAgentClientInvocation(getAgentClientAdapter('codex').invocation, {
35
- skillName: params.skill,
36
- args: params.args
37
- });
38
- }
39
- return `/${suffix}`;
39
+ return renderAgentClientInvocation(
40
+ getAgentClientAdapter(TUI_AGENT_CLIENT_IDS[params.tui]).invocation,
41
+ { skillName: params.skill, args: params.args }
42
+ );
40
43
  }
41
44
 
42
45
  export function buildTuiCommand(tui: TuiName, prompt: string): [string, string[]] {
@@ -51,6 +51,12 @@ type SandboxRuntimeCapabilityProjection = Readonly<{
51
51
  }>;
52
52
  repair?: AgentClientSandboxRecoveryCheck['repair'];
53
53
  }>[];
54
+ image: SandboxImageContribution;
55
+ }>;
56
+
57
+ type SandboxImageContribution = Readonly<{
58
+ dockerfileFragments: readonly string[];
59
+ dotfilesExclusions: readonly string[];
54
60
  }>;
55
61
 
56
62
  type PlannedAgentClientRecoveryCheck = Readonly<{
@@ -64,6 +70,7 @@ type SandboxCapabilityPlan = Readonly<{
64
70
  hooksByPhase: Readonly<Record<SandboxHookPhase, readonly AgentClientSandboxHook[]>>;
65
71
  recoveryChecks: readonly PlannedAgentClientRecoveryCheck[];
66
72
  aliases: readonly SandboxAlias[];
73
+ image: SandboxImageContribution;
67
74
  imageSignature: string;
68
75
  runtimeSignature: string;
69
76
  runtimeProjection: SandboxRuntimeCapabilityProjection;
@@ -147,6 +154,36 @@ function assertUniqueTools(tools: readonly SandboxTool[]): void {
147
154
  }
148
155
  }
149
156
 
157
+ function unique(values: readonly string[]): readonly string[] {
158
+ return Object.freeze([...new Set(values)]);
159
+ }
160
+
161
+ function sandboxImageContribution(
162
+ adapters: readonly AgentClientAdapter[],
163
+ tools: readonly SandboxTool[]
164
+ ): SandboxImageContribution {
165
+ const homePrefix = '/home/devuser/';
166
+ const derivedExclusions = tools.map((tool) => {
167
+ if (!tool.containerMount.startsWith(homePrefix)) {
168
+ throw new Error(
169
+ `Agent Client '${tool.id}' container mount must be below ${homePrefix.slice(0, -1)}`
170
+ );
171
+ }
172
+ return tool.containerMount.slice(homePrefix.length);
173
+ });
174
+ return Object.freeze({
175
+ dockerfileFragments: unique(adapters.flatMap((adapter) =>
176
+ adapter.sandbox.image?.dockerfileFragment
177
+ ? [adapter.sandbox.image.dockerfileFragment]
178
+ : []
179
+ )),
180
+ dotfilesExclusions: unique([
181
+ ...derivedExclusions,
182
+ ...adapters.flatMap((adapter) => adapter.sandbox.image?.dotfilesExclusions ?? [])
183
+ ])
184
+ });
185
+ }
186
+
150
187
  function hooksByPhase(
151
188
  adapters: readonly AgentClientAdapter[]
152
189
  ): SandboxCapabilityPlan['hooksByPhase'] {
@@ -207,6 +244,7 @@ function createSandboxCapabilityPlan(
207
244
 
208
245
  const tools = Object.freeze([...nonClientTools, ...selectedTools, ...selectedCustomTools]);
209
246
  assertUniqueTools(tools);
247
+ const image = sandboxImageContribution(selectedAgentClients, selectedTools);
210
248
  const phaseHooks = hooksByPhase(selectedAgentClients);
211
249
  const hookProjection = selectedAgentClients.flatMap((adapter) =>
212
250
  adapter.sandbox.hooks.map((hook) => ({
@@ -239,7 +277,8 @@ function createSandboxCapabilityPlan(
239
277
  tools: Object.freeze(tools.map(runtimeToolProjection)),
240
278
  selectedAgentClients: Object.freeze(selectedAgentClients.map((adapter) => adapter.id)),
241
279
  hooks: Object.freeze(hookProjection),
242
- recoveryChecks: recoveryCheckProjection
280
+ recoveryChecks: recoveryCheckProjection,
281
+ image
243
282
  });
244
283
  const cleanupInventory = Object.freeze([
245
284
  agentInfraTool(config.home),
@@ -256,6 +295,7 @@ function createSandboxCapabilityPlan(
256
295
  aliases: Object.freeze(
257
296
  listAgentClientAdapters().flatMap((adapter) => adapter.sandbox.aliases)
258
297
  ),
298
+ image,
259
299
  imageSignature: hash(tools.map(({ id, install }) => ({ id, install }))),
260
300
  runtimeSignature: hash(runtimeProjection),
261
301
  runtimeProjection,
@@ -367,12 +407,14 @@ export {
367
407
  DEFAULT_SANDBOX_HOOK_TIMEOUT_MS,
368
408
  createSandboxCapabilityPlan,
369
409
  runBoundedSandboxHookCommand,
370
- runSandboxHooks
410
+ runSandboxHooks,
411
+ sandboxImageContribution
371
412
  };
372
413
  export type {
373
414
  HookExecutionResult,
374
415
  SandboxCapabilityConfig,
375
416
  SandboxCapabilityPlan,
417
+ SandboxImageContribution,
376
418
  PlannedAgentClientRecoveryCheck,
377
419
  SandboxRuntimeCapabilityProjection
378
420
  };
@@ -11,10 +11,12 @@ import {
11
11
  startSandboxContainer,
12
12
  type SandboxRow
13
13
  } from './commands/list-running.ts';
14
+ import { resolveSandboxTarget, type SandboxWorkspaceIdentity } from './workspace-identity.ts';
14
15
 
15
16
  export type SandboxCaptureRequest = {
16
17
  taskRef: string;
17
18
  branch: string;
19
+ workspace?: SandboxWorkspaceIdentity;
18
20
  command: string[];
19
21
  timeoutMs?: number;
20
22
  recreate?: boolean;
@@ -44,6 +46,7 @@ export type SandboxCaptureOptions = {
44
46
  startContainer?: (name: string) => void;
45
47
  ensureReady?: (params: {
46
48
  branch: string;
49
+ workspace?: SandboxWorkspaceIdentity;
47
50
  row: SandboxRow;
48
51
  allowRecreate: boolean;
49
52
  }) => Promise<SandboxReadyResult>;
@@ -183,6 +186,8 @@ export async function runInSandbox(
183
186
  ): Promise<SandboxCaptureResult> {
184
187
  const config = options.engine ? null : loadConfig();
185
188
  const engine = options.engine ?? detectEngine(config!);
189
+ const workspace = request.workspace
190
+ ?? (config ? resolveSandboxTarget(request.taskRef, config.repoRoot).workspace : undefined);
186
191
  const rows =
187
192
  options.rows ??
188
193
  (() => {
@@ -200,6 +205,7 @@ export async function runInSandbox(
200
205
  if (options.ensureReady) {
201
206
  container = (await options.ensureReady({
202
207
  branch: request.branch,
208
+ workspace,
203
209
  row: found,
204
210
  allowRecreate: request.recreate ?? false
205
211
  })).container;
@@ -208,11 +214,12 @@ export async function runInSandbox(
208
214
  config,
209
215
  engine,
210
216
  branch: request.branch,
217
+ workspace,
211
218
  row: found,
212
219
  allowRecreate: request.recreate,
213
- recreate: async (targetBranch) => {
220
+ recreate: async () => {
214
221
  const { create } = await import('./commands/create.ts');
215
- await create([targetBranch, '--no-refresh']);
222
+ await create([request.taskRef, '--no-refresh']);
216
223
  }
217
224
  })).container;
218
225
  } else if (!found.running) {