@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  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/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -0,0 +1,345 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createGitHubClient } from '../platform/github-client.js';
4
+ import { enumerateAllTaskDirs } from '../task/resolve-ref.js';
5
+ import { captureText, inspectPrivacy, redactExcerpt } from './privacy.js';
6
+ import { canonicalJsonBytes, sha256 } from './store.js';
7
+ function endpointForPage(endpoint, page, perPage) {
8
+ const parsed = new URL(endpoint, 'https://api.github.invalid/');
9
+ if (parsed.searchParams.has('page') || parsed.searchParams.has('per_page')) {
10
+ throw new Error('endpoint must not predefine page or per_page');
11
+ }
12
+ parsed.searchParams.set('per_page', String(perPage));
13
+ parsed.searchParams.set('page', String(page));
14
+ return `${parsed.pathname.replace(/^\//, '')}${parsed.search}`;
15
+ }
16
+ function fetchRestCollection(options) {
17
+ const perPage = options.perPage ?? 100;
18
+ if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {
19
+ return { ok: false, error: { code: 'PAGINATION_INVALID', message: 'perPage must be between 1 and 100' } };
20
+ }
21
+ let pageIndex = 1;
22
+ let previousHash = null;
23
+ const identities = new Set();
24
+ const items = [];
25
+ const pages = [];
26
+ while (true) {
27
+ let url;
28
+ try {
29
+ url = endpointForPage(options.endpoint, pageIndex, perPage);
30
+ }
31
+ catch (error) {
32
+ return { ok: false, error: { code: 'PAGINATION_INVALID', message: String(error) } };
33
+ }
34
+ const response = options.client.json(['api', url]);
35
+ if (!response.ok)
36
+ return { ok: false, error: response.error };
37
+ if (!Array.isArray(response.value) || response.value.length > perPage) {
38
+ return {
39
+ ok: false,
40
+ error: { code: 'INVALID_PLATFORM_RESPONSE', message: `GitHub page ${pageIndex} is not a valid array page` }
41
+ };
42
+ }
43
+ let selected;
44
+ try {
45
+ selected = response.value.map((entry) => options.select ? options.select(entry) : entry);
46
+ }
47
+ catch (error) {
48
+ return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: String(error) } };
49
+ }
50
+ let bytes;
51
+ try {
52
+ bytes = canonicalJsonBytes(selected);
53
+ }
54
+ catch (error) {
55
+ return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: String(error) } };
56
+ }
57
+ const canonicalSha256 = sha256(bytes);
58
+ if (previousHash === canonicalSha256 && response.value.length > 0) {
59
+ return {
60
+ ok: false,
61
+ error: { code: 'PAGINATION_UNSTABLE', message: `GitHub page ${pageIndex} repeats the previous page` }
62
+ };
63
+ }
64
+ for (const entry of selected) {
65
+ const identity = options.identify(entry);
66
+ if (!identity || identities.has(identity)) {
67
+ return {
68
+ ok: false,
69
+ error: { code: 'PAGINATION_UNSTABLE', message: `duplicate or empty identity on page ${pageIndex}` }
70
+ };
71
+ }
72
+ identities.add(identity);
73
+ items.push(entry);
74
+ }
75
+ pages.push({ evidence: { index: pageIndex, itemCount: response.value.length, canonicalSha256 }, bytes });
76
+ previousHash = canonicalSha256;
77
+ if (response.value.length < perPage)
78
+ break;
79
+ pageIndex += 1;
80
+ }
81
+ const evidence = {
82
+ endpoint: options.endpoint,
83
+ requestCount: pages.length,
84
+ dataPageCount: pages.filter((page) => page.evidence.itemCount > 0).length,
85
+ itemCount: items.length,
86
+ termination: 'short-page',
87
+ pages: pages.map((page) => page.evidence)
88
+ };
89
+ return {
90
+ ok: true,
91
+ value: {
92
+ items,
93
+ evidence,
94
+ pageObjects: pages.map((page) => ({
95
+ sourceKind: 'github-rest',
96
+ sourceIdentity: `${options.endpoint}#page=${page.evidence.index}`,
97
+ sha256: page.evidence.canonicalSha256,
98
+ bytes: page.bytes.length,
99
+ content: page.bytes.toString('utf8'),
100
+ disposition: { state: 'included' }
101
+ }))
102
+ }
103
+ };
104
+ }
105
+ function relativeIdentity(repoRoot, filePath) {
106
+ const relative = path.relative(repoRoot, filePath);
107
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
108
+ throw new Error(`path escapes repository: ${filePath}`);
109
+ return relative.split(path.sep).join('/');
110
+ }
111
+ function readStableFile(repoRoot, filePath, sourceKind) {
112
+ const before = fs.lstatSync(filePath);
113
+ if (!before.isFile() || before.isSymbolicLink())
114
+ throw new Error(`unsafe source file: ${filePath}`);
115
+ const content = fs.readFileSync(filePath, 'utf8');
116
+ const after = fs.lstatSync(filePath);
117
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
118
+ throw new Error(`source changed while reading: ${filePath}`);
119
+ }
120
+ return captureText(sourceKind, relativeIdentity(repoRoot, filePath), content);
121
+ }
122
+ function listFiles(directory) {
123
+ if (!fs.existsSync(directory))
124
+ return [];
125
+ const output = [];
126
+ const walk = (current) => {
127
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
128
+ const candidate = path.join(current, entry.name);
129
+ if (entry.isSymbolicLink())
130
+ throw new Error(`symlink source is not allowed: ${candidate}`);
131
+ if (entry.isDirectory())
132
+ walk(candidate);
133
+ else if (entry.isFile())
134
+ output.push(candidate);
135
+ }
136
+ };
137
+ walk(directory);
138
+ return output;
139
+ }
140
+ function structuredReceipt(repoRoot, filePath) {
141
+ const raw = readStableFile(repoRoot, filePath, 'structured-telemetry');
142
+ if (!raw.content)
143
+ return raw;
144
+ try {
145
+ const parsed = JSON.parse(raw.content);
146
+ const fields = ['client', 'model', 'reasoning', 'stage', 'toolCategory', 'time', 'status', 'retry', 'error', 'fallback'];
147
+ const selected = Object.fromEntries(fields.filter((field) => parsed[field] !== undefined).map((field) => [field, jsonClone(parsed[field])]));
148
+ selected.sourceSha256 = raw.sha256;
149
+ return captureText('structured-telemetry', raw.sourceIdentity, canonicalJsonBytes(selected).toString('utf8'));
150
+ }
151
+ catch {
152
+ return { ...raw, content: undefined, disposition: { state: 'unavailable', reason: 'invalid receipt JSON' } };
153
+ }
154
+ }
155
+ function unavailableLog(repoRoot, filePath, includeExcerpts) {
156
+ const raw = readStableFile(repoRoot, filePath, 'local-file');
157
+ if (raw.disposition?.state === 'excluded-sensitive')
158
+ return raw;
159
+ const recognizedBody = /\/(?:sessions?|tools?)(?:\/|[-_])/i.test(raw.sourceIdentity);
160
+ if (includeExcerpts && recognizedBody && raw.content) {
161
+ const excerpt = redactExcerpt(raw.content);
162
+ if (excerpt !== null)
163
+ return captureText('local-file', `${raw.sourceIdentity}#excerpt`, excerpt);
164
+ }
165
+ return {
166
+ sourceKind: 'local-file',
167
+ sourceIdentity: raw.sourceIdentity,
168
+ sha256: raw.sha256,
169
+ bytes: raw.bytes,
170
+ disposition: { state: 'unavailable', reason: recognizedBody ? 'body-excerpts-disabled' : 'unknown-log-format' }
171
+ };
172
+ }
173
+ function collectLocalObjects(repoRoot, options = {}) {
174
+ try {
175
+ const objects = [];
176
+ for (const task of enumerateAllTaskDirs(repoRoot)) {
177
+ for (const filePath of listFiles(task.taskDir)) {
178
+ if (!/\.(?:md|json)$/i.test(filePath))
179
+ continue;
180
+ objects.push(readStableFile(repoRoot, filePath, 'local-file'));
181
+ }
182
+ }
183
+ const logsRoot = path.join(repoRoot, '.agents', 'workspace', 'logs');
184
+ for (const filePath of listFiles(logsRoot)) {
185
+ if (!/\.(?:md|json)$/i.test(filePath))
186
+ continue;
187
+ const identity = relativeIdentity(repoRoot, filePath);
188
+ if (/\/entropy-check\/.*\.md$/i.test(identity)) {
189
+ objects.push(readStableFile(repoRoot, filePath, 'operational-report'));
190
+ }
191
+ else if (/\/(?:delegation|orchestration)(?:\/|[-_]).*\.json$/i.test(identity)) {
192
+ objects.push(structuredReceipt(repoRoot, filePath));
193
+ }
194
+ else {
195
+ objects.push(unavailableLog(repoRoot, filePath, options.includeExcerpts === true));
196
+ }
197
+ }
198
+ return { ok: true, value: objects.sort((a, b) => a.sourceIdentity.localeCompare(b.sourceIdentity)) };
199
+ }
200
+ catch (error) {
201
+ return { ok: false, error: { code: 'LOCAL_SOURCE_UNSTABLE', message: error instanceof Error ? error.message : String(error) } };
202
+ }
203
+ }
204
+ function githubIdentity(item) {
205
+ if (!item || typeof item !== 'object')
206
+ return '';
207
+ const record = item;
208
+ for (const key of ['id', 'node_id', 'sha', 'number']) {
209
+ if (typeof record[key] === 'string' || typeof record[key] === 'number')
210
+ return `${key}:${String(record[key])}`;
211
+ }
212
+ return '';
213
+ }
214
+ function jsonClone(value) {
215
+ return JSON.parse(JSON.stringify(value));
216
+ }
217
+ function projectFields(value, fields) {
218
+ if (!value || typeof value !== 'object' || Array.isArray(value))
219
+ return undefined;
220
+ const record = value;
221
+ return Object.fromEntries(fields.filter((field) => record[field] !== undefined).map((field) => [field, jsonClone(record[field])]));
222
+ }
223
+ function projectSensitiveText(value) {
224
+ if (typeof value !== 'string')
225
+ return undefined;
226
+ const disposition = inspectPrivacy(value);
227
+ return disposition.state === 'included'
228
+ ? value
229
+ : { state: 'unavailable', reason: 'privacy-excluded', ruleId: disposition.ruleId, sha256: sha256(value) };
230
+ }
231
+ function projectGitHubItem(item) {
232
+ if (!item || typeof item !== 'object' || Array.isArray(item))
233
+ throw new Error('GitHub item must be an object');
234
+ const source = item;
235
+ const output = {};
236
+ const scalarFields = [
237
+ 'id', 'node_id', 'number', 'html_url', 'url', 'state', 'title', 'created_at', 'updated_at', 'closed_at',
238
+ 'merged_at', 'review_state', 'commit_id', 'sha', 'merge_commit_sha', 'milestone'
239
+ ];
240
+ for (const field of scalarFields) {
241
+ if (source[field] !== undefined)
242
+ output[field] = jsonClone(source[field]);
243
+ }
244
+ for (const field of ['user', 'head', 'base', 'author', 'committer']) {
245
+ const projected = projectFields(source[field], ['id', 'login', 'sha', 'ref']);
246
+ if (projected)
247
+ output[field] = projected;
248
+ }
249
+ for (const field of ['labels', 'assignees']) {
250
+ if (Array.isArray(source[field]))
251
+ output[field] = jsonClone(source[field]);
252
+ }
253
+ for (const field of ['body', 'message']) {
254
+ const projected = projectSensitiveText(source[field]);
255
+ if (projected !== undefined)
256
+ output[field] = projected;
257
+ }
258
+ if (Array.isArray(source.parents)) {
259
+ output.parents = source.parents.flatMap((parent) => {
260
+ const projected = projectFields(parent, ['sha']);
261
+ return projected ? [projected] : [];
262
+ });
263
+ }
264
+ const commit = projectFields(source.commit, []);
265
+ if (commit) {
266
+ const sourceCommit = source.commit;
267
+ const projectedCommit = {};
268
+ const message = projectSensitiveText(sourceCommit.message);
269
+ if (message !== undefined)
270
+ projectedCommit.message = message;
271
+ const tree = projectFields(sourceCommit.tree, ['sha']);
272
+ if (tree)
273
+ projectedCommit.tree = tree;
274
+ for (const field of ['author', 'committer']) {
275
+ const actor = projectFields(sourceCommit[field], ['name', 'email', 'date']);
276
+ if (actor)
277
+ projectedCommit[field] = actor;
278
+ }
279
+ output.commit = projectedCommit;
280
+ }
281
+ return output;
282
+ }
283
+ function collectGitHubObjects(repoRoot, client = createGitHubClient()) {
284
+ const repoResult = client.json(['repo', 'view', '--json', 'nameWithOwner'], { cwd: repoRoot });
285
+ if (!repoResult.ok)
286
+ return { ok: false, error: repoResult.error };
287
+ const repository = repoResult.value.nameWithOwner;
288
+ if (!repository || !/^[^/]+\/[^/]+$/.test(repository)) {
289
+ return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'GitHub repository identity is missing' } };
290
+ }
291
+ const objects = [];
292
+ const evidence = [];
293
+ const captureEndpoint = (endpoint) => {
294
+ const result = fetchRestCollection({
295
+ client,
296
+ endpoint,
297
+ identify: githubIdentity,
298
+ select: (item) => projectGitHubItem(item)
299
+ });
300
+ if (!result.ok)
301
+ return result;
302
+ objects.push(...result.value.pageObjects);
303
+ evidence.push(result.value.evidence);
304
+ return result;
305
+ };
306
+ const issues = captureEndpoint(`repos/${repository}/issues?state=all&sort=created&direction=asc`);
307
+ if (!issues.ok)
308
+ return issues;
309
+ const pulls = captureEndpoint(`repos/${repository}/pulls?state=all&sort=created&direction=asc`);
310
+ if (!pulls.ok)
311
+ return pulls;
312
+ for (const issue of issues.value.items) {
313
+ const number = issue.number;
314
+ if (typeof number !== 'number')
315
+ return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'Issue number is missing' } };
316
+ const comments = captureEndpoint(`repos/${repository}/issues/${number}/comments`);
317
+ if (!comments.ok)
318
+ return comments;
319
+ }
320
+ for (const pull of pulls.value.items) {
321
+ const record = pull;
322
+ const number = record.number;
323
+ const head = record.head;
324
+ const headSha = head?.sha;
325
+ if (typeof number !== 'number' || typeof headSha !== 'string') {
326
+ return { ok: false, error: { code: 'INVALID_PLATFORM_RESPONSE', message: 'Pull request identity or head SHA is missing' } };
327
+ }
328
+ for (const suffix of ['comments', 'reviews', 'commits']) {
329
+ const nested = captureEndpoint(`repos/${repository}/pulls/${number}/${suffix}`);
330
+ if (!nested.ok)
331
+ return nested;
332
+ }
333
+ const current = client.json(['api', `repos/${repository}/pulls/${number}`]);
334
+ if (!current.ok)
335
+ return { ok: false, error: current.error };
336
+ const projected = projectGitHubItem(current.value);
337
+ const currentHead = projected.head;
338
+ if (currentHead?.sha !== headSha) {
339
+ return { ok: false, error: { code: 'SOURCE_DRIFT', message: `Pull request ${number} head changed during capture` } };
340
+ }
341
+ }
342
+ return { ok: true, value: { repository, objects, endpoints: evidence } };
343
+ }
344
+ export { collectGitHubObjects, collectLocalObjects, fetchRestCollection, projectGitHubItem };
345
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1,282 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ function sha256(bytes) {
5
+ return crypto.createHash('sha256').update(bytes).digest('hex');
6
+ }
7
+ function compareCodePoints(left, right) {
8
+ const a = Array.from(left, (char) => char.codePointAt(0));
9
+ const b = Array.from(right, (char) => char.codePointAt(0));
10
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
11
+ if (a[index] !== b[index])
12
+ return a[index] - b[index];
13
+ }
14
+ return a.length - b.length;
15
+ }
16
+ function canonicalString(value, seen) {
17
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') {
18
+ return JSON.stringify(value);
19
+ }
20
+ if (typeof value === 'number') {
21
+ if (!Number.isFinite(value))
22
+ throw new Error('canonical JSON accepts only finite JSON values');
23
+ return JSON.stringify(value);
24
+ }
25
+ if (Array.isArray(value)) {
26
+ if (seen.has(value))
27
+ throw new Error('canonical JSON accepts only acyclic JSON values');
28
+ seen.add(value);
29
+ const output = `[${value.map((entry) => canonicalString(entry, seen)).join(',')}]`;
30
+ seen.delete(value);
31
+ return output;
32
+ }
33
+ if (typeof value === 'object') {
34
+ if (seen.has(value))
35
+ throw new Error('canonical JSON accepts only acyclic JSON values');
36
+ seen.add(value);
37
+ const record = value;
38
+ const fields = Object.keys(record)
39
+ .sort(compareCodePoints)
40
+ .map((key) => `${JSON.stringify(key)}:${canonicalString(record[key], seen)}`);
41
+ seen.delete(value);
42
+ return `{${fields.join(',')}}`;
43
+ }
44
+ throw new Error('canonical JSON accepts only JSON values');
45
+ }
46
+ function canonicalJsonBytes(value) {
47
+ return Buffer.from(canonicalString(value, new Set()), 'utf8');
48
+ }
49
+ function ensurePrivateDirectory(directory) {
50
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
51
+ if (process.platform !== 'win32')
52
+ fs.chmodSync(directory, 0o700);
53
+ }
54
+ function writeExclusive(filePath, bytes) {
55
+ ensurePrivateDirectory(path.dirname(filePath));
56
+ try {
57
+ const fd = fs.openSync(filePath, 'wx', 0o600);
58
+ try {
59
+ fs.writeFileSync(fd, bytes);
60
+ fs.fsyncSync(fd);
61
+ }
62
+ finally {
63
+ fs.closeSync(fd);
64
+ }
65
+ return true;
66
+ }
67
+ catch (error) {
68
+ if (error.code !== 'EEXIST')
69
+ throw error;
70
+ return false;
71
+ }
72
+ }
73
+ function createObjectStore(root) {
74
+ const objectRoot = path.join(root, 'objects', 'sha256');
75
+ ensurePrivateDirectory(objectRoot);
76
+ return {
77
+ pathFor(digest) {
78
+ if (!/^[a-f0-9]{64}$/.test(digest))
79
+ throw new Error(`invalid SHA-256: ${digest}`);
80
+ return path.join(objectRoot, digest.slice(0, 2), digest);
81
+ },
82
+ put(bytes) {
83
+ const digest = sha256(bytes);
84
+ const objectPath = path.join(objectRoot, digest.slice(0, 2), digest);
85
+ const created = writeExclusive(objectPath, bytes);
86
+ const stored = fs.readFileSync(objectPath);
87
+ if (sha256(stored) !== digest)
88
+ throw new Error(`CAS hash mismatch for ${digest}`);
89
+ return { sha256: digest, path: objectPath, created };
90
+ }
91
+ };
92
+ }
93
+ function dispositionCounts(objects) {
94
+ const counts = { included: 0, 'excluded-sensitive': 0, unavailable: 0 };
95
+ for (const object of objects)
96
+ counts[object.disposition?.state ?? 'included'] += 1;
97
+ return counts;
98
+ }
99
+ function jsonLine(value) {
100
+ return `${canonicalJsonBytes(value).toString('utf8')}\n`;
101
+ }
102
+ function writePrivateFile(filePath, content) {
103
+ fs.writeFileSync(filePath, content, { mode: 0o600, flag: 'wx' });
104
+ if (process.platform !== 'win32')
105
+ fs.chmodSync(filePath, 0o600);
106
+ }
107
+ function manifestDigest(manifest) {
108
+ return sha256(canonicalJsonBytes(manifest));
109
+ }
110
+ function publishSnapshot(root, input) {
111
+ ensurePrivateDirectory(root);
112
+ ensurePrivateDirectory(path.join(root, 'repairs'));
113
+ const date = new Date(input.observedTo);
114
+ if (!Number.isFinite(date.getTime()))
115
+ throw new Error('observedTo must be an ISO timestamp');
116
+ const identity = {
117
+ scope: input.scope,
118
+ repository: input.repository,
119
+ observedFrom: input.observedFrom,
120
+ observedTo: input.observedTo,
121
+ objects: input.objects,
122
+ endpoints: input.endpoints,
123
+ excerptsEnabled: input.excerptsEnabled,
124
+ dispositionCounts: dispositionCounts(input.objects),
125
+ recordCount: input.records.length,
126
+ findingCount: input.quality.length,
127
+ repairCount: input.repairs.length
128
+ };
129
+ const identityDigest = sha256(canonicalJsonBytes(identity));
130
+ const stamp = input.observedTo.replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
131
+ const snapshotId = `${stamp}-${identityDigest.slice(0, 12)}`;
132
+ const partialManifest = {
133
+ schema: 'raw-manifest/v1',
134
+ snapshotId,
135
+ ...identity,
136
+ privacyPolicyVersion: 'process-data-privacy/v1'
137
+ };
138
+ const manifest = { ...partialManifest, manifestSha256: manifestDigest(partialManifest) };
139
+ const stagingRoot = path.join(root, '.staging');
140
+ ensurePrivateDirectory(stagingRoot);
141
+ const staging = fs.mkdtempSync(path.join(stagingRoot, `${snapshotId}-`));
142
+ if (process.platform !== 'win32')
143
+ fs.chmodSync(staging, 0o700);
144
+ try {
145
+ writePrivateFile(path.join(staging, 'manifest.json'), jsonLine(manifest));
146
+ writePrivateFile(path.join(staging, 'normalized.jsonl'), input.records.map((record) => jsonLine({ schema: 'normalized/v1', ...record })).join(''));
147
+ writePrivateFile(path.join(staging, 'quality.json'), jsonLine({ schema: 'quality/v1', findings: input.quality }));
148
+ writePrivateFile(path.join(staging, 'repair-manifest.jsonl'), input.repairs.map((repair) => jsonLine({ schema: 'repair/v1', ...repair })).join(''));
149
+ const destination = path.join(root, 'snapshots', String(date.getUTCFullYear()), String(date.getUTCMonth() + 1).padStart(2, '0'), String(date.getUTCDate()).padStart(2, '0'), snapshotId);
150
+ ensurePrivateDirectory(path.dirname(destination));
151
+ if (fs.existsSync(destination)) {
152
+ const existing = fs.readFileSync(path.join(destination, 'manifest.json'), 'utf8');
153
+ if (existing !== jsonLine(manifest))
154
+ throw new Error(`snapshot collision: ${snapshotId}`);
155
+ fs.rmSync(staging, { recursive: true, force: true });
156
+ return { snapshotId, path: destination, created: false };
157
+ }
158
+ fs.renameSync(staging, destination);
159
+ return { snapshotId, path: destination, created: true };
160
+ }
161
+ catch (error) {
162
+ fs.rmSync(staging, { recursive: true, force: true });
163
+ throw error;
164
+ }
165
+ }
166
+ function findSnapshot(root, snapshotId) {
167
+ const snapshotRoot = path.join(root, 'snapshots');
168
+ if (!fs.existsSync(snapshotRoot))
169
+ return null;
170
+ const stack = [snapshotRoot];
171
+ while (stack.length > 0) {
172
+ const current = stack.pop();
173
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
174
+ if (!entry.isDirectory())
175
+ continue;
176
+ const candidate = path.join(current, entry.name);
177
+ if (entry.name === snapshotId && fs.existsSync(path.join(candidate, 'manifest.json')))
178
+ return candidate;
179
+ stack.push(candidate);
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+ function verifySnapshot(root, snapshotId) {
185
+ const snapshotPath = findSnapshot(root, snapshotId);
186
+ if (!snapshotPath)
187
+ return { ok: false, snapshotId, snapshotPath: null, errors: ['snapshot not found'] };
188
+ const errors = [];
189
+ const checkMode = (target, expected, label) => {
190
+ if (process.platform === 'win32' || !fs.existsSync(target))
191
+ return;
192
+ const actual = fs.statSync(target).mode & 0o777;
193
+ if (actual !== expected)
194
+ errors.push(`${label} permission mismatch: expected ${expected.toString(8)}, got ${actual.toString(8)}`);
195
+ };
196
+ checkMode(snapshotPath, 0o700, 'snapshot directory');
197
+ for (const file of ['manifest.json', 'normalized.jsonl', 'quality.json', 'repair-manifest.jsonl']) {
198
+ checkMode(path.join(snapshotPath, file), 0o600, file);
199
+ }
200
+ let manifest;
201
+ try {
202
+ manifest = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'manifest.json'), 'utf8'));
203
+ }
204
+ catch {
205
+ return { ok: false, snapshotId, snapshotPath, errors: ['manifest is invalid JSON'] };
206
+ }
207
+ if (manifest.schema !== 'raw-manifest/v1')
208
+ errors.push(`unsupported manifest schema: ${manifest.schema}`);
209
+ if (manifest.snapshotId !== snapshotId)
210
+ errors.push('snapshot identity mismatch');
211
+ if (typeof manifest.excerptsEnabled !== 'boolean')
212
+ errors.push('manifest excerpts opt-in is invalid');
213
+ if (!manifest.dispositionCounts || canonicalJsonBytes(manifest.dispositionCounts).toString('utf8') !== canonicalJsonBytes(dispositionCounts(manifest.objects)).toString('utf8')) {
214
+ errors.push('manifest disposition counts mismatch');
215
+ }
216
+ const { manifestSha256, ...partial } = manifest;
217
+ if (manifestDigest(partial) !== manifestSha256)
218
+ errors.push('manifest hash mismatch');
219
+ const store = createObjectStore(root);
220
+ try {
221
+ const lines = fs.readFileSync(path.join(snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean);
222
+ const ids = new Set();
223
+ for (const line of lines) {
224
+ const record = JSON.parse(line);
225
+ if (record.schema !== 'normalized/v1' || typeof record.recordId !== 'string') {
226
+ errors.push('normalized record schema is invalid');
227
+ continue;
228
+ }
229
+ if (ids.has(record.recordId))
230
+ errors.push(`duplicate normalized record: ${record.recordId}`);
231
+ ids.add(record.recordId);
232
+ }
233
+ if (lines.length !== manifest.recordCount)
234
+ errors.push('normalized record count mismatch');
235
+ const quality = JSON.parse(fs.readFileSync(path.join(snapshotPath, 'quality.json'), 'utf8'));
236
+ if (quality.schema !== 'quality/v1' || !Array.isArray(quality.findings))
237
+ errors.push('quality schema is invalid');
238
+ else if (quality.findings.length !== manifest.findingCount)
239
+ errors.push('quality finding count mismatch');
240
+ const repairLines = fs.readFileSync(path.join(snapshotPath, 'repair-manifest.jsonl'), 'utf8').split('\n').filter(Boolean);
241
+ for (const line of repairLines) {
242
+ const repair = JSON.parse(line);
243
+ if (repair.schema !== 'repair/v1' || typeof repair.repairId !== 'string')
244
+ errors.push('repair manifest schema is invalid');
245
+ }
246
+ if (repairLines.length !== manifest.repairCount)
247
+ errors.push('repair manifest count mismatch');
248
+ }
249
+ catch {
250
+ errors.push('snapshot derived files are missing or invalid');
251
+ }
252
+ for (const object of manifest.objects) {
253
+ if (object.disposition?.state && object.disposition.state !== 'included')
254
+ continue;
255
+ try {
256
+ const objectPath = store.pathFor(object.sha256);
257
+ checkMode(path.dirname(objectPath), 0o700, `object directory ${object.sha256.slice(0, 2)}`);
258
+ checkMode(objectPath, 0o600, `object ${object.sourceIdentity}`);
259
+ const bytes = fs.readFileSync(objectPath);
260
+ if (sha256(bytes) !== object.sha256)
261
+ errors.push(`object hash mismatch: ${object.sourceIdentity}`);
262
+ if (bytes.length !== object.bytes)
263
+ errors.push(`object byte count mismatch: ${object.sourceIdentity}`);
264
+ }
265
+ catch {
266
+ errors.push(`object missing: ${object.sourceIdentity}`);
267
+ }
268
+ }
269
+ for (const page of manifest.endpoints.flatMap((endpoint) => endpoint.pages)) {
270
+ try {
271
+ if (sha256(fs.readFileSync(store.pathFor(page.canonicalSha256))) !== page.canonicalSha256) {
272
+ errors.push(`page hash mismatch: ${page.index}`);
273
+ }
274
+ }
275
+ catch {
276
+ errors.push(`page object missing: ${page.canonicalSha256}`);
277
+ }
278
+ }
279
+ return { ok: errors.length === 0, snapshotId, snapshotPath, errors, manifest };
280
+ }
281
+ export { canonicalJsonBytes, createObjectStore, findSnapshot, publishSnapshot, sha256, verifySnapshot };
282
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.js';
4
- import { resolveTaskBranch } from '../sandbox/task-resolver.js';
4
+ import { resolveSandboxTarget } from '../sandbox/workspace-identity.js';
5
5
  import { createRunId, runInSandbox } from '../sandbox/capture.js';
6
6
  import { loadShortIdByTaskId, normalizeShortIdInput } from '../task/short-id.js';
7
7
  import { buildTuiCommand, renderPrompt, selectTui } from './tui.js';
@@ -101,7 +101,6 @@ function resolveActiveTaskIdentity(taskRef, repoRoot) {
101
101
  let resolvedTaskRef = taskRef;
102
102
  if (TASK_ID_RE.test(taskRef)) {
103
103
  taskId = taskRef;
104
- resolvedTaskRef = loadShortIdByTaskId(repoRoot).get(taskId) ?? taskRef;
105
104
  }
106
105
  else {
107
106
  const normalized = normalizeShortIdInput(taskRef, { shortIdLength: readShortIdLength(repoRoot) });
@@ -165,7 +164,8 @@ export async function runSkill(args, options = {}) {
165
164
  return result.exitCode ?? (result.signal ? 1 : 0);
166
165
  }
167
166
  const repoRoot = options.repoRoot ?? config?.repoRoot ?? process.cwd();
168
- const branch = resolveTaskBranch(parsed.taskRef, repoRoot);
167
+ const target = resolveSandboxTarget(parsed.taskRef, repoRoot);
168
+ const branch = target.branch;
169
169
  const identity = resolveActiveTaskIdentity(parsed.taskRef, repoRoot);
170
170
  const runId = identity ? createRunId() : undefined;
171
171
  const writeStdout = options.writeStdout ?? ((chunk) => process.stdout.write(chunk));
@@ -175,6 +175,7 @@ export async function runSkill(args, options = {}) {
175
175
  const result = await runSandbox({
176
176
  taskRef: parsed.taskRef,
177
177
  branch,
178
+ workspace: target.workspace,
178
179
  command,
179
180
  runId,
180
181
  recreate: parsed.recreate