@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
@@ -109,7 +109,7 @@ registerPlatformCapabilities('github', {
109
109
  inspectRequiredChecks({ client, repository, number, cwd }) {
110
110
  const github = (client as GitHubClient | undefined) || createGitHubClient();
111
111
  const inspected = github.json<unknown>([
112
- 'pr', 'checks', String(number), '--repo', repository, '--required',
112
+ 'pr', 'checks', String(number), '--repo', repository,
113
113
  '--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
114
114
  ], { cwd });
115
115
  return inspected.ok
@@ -19,6 +19,8 @@ import { planPullRequestMetadata } from './pull-request-metadata.ts';
19
19
  import { platformResult } from './types.ts';
20
20
  import type { PlatformOperation, PlatformResult } from './types.ts';
21
21
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
22
+ import { inspectCreatePrCommitGate } from '../task/commit-finalization.ts';
23
+ import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
22
24
 
23
25
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
24
26
 
@@ -747,13 +749,23 @@ function resolveExternalPullRequest(taskRef: string, options: ResolveExternalOpt
747
749
  }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
748
750
  }
749
751
 
750
- function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
752
+ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptions): PullRequestResult {
751
753
  const base = resolvedContext(taskRef, options);
752
754
  if (!base.ok) return base.output;
753
755
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
754
756
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
755
757
  });
756
758
  if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
759
+ const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
760
+ if (!gate.allowed) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
761
+ platform: base.context.platform,
762
+ capabilities: base.context.capabilities,
763
+ error: {
764
+ code: gate.code!,
765
+ message: `${gate.message}; action=${gate.action}`,
766
+ retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
767
+ }
768
+ });
757
769
  const located = locatePullRequest(base, options.head, options.base);
758
770
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
759
771
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
@@ -811,6 +823,42 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
811
823
  });
812
824
  }
813
825
 
826
+ function createPlatformPullRequest(taskRef: string, options: CreateOptions): PullRequestResult {
827
+ const base = resolvedContext(taskRef, options);
828
+ if (!base.ok) return base.output;
829
+ if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
830
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
831
+ error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
832
+ });
833
+ }
834
+ if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
835
+ try {
836
+ return withTaskExecutionLock(
837
+ base.resolved.repoRoot,
838
+ base.resolved.taskId,
839
+ 'platform-pr.create',
840
+ () => createPlatformPullRequestUnlocked(taskRef, options)
841
+ );
842
+ } catch (error) {
843
+ if (error instanceof TaskExecutionLockError) {
844
+ return result('blocked', base.resolved.taskId, base.issueNumber, null, {
845
+ platform: base.context.platform,
846
+ capabilities: base.context.capabilities,
847
+ error: { code: error.code, message: error.message, retryable: true }
848
+ });
849
+ }
850
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
851
+ platform: base.context.platform,
852
+ capabilities: base.context.capabilities,
853
+ error: {
854
+ code: 'PR_CREATE_FAILED',
855
+ message: error instanceof Error ? error.message : String(error),
856
+ retryable: false
857
+ }
858
+ });
859
+ }
860
+ }
861
+
814
862
  function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
815
863
  const base = resolvedContext(taskRef, options);
816
864
  if (!base.ok) return base.output;
@@ -48,18 +48,30 @@ function reconcileReleaseMilestones(version: string, options: ReleaseOptions = {
48
48
  if (!listed.ok) return { ...platformResult(listed.error.retryable ? 'blocked' : 'failed'), changed: false, operations: [], error: listed.error };
49
49
  const milestones = flattenPages(listed.value).map((item) => ({ title: String(item.title || ''), number: Number(item.number), state: String(item.state || '') }));
50
50
  const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]);
51
- const ensure = [`${major}.${minor}.${patch + 1}`, `${major}.${minor}.x`];
52
- if (patch === 0) ensure.push(`${major}.${minor + 1}.0`, `${major}.${minor + 1}.x`);
51
+ const nextPatch = `${major}.${minor}.${patch + 1}`;
52
+ const currentLine = `${major}.${minor}.x`;
53
+ const ensure = [
54
+ { title: nextPatch, description: `Issues that we want to release in v${nextPatch}.` },
55
+ { title: currentLine, description: `Issues that we want to resolve in ${major}.${minor} line.` }
56
+ ];
57
+ if (patch === 0) {
58
+ const nextMinor = `${major}.${minor + 1}.0`;
59
+ const nextLine = `${major}.${minor + 1}.x`;
60
+ ensure.push(
61
+ { title: nextMinor, description: `Issues that we want to release in v${nextMinor}.` },
62
+ { title: nextLine, description: `Issues that we want to resolve in ${major}.${minor + 1} line.` }
63
+ );
64
+ }
53
65
  const operations: Array<{ name: string; title: string; status: 'applied' | 'no-op' | 'failed' }> = [];
54
66
  const current = milestones.find((item) => item.title === version);
55
67
  if (current && current.state !== 'closed') {
56
68
  const closed = client.json(['api', '--method', 'PATCH', `repos/${context.platform.repository}/milestones/${current.number}`, '-f', 'state=closed'], { cwd: options.cwd });
57
69
  operations.push({ name: 'close-milestone', title: version, status: closed.ok ? 'applied' : 'failed' });
58
70
  } else operations.push({ name: 'close-milestone', title: version, status: 'no-op' });
59
- for (const title of ensure) {
71
+ for (const { title, description } of ensure) {
60
72
  if (milestones.some((item) => item.title === title)) operations.push({ name: 'ensure-milestone', title, status: 'no-op' });
61
73
  else {
62
- const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`], { cwd: options.cwd });
74
+ const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`, '-f', `description=${description}`], { cwd: options.cwd });
63
75
  operations.push({ name: 'ensure-milestone', title, status: created.ok ? 'applied' : 'failed' });
64
76
  }
65
77
  }
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ import { detectRepoRoot } from '../task/resolve-ref.ts';
6
+ import { normalizeObjects } from './normalize.ts';
7
+ import { reconcileRecords } from './reconcile.ts';
8
+ import { readAppliedOverlays, repairSnapshot } from './repair.ts';
9
+ import { collectGitHubObjects, collectLocalObjects } from './sources.ts';
10
+ import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.ts';
11
+
12
+ import type { CapturedObject, NormalizedRecord, RestCollectionEvidence } from './types.ts';
13
+
14
+ const USAGE = `Usage: ai data <command> [options]
15
+
16
+ Commands:
17
+ capture [--source all|local|github] [--root <dir>] [--include-excerpts]
18
+ verify <snapshot-id> [--root <dir>]
19
+ audit <snapshot-id> [--root <dir>] [--format json|text]
20
+ repair <snapshot-id> [--root <dir>] [--apply]
21
+ export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
22
+ `;
23
+
24
+ type ParsedArgs = { positionals: string[]; options: Map<string, string | true> };
25
+
26
+ function parseArgs(args: string[], valueOptions: Set<string>, booleanOptions: Set<string>): ParsedArgs {
27
+ const positionals: string[] = [];
28
+ const options = new Map<string, string | true>();
29
+ for (let index = 0; index < args.length; index += 1) {
30
+ const arg = args[index]!;
31
+ if (!arg.startsWith('--')) {
32
+ positionals.push(arg);
33
+ continue;
34
+ }
35
+ if (booleanOptions.has(arg)) {
36
+ if (options.has(arg)) throw new Error(`duplicate option: ${arg}`);
37
+ options.set(arg, true);
38
+ continue;
39
+ }
40
+ if (!valueOptions.has(arg)) throw new Error(`unknown option: ${arg}`);
41
+ const value = args[index + 1];
42
+ if (!value || value.startsWith('--')) throw new Error(`missing value for ${arg}`);
43
+ if (options.has(arg)) throw new Error(`duplicate option: ${arg}`);
44
+ options.set(arg, value);
45
+ index += 1;
46
+ }
47
+ return { positionals, options };
48
+ }
49
+
50
+ function dataRoot(repoRoot: string, requested?: string): string {
51
+ const workspace = path.resolve(repoRoot, '.agents', 'workspace');
52
+ const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
53
+ const relative = path.relative(repoRoot, resolved);
54
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('data root must be inside the repository');
55
+ const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
56
+ if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
57
+ throw new Error('data root cannot be the workspace root or a task directory');
58
+ }
59
+ let current = resolved;
60
+ while (current !== repoRoot) {
61
+ if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('data root cannot traverse a symlink');
62
+ current = path.dirname(current);
63
+ }
64
+ return resolved;
65
+ }
66
+
67
+ function repositoryIdentity(repoRoot: string): string {
68
+ try {
69
+ return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
70
+ } catch {
71
+ return path.basename(repoRoot);
72
+ }
73
+ }
74
+
75
+ function writeJson(value: unknown): void {
76
+ process.stdout.write(`${JSON.stringify(value)}\n`);
77
+ }
78
+
79
+ function optionString(parsed: ParsedArgs, name: string): string | undefined {
80
+ const value = parsed.options.get(name);
81
+ return typeof value === 'string' ? value : undefined;
82
+ }
83
+
84
+ function capture(args: string[]): number {
85
+ const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
86
+ if (parsed.positionals.length > 0) throw new Error('capture does not accept positional arguments');
87
+ const scope = optionString(parsed, '--source') ?? 'all';
88
+ if (!['all', 'local', 'github'].includes(scope)) throw new Error('--source must be all, local, or github');
89
+ const repoRoot = detectRepoRoot();
90
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
91
+ const observedFrom = new Date().toISOString();
92
+ const objects: CapturedObject[] = [];
93
+ const endpoints: RestCollectionEvidence[] = [];
94
+ let repository = repositoryIdentity(repoRoot);
95
+
96
+ if (scope === 'all' || scope === 'local') {
97
+ const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
98
+ if (!local.ok) { writeJson({ ok: false, error: local.error }); return 2; }
99
+ objects.push(...local.value);
100
+ }
101
+ if (scope === 'all' || scope === 'github') {
102
+ const github = collectGitHubObjects(repoRoot);
103
+ if (!github.ok) { writeJson({ ok: false, error: github.error }); return 2; }
104
+ objects.push(...github.value.objects);
105
+ endpoints.push(...github.value.endpoints);
106
+ repository = github.value.repository;
107
+ }
108
+
109
+ const store = createObjectStore(root);
110
+ for (const object of objects) {
111
+ if (object.content !== undefined && object.disposition?.state === 'included') {
112
+ const stored = store.put(Buffer.from(object.content, 'utf8'));
113
+ if (stored.sha256 !== object.sha256) throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
114
+ }
115
+ }
116
+ const records = normalizeObjects(objects);
117
+ const reconciliation = reconcileRecords(records, scope as 'all' | 'local' | 'github');
118
+ const observedTo = new Date().toISOString();
119
+ const published = publishSnapshot(root, {
120
+ scope: scope as 'all' | 'local' | 'github',
121
+ repository,
122
+ observedFrom,
123
+ observedTo,
124
+ objects: objects.map(({ content: _content, ...object }) => object),
125
+ endpoints,
126
+ excerptsEnabled: parsed.options.has('--include-excerpts'),
127
+ records,
128
+ quality: reconciliation.findings,
129
+ repairs: reconciliation.repairs
130
+ });
131
+ writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
132
+ return 0;
133
+ }
134
+
135
+ function verify(args: string[]): number {
136
+ const parsed = parseArgs(args, new Set(['--root']), new Set());
137
+ if (parsed.positionals.length !== 1) throw new Error('verify requires one snapshot id');
138
+ const repoRoot = detectRepoRoot();
139
+ const result = verifySnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]!);
140
+ writeJson(result);
141
+ return result.ok ? 0 : 1;
142
+ }
143
+
144
+ function audit(args: string[]): number {
145
+ const parsed = parseArgs(args, new Set(['--root', '--format']), new Set());
146
+ if (parsed.positionals.length !== 1) throw new Error('audit requires one snapshot id');
147
+ const format = optionString(parsed, '--format') ?? 'text';
148
+ if (!['json', 'text'].includes(format)) throw new Error('--format must be json or text');
149
+ const repoRoot = detectRepoRoot();
150
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
151
+ const verified = verifySnapshot(root, parsed.positionals[0]!);
152
+ if (!verified.ok || !verified.snapshotPath || !verified.manifest) { writeJson(verified); return 1; }
153
+ const quality = JSON.parse(fs.readFileSync(path.join(verified.snapshotPath, 'quality.json'), 'utf8')) as { findings: unknown[] };
154
+ const records = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean).map((line) => {
155
+ const { schema: _schema, ...record } = JSON.parse(line) as { schema: string } & NormalizedRecord;
156
+ return record;
157
+ });
158
+ const recomputed = reconcileRecords(records, verified.manifest.scope);
159
+ if (!canonicalJsonBytes(recomputed.findings).equals(canonicalJsonBytes(quality.findings))) {
160
+ writeJson({ ok: false, error: { code: 'QUALITY_MISMATCH', message: 'stored quality baseline does not match recomputed findings' } });
161
+ return 1;
162
+ }
163
+ if (format === 'json') writeJson({ ok: true, snapshotId: parsed.positionals[0], findings: recomputed.findings });
164
+ else process.stdout.write(`Snapshot ${parsed.positionals[0]}: ${quality.findings.length} findings\n`);
165
+ return 0;
166
+ }
167
+
168
+ function repair(args: string[]): number {
169
+ const parsed = parseArgs(args, new Set(['--root']), new Set(['--apply']));
170
+ if (parsed.positionals.length !== 1) throw new Error('repair requires one snapshot id');
171
+ const repoRoot = detectRepoRoot();
172
+ const result = repairSnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]!, parsed.options.has('--apply'));
173
+ writeJson({ ok: true, ...result });
174
+ return 0;
175
+ }
176
+
177
+ function exportSnapshot(args: string[]): number {
178
+ const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
179
+ if (parsed.positionals.length !== 1) throw new Error('export requires one snapshot id');
180
+ const repairMode = optionString(parsed, '--repairs') ?? 'none';
181
+ if (!['none', 'applied'].includes(repairMode)) throw new Error('--repairs must be none or applied');
182
+ const output = optionString(parsed, '--output') ?? '-';
183
+ const repoRoot = detectRepoRoot();
184
+ const root = dataRoot(repoRoot, optionString(parsed, '--root'));
185
+ const verified = verifySnapshot(root, parsed.positionals[0]!);
186
+ if (!verified.ok || !verified.snapshotPath) { writeJson(verified); return 1; }
187
+ let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
188
+ if (repairMode === 'applied') content += readAppliedOverlays(root, parsed.positionals[0]!);
189
+ if (output === '-') process.stdout.write(content);
190
+ else {
191
+ const destination = path.resolve(repoRoot, output);
192
+ const relative = path.relative(repoRoot, destination);
193
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('export output must be inside the repository');
194
+ fs.writeFileSync(destination, content, { flag: 'wx', mode: 0o600 });
195
+ writeJson({ ok: true, snapshotId: parsed.positionals[0], output: relative });
196
+ }
197
+ return 0;
198
+ }
199
+
200
+ async function cmdData(args: string[]): Promise<number> {
201
+ const [command, ...rest] = args;
202
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
203
+ process.stdout.write(USAGE);
204
+ return command ? 0 : 1;
205
+ }
206
+ try {
207
+ switch (command) {
208
+ case 'capture': return capture(rest);
209
+ case 'verify': return verify(rest);
210
+ case 'audit': return audit(rest);
211
+ case 'repair': return repair(rest);
212
+ case 'export': return exportSnapshot(rest);
213
+ default:
214
+ process.stderr.write(`Unknown data command: ${command}\n`);
215
+ process.stdout.write(USAGE);
216
+ return 1;
217
+ }
218
+ } catch (error) {
219
+ writeJson({ ok: false, error: { code: 'DATA_COMMAND_FAILED', message: error instanceof Error ? error.message : String(error) } });
220
+ return 1;
221
+ }
222
+ }
223
+
224
+ export { cmdData, dataRoot };
@@ -0,0 +1,202 @@
1
+ import { sha256 } from './store.ts';
2
+
3
+ import type { CapturedObject, JsonValue, NormalizedKind, NormalizedRecord } from './types.ts';
4
+
5
+ const TASK_ID_RE = /TASK-\d{8}-\d{6}/;
6
+ const ACTIVITY_RE = /^- (\d{4}-\d{2}-\d{2} [^ ]+) — \*\*(.+?)\*\* by ([^ ]+) — (.+)$/gm;
7
+ const LEDGER_RE = /^\|\s*((?:AN|PL|CD)-\d+)\s*\|\s*(analysis|plan|code)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|$/gm;
8
+ const RULING_RE = /^###\s+(HDR-\d+)\s*$([\s\S]*?)(?=^###\s+|^##\s+|(?![\s\S]))/gm;
9
+ const CONFLICT_STATUSES = new Set(['open', 'accepted', 'adjusted', 'refuted', 'cannot-judge', 'needs-human-decision']);
10
+
11
+ function recordId(kind: NormalizedKind, identity: string, suffix = ''): string {
12
+ return `${kind}:${sha256(`${identity}\0${suffix}`).slice(0, 24)}`;
13
+ }
14
+
15
+ function canonicalActor(raw: string): string | null {
16
+ const lowered = raw.toLowerCase();
17
+ const aliases: Record<string, string> = {
18
+ 'claude-code': 'claude',
19
+ 'antigravity-cli': 'antigravity',
20
+ 'gemini-cli': 'gemini',
21
+ gemini: 'gemini'
22
+ };
23
+ if (['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'human'].includes(lowered)) return lowered;
24
+ return aliases[lowered] ?? null;
25
+ }
26
+
27
+ function nextOccurrence(occurrences: Map<string, number>, identity: string): string {
28
+ const occurrence = (occurrences.get(identity) ?? 0) + 1;
29
+ occurrences.set(identity, occurrence);
30
+ return occurrence === 1 ? '' : `occurrence=${occurrence}`;
31
+ }
32
+
33
+ function normalizePlatformPage(object: CapturedObject): NormalizedRecord[] {
34
+ if (!object.content) return [];
35
+ try {
36
+ const page = JSON.parse(object.content) as unknown;
37
+ if (!Array.isArray(page)) return [];
38
+ return page.map((entry, index) => {
39
+ const value = entry as Record<string, unknown>;
40
+ const identity = String(value.id ?? value.node_id ?? value.sha ?? value.number ?? `${object.sourceIdentity}:${index}`);
41
+ const platformIdentity = /\/issues\?/.test(object.sourceIdentity) && typeof value.number === 'number'
42
+ ? `issue:${value.number}`
43
+ : /\/pulls\?/.test(object.sourceIdentity) && typeof value.number === 'number'
44
+ ? `pr:${value.number}`
45
+ : /\/issues\/\d+\/comments/.test(object.sourceIdentity)
46
+ ? `issue-comment:${identity}`
47
+ : /\/pulls\/\d+\/reviews/.test(object.sourceIdentity)
48
+ ? `review:${identity}`
49
+ : /\/pulls\/\d+\/commits/.test(object.sourceIdentity)
50
+ ? `commit:${identity}`
51
+ : `platform:${identity}`;
52
+ const binding = typeof value.body === 'string' ? value.body.match(TASK_ID_RE)?.[0] : undefined;
53
+ return {
54
+ recordId: recordId('platform-resource', object.sourceIdentity, identity),
55
+ kind: 'platform-resource',
56
+ sourceIdentity: platformIdentity,
57
+ sourceSha256: object.sha256,
58
+ ...(binding ? { binding } : {}),
59
+ data: entry as JsonValue
60
+ };
61
+ });
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ function normalizeObjects(objects: CapturedObject[]): NormalizedRecord[] {
68
+ const records: NormalizedRecord[] = [];
69
+ for (const object of objects) {
70
+ if (object.disposition?.state && object.disposition.state !== 'included') {
71
+ records.push({
72
+ recordId: recordId('missing', object.sourceIdentity),
73
+ kind: 'missing',
74
+ sourceIdentity: object.sourceIdentity,
75
+ sourceSha256: object.sha256,
76
+ data: object.disposition.state === 'excluded-sensitive'
77
+ ? { reason: 'privacy-excluded', ruleId: object.disposition.ruleId }
78
+ : { reason: object.disposition.reason }
79
+ });
80
+ continue;
81
+ }
82
+ if (object.sourceKind === 'github-rest') {
83
+ records.push(...normalizePlatformPage(object));
84
+ continue;
85
+ }
86
+ if (object.sourceKind === 'operational-report') {
87
+ records.push({
88
+ recordId: recordId('operational-report', object.sourceIdentity),
89
+ kind: 'operational-report',
90
+ sourceIdentity: object.sourceIdentity,
91
+ sourceSha256: object.sha256
92
+ });
93
+ continue;
94
+ }
95
+ if (object.sourceKind === 'structured-telemetry') {
96
+ let data: JsonValue = { availability: 'unknown' };
97
+ try {
98
+ data = JSON.parse(object.content ?? '{}') as JsonValue;
99
+ } catch {
100
+ data = { availability: 'unavailable', reason: 'invalid receipt JSON' };
101
+ }
102
+ records.push({
103
+ recordId: recordId('telemetry', object.sourceIdentity),
104
+ kind: 'telemetry',
105
+ sourceIdentity: object.sourceIdentity,
106
+ sourceSha256: object.sha256,
107
+ data
108
+ });
109
+ continue;
110
+ }
111
+
112
+ const taskId = object.sourceIdentity.match(TASK_ID_RE)?.[0];
113
+ if (/\/task\.md$/.test(object.sourceIdentity) && taskId) {
114
+ const activityOccurrences = new Map<string, number>();
115
+ const findingOccurrences = new Map<string, number>();
116
+ const issueNumber = object.content?.match(/^issue_number:\s*(\d+)$/m)?.[1];
117
+ const agentInfraVersion = object.content?.match(/^agent_infra_version:\s*(\S+)$/m)?.[1];
118
+ records.push({
119
+ recordId: recordId('task', taskId),
120
+ kind: 'task',
121
+ sourceIdentity: taskId,
122
+ sourceSha256: object.sha256,
123
+ ...(issueNumber ? { binding: `issue:${issueNumber}` } : {}),
124
+ data: {
125
+ agentInfraVersion: agentInfraVersion
126
+ ? { state: 'known', value: agentInfraVersion }
127
+ : { state: 'unknown', reason: 'legacy-task-missing-agent-infra-version' }
128
+ }
129
+ });
130
+ for (const match of object.content?.matchAll(ACTIVITY_RE) ?? []) {
131
+ const rawActor = match[3]!;
132
+ const identity = `${match[1]}:${match[2]}`;
133
+ const occurrence = nextOccurrence(activityOccurrences, identity);
134
+ records.push({
135
+ recordId: recordId('lifecycle-event', taskId, occurrence ? `${identity}\0${occurrence}` : identity),
136
+ kind: 'lifecycle-event',
137
+ sourceIdentity: `${taskId}#activity=${match[1]}`,
138
+ sourceSha256: object.sha256,
139
+ data: {
140
+ timestamp: match[1]!,
141
+ action: match[2]!,
142
+ actorRaw: rawActor,
143
+ actorCanonical: canonicalActor(rawActor),
144
+ note: match[4]!
145
+ }
146
+ });
147
+ }
148
+ for (const match of object.content?.matchAll(LEDGER_RE) ?? []) {
149
+ const findingId = match[1]!.trim();
150
+ const stage = match[2]!.trim();
151
+ const round = match[3]!.trim();
152
+ const severity = match[4]!.trim();
153
+ const status = match[5]!.trim();
154
+ const evidence = match[6]!.trim();
155
+ const identity = `${taskId}#finding=${findingId}`;
156
+ const occurrence = nextOccurrence(findingOccurrences, identity);
157
+ const data = { findingId, stage, round, severity, status, evidence };
158
+ records.push({
159
+ recordId: recordId('review-finding', identity, occurrence),
160
+ kind: 'review-finding',
161
+ sourceIdentity: identity,
162
+ sourceSha256: object.sha256,
163
+ binding: taskId,
164
+ data
165
+ });
166
+ if (status && CONFLICT_STATUSES.has(status)) {
167
+ records.push({
168
+ recordId: recordId('conflict', identity, occurrence),
169
+ kind: 'conflict',
170
+ sourceIdentity: `${identity}#status=${status}`,
171
+ sourceSha256: object.sha256,
172
+ binding: taskId,
173
+ data
174
+ });
175
+ }
176
+ }
177
+ for (const match of object.content?.matchAll(RULING_RE) ?? []) {
178
+ const rulingId = match[1]!;
179
+ const identity = `${taskId}#ruling=${rulingId}`;
180
+ records.push({
181
+ recordId: recordId('human-ruling', identity),
182
+ kind: 'human-ruling',
183
+ sourceIdentity: identity,
184
+ sourceSha256: object.sha256,
185
+ binding: taskId,
186
+ data: { rulingId, evidence: match[2]!.trim() }
187
+ });
188
+ }
189
+ continue;
190
+ }
191
+ records.push({
192
+ recordId: recordId('artifact', object.sourceIdentity),
193
+ kind: 'artifact',
194
+ sourceIdentity: object.sourceIdentity,
195
+ sourceSha256: object.sha256,
196
+ ...(taskId ? { binding: taskId } : {})
197
+ });
198
+ }
199
+ return records.sort((left, right) => left.recordId.localeCompare(right.recordId));
200
+ }
201
+
202
+ export { normalizeObjects };
@@ -0,0 +1,41 @@
1
+ import { sha256 } from './store.ts';
2
+
3
+ import type { CapturedObject, PrivacyDisposition, SourceKind } from './types.ts';
4
+
5
+ type ScannedDisposition =
6
+ | Extract<PrivacyDisposition, { state: 'included' }>
7
+ | Extract<PrivacyDisposition, { state: 'excluded-sensitive' }>;
8
+
9
+ const RULES = [
10
+ { id: 'private-key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
11
+ { id: 'github-token', pattern: /\b(?:gh[opusr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/ },
12
+ { id: 'bearer-token', pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i },
13
+ { id: 'aws-access-key', pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
14
+ { id: 'slack-token', pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/ },
15
+ { id: 'slack-webhook', pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/_-]{20,}/i },
16
+ { id: 'credential-field', pattern: /(?:api[_-]?key|password|secret)\s*[:=]\s*["']?[^\s"']{8,}/i }
17
+ ] as const;
18
+
19
+ function inspectPrivacy(content: string): ScannedDisposition {
20
+ for (const rule of RULES) {
21
+ if (rule.pattern.test(content)) return { state: 'excluded-sensitive', ruleId: rule.id };
22
+ }
23
+ return { state: 'included' };
24
+ }
25
+
26
+ function captureText(sourceKind: SourceKind, sourceIdentity: string, content: string): CapturedObject {
27
+ const bytes = Buffer.byteLength(content);
28
+ const digest = sha256(Buffer.from(content, 'utf8'));
29
+ const disposition = inspectPrivacy(content);
30
+ return disposition.state === 'included'
31
+ ? { sourceKind, sourceIdentity, sha256: digest, bytes, content, disposition }
32
+ : { sourceKind, sourceIdentity, sha256: digest, bytes, disposition };
33
+ }
34
+
35
+ function redactExcerpt(content: string, maxBytes = 4096): string | null {
36
+ if (inspectPrivacy(content).state !== 'included') return null;
37
+ const buffer = Buffer.from(content, 'utf8');
38
+ return buffer.subarray(0, maxBytes).toString('utf8').replace(/\b(token|password|secret)=\S+/gi, '$1=[REDACTED]');
39
+ }
40
+
41
+ export { captureText, inspectPrivacy, redactExcerpt };