@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.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -123
- package/dist/lib/agent-clients/adapter.js +92 -2
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +122 -18
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +109 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +103 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +363 -28
- package/dist/lib/task/resolve-ref.js +41 -1
- package/lib/agent-clients/adapter.ts +126 -2
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +81 -11
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +126 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +143 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +501 -33
- package/lib/task/resolve-ref.ts +46 -1
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- 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,
|
|
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
|
|
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;
|
package/lib/platform/releases.ts
CHANGED
|
@@ -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
|
|
52
|
-
|
|
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 };
|