@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
|
@@ -6,9 +6,10 @@ const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [inte
|
|
|
6
6
|
const FLAGS = {
|
|
7
7
|
'--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
|
|
8
8
|
'--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
|
|
9
|
-
'--status': 'status', '--artifact': 'artifact'
|
|
9
|
+
'--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation'
|
|
10
10
|
};
|
|
11
11
|
const NUMERIC = new Set(['ordinal', 'round']);
|
|
12
|
+
const BOOLEAN = new Set(['needsImplementation']);
|
|
12
13
|
function usageFailure(message) {
|
|
13
14
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LEDGER_PAYLOAD_INVALID', message } })}\n`);
|
|
14
15
|
process.stderr.write(USAGE);
|
|
@@ -55,8 +56,12 @@ function taskLedger(args = []) {
|
|
|
55
56
|
usageFailure(`option '${flag}' requires a value`);
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
59
|
+
if (BOOLEAN.has(key) && value !== 'true' && value !== 'false') {
|
|
60
|
+
usageFailure(`option '${flag}' must be true or false`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
58
63
|
seen.add(flag);
|
|
59
|
-
values[key] = NUMERIC.has(key) ? Number(value) : value;
|
|
64
|
+
values[key] = NUMERIC.has(key) ? Number(value) : BOOLEAN.has(key) ? value === 'true' : value;
|
|
60
65
|
}
|
|
61
66
|
const required = {
|
|
62
67
|
'finding-upsert': ['stage', 'reviewArtifact', 'ordinal', 'severity', 'evidence'],
|
|
@@ -66,7 +71,11 @@ function taskLedger(args = []) {
|
|
|
66
71
|
'decision-upsert': ['id', 'stage', 'artifact'],
|
|
67
72
|
'stage-status': ['stage']
|
|
68
73
|
};
|
|
69
|
-
const
|
|
74
|
+
const optional = {
|
|
75
|
+
'finding-review': ['needsImplementation'],
|
|
76
|
+
'decision-upsert': ['needsImplementation']
|
|
77
|
+
};
|
|
78
|
+
const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...(optional[kind] ?? [])]);
|
|
70
79
|
const unexpected = Object.keys(values).find((key) => !allowed.has(key));
|
|
71
80
|
const missing = required[kind].find((key) => values[key] === undefined);
|
|
72
81
|
if (unexpected) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
2
2
|
import { applyTaskLifecycle, lifecycleIntentCatalog } from '../task/lifecycle.js';
|
|
3
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
3
5
|
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\n`;
|
|
4
6
|
const FLAGS = {
|
|
5
7
|
'--agent': 'agent', '--reason': 'reason', '--unblock-condition': 'unblockCondition',
|
|
@@ -56,7 +58,23 @@ function taskLifecycle(args = []) {
|
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
60
|
values.agent = agent;
|
|
59
|
-
const
|
|
61
|
+
const resolved = resolveTaskRef(String(values.taskRef));
|
|
62
|
+
if (!resolved.ok) {
|
|
63
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: resolved.code, message: resolved.message } })}\n`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-lifecycle.${String(values.intent)}`, () => applyTaskLifecycle(values));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(error instanceof TaskExecutionLockError))
|
|
73
|
+
throw error;
|
|
74
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: error.code, message: error.message } })}\n`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
60
78
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
61
79
|
if (result.status === 'failed')
|
|
62
80
|
process.exitCode = 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
|
|
2
|
-
import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginOrResumeOrchestration,
|
|
2
|
+
import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, prepareOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration } from '../task/orchestration.js';
|
|
3
3
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
4
|
-
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|
|
|
4
|
+
const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-status> [options]\n';
|
|
5
5
|
function usageFailure(message) {
|
|
6
6
|
process.stdout.write(`${JSON.stringify({
|
|
7
7
|
status: 'failed', changed: false,
|
|
@@ -20,21 +20,35 @@ function taskOrchestration(args = []) {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const [taskRef, intent] = args;
|
|
23
|
-
if (![
|
|
23
|
+
if (![
|
|
24
|
+
'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
|
|
25
|
+
'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
|
|
26
|
+
].includes(intent)) {
|
|
24
27
|
usageFailure(`unknown intent '${intent}'`);
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
const values = {};
|
|
28
31
|
const seen = new Set();
|
|
32
|
+
let orchestrated = false;
|
|
29
33
|
for (let index = 2; index < args.length; index += 1) {
|
|
30
34
|
const flag = args[index];
|
|
35
|
+
if (flag === '--orchestrated') {
|
|
36
|
+
if (seen.has(flag)) {
|
|
37
|
+
usageFailure(`duplicate option '${flag}'`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
seen.add(flag);
|
|
41
|
+
orchestrated = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
31
44
|
if (![
|
|
32
45
|
'--max-steps', '--executor-model', '--executor-reasoning-effort',
|
|
33
46
|
'--reviewer-model', '--reviewer-reasoning-effort',
|
|
34
47
|
'--client', '--requested-model', '--requested-reasoning-effort', '--parent-id', '--before-fingerprint',
|
|
35
48
|
'--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
|
|
36
49
|
'--model-fallback-reason', '--reasoning-effort-fallback-reason',
|
|
37
|
-
'--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent'
|
|
50
|
+
'--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
|
|
51
|
+
'--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
38
52
|
].includes(flag)) {
|
|
39
53
|
usageFailure(`unknown option '${flag}'`);
|
|
40
54
|
return;
|
|
@@ -51,13 +65,116 @@ function taskOrchestration(args = []) {
|
|
|
51
65
|
}
|
|
52
66
|
values[flag] = value;
|
|
53
67
|
}
|
|
68
|
+
const commitIntentFlags = {
|
|
69
|
+
'commit-begin': ['--agent', '--baseline-head', '--orchestrated'],
|
|
70
|
+
'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref'],
|
|
71
|
+
'commit-complete': ['--token', '--agent'],
|
|
72
|
+
'commit-recover': ['--agent'],
|
|
73
|
+
'commit-abort': ['--token', '--expected-head'],
|
|
74
|
+
'commit-status': []
|
|
75
|
+
};
|
|
76
|
+
if (intent in commitIntentFlags) {
|
|
77
|
+
const allowed = commitIntentFlags[intent];
|
|
78
|
+
const invalid = [...seen].find((flag) => !allowed.includes(flag));
|
|
79
|
+
if (invalid) {
|
|
80
|
+
usageFailure(`intent '${intent}' does not accept '${invalid}'`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const commitOnly = [...seen].find((flag) => [
|
|
86
|
+
'--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
|
|
87
|
+
].includes(flag));
|
|
88
|
+
if (commitOnly) {
|
|
89
|
+
usageFailure(`intent '${intent}' does not accept '${commitOnly}'`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
54
93
|
const requireValues = (flags) => flags.find((flag) => values[flag] === undefined);
|
|
55
94
|
if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
|
|
56
95
|
usageFailure(`unknown client '${values['--client']}'`);
|
|
57
96
|
return;
|
|
58
97
|
}
|
|
59
98
|
let result;
|
|
60
|
-
if (intent === 'begin
|
|
99
|
+
if (intent === 'commit-begin') {
|
|
100
|
+
const missing = requireValues(['--agent', '--baseline-head']);
|
|
101
|
+
if (missing) {
|
|
102
|
+
usageFailure(`intent 'commit-begin' requires '${missing}'`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const agent = normalizeAgentToken(values['--agent']);
|
|
106
|
+
if (!agent) {
|
|
107
|
+
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
result = beginCommitIntent(taskRef, {
|
|
111
|
+
agent, orchestrated, baselineHead: values['--baseline-head']
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (intent === 'commit-checkpoint') {
|
|
115
|
+
const missing = requireValues(['--token', '--kind', '--head']);
|
|
116
|
+
if (missing) {
|
|
117
|
+
usageFailure(`intent 'commit-checkpoint' requires '${missing}'`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!['committed', 'pushed'].includes(values['--kind'])) {
|
|
121
|
+
usageFailure('--kind must be committed or pushed');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (values['--kind'] === 'pushed') {
|
|
125
|
+
const pushedMissing = requireValues(['--remote', '--ref']);
|
|
126
|
+
if (pushedMissing) {
|
|
127
|
+
usageFailure(`pushed checkpoint requires '${pushedMissing}'`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (values['--remote'] !== undefined || values['--ref'] !== undefined) {
|
|
132
|
+
usageFailure('committed checkpoint does not accept --remote or --ref');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
result = checkpointCommitIntent(taskRef, {
|
|
136
|
+
token: values['--token'], kind: values['--kind'],
|
|
137
|
+
head: values['--head'], remote: values['--remote'], ref: values['--ref']
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else if (intent === 'commit-complete') {
|
|
141
|
+
const missing = requireValues(['--token', '--agent']);
|
|
142
|
+
if (missing) {
|
|
143
|
+
usageFailure(`intent 'commit-complete' requires '${missing}'`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const agent = normalizeAgentToken(values['--agent']);
|
|
147
|
+
if (!agent) {
|
|
148
|
+
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
result = completeCommitIntent(taskRef, { token: values['--token'], agent });
|
|
152
|
+
}
|
|
153
|
+
else if (intent === 'commit-recover') {
|
|
154
|
+
const missing = requireValues(['--agent']);
|
|
155
|
+
if (missing) {
|
|
156
|
+
usageFailure(`intent 'commit-recover' requires '${missing}'`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const agent = normalizeAgentToken(values['--agent']);
|
|
160
|
+
if (!agent) {
|
|
161
|
+
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
result = recoverCommitIntent(taskRef, { agent });
|
|
165
|
+
}
|
|
166
|
+
else if (intent === 'commit-abort') {
|
|
167
|
+
const missing = requireValues(['--token', '--expected-head']);
|
|
168
|
+
if (missing) {
|
|
169
|
+
usageFailure(`intent 'commit-abort' requires '${missing}'`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] });
|
|
173
|
+
}
|
|
174
|
+
else if (intent === 'commit-status') {
|
|
175
|
+
result = statusCommitIntent(taskRef);
|
|
176
|
+
}
|
|
177
|
+
else if (intent === 'begin-or-resume') {
|
|
61
178
|
const missing = requireValues(['--client']);
|
|
62
179
|
if (missing) {
|
|
63
180
|
usageFailure(`intent 'begin-or-resume' requires '${missing}'`);
|
|
@@ -160,19 +277,6 @@ function taskOrchestration(args = []) {
|
|
|
160
277
|
});
|
|
161
278
|
}
|
|
162
279
|
}
|
|
163
|
-
else if (intent === 'stage-completed') {
|
|
164
|
-
const missing = requireValues(['--agent']);
|
|
165
|
-
if (missing) {
|
|
166
|
-
usageFailure(`intent 'stage-completed' requires '${missing}'`);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const agent = normalizeAgentToken(values['--agent']);
|
|
170
|
-
if (!agent) {
|
|
171
|
-
usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
result = completeCommitOrchestrationStage(taskRef, agent);
|
|
175
|
-
}
|
|
176
280
|
else if (intent === 'advance') {
|
|
177
281
|
result = advanceOrchestration(taskRef);
|
|
178
282
|
}
|
|
@@ -2,6 +2,14 @@ import process from 'node:process';
|
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
3
|
import semver from 'semver';
|
|
4
4
|
const MINIMUM_GITHUB_CLI_VERSION = '2.16.0';
|
|
5
|
+
const GITHUB_CLI_MAX_BUFFER = 64 * 1024 * 1024;
|
|
6
|
+
const ERROR_DETAIL_LIMIT = 4096;
|
|
7
|
+
function boundedFailureDetail(result) {
|
|
8
|
+
const diagnostic = `${result.stderr}\n${result.error?.message || ''}`.trim() || result.stdout.trim();
|
|
9
|
+
return diagnostic.length <= ERROR_DETAIL_LIMIT
|
|
10
|
+
? diagnostic
|
|
11
|
+
: `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
|
|
12
|
+
}
|
|
5
13
|
function defaultRunner(args, options) {
|
|
6
14
|
const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
|
|
7
15
|
let prefix = [];
|
|
@@ -16,6 +24,7 @@ function defaultRunner(args, options) {
|
|
|
16
24
|
encoding: 'utf8',
|
|
17
25
|
env: process.env,
|
|
18
26
|
input: options.input,
|
|
27
|
+
maxBuffer: GITHUB_CLI_MAX_BUFFER,
|
|
19
28
|
shell: false
|
|
20
29
|
});
|
|
21
30
|
return {
|
|
@@ -37,7 +46,15 @@ function defaultSleep(delayMs) {
|
|
|
37
46
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delayMs);
|
|
38
47
|
}
|
|
39
48
|
function classifyGitHubFailure(result) {
|
|
40
|
-
const
|
|
49
|
+
const errorCode = result.error && 'code' in result.error ? result.error.code : undefined;
|
|
50
|
+
if (errorCode === 'ENOBUFS') {
|
|
51
|
+
return {
|
|
52
|
+
code: 'PLATFORM_OUTPUT_TOO_LARGE',
|
|
53
|
+
message: 'GitHub CLI output exceeded the configured limit',
|
|
54
|
+
retryable: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const detail = boundedFailureDetail(result);
|
|
41
58
|
const lower = detail.toLowerCase();
|
|
42
59
|
if (/\b401\b|bad credentials|authentication required|not logged into/.test(lower)) {
|
|
43
60
|
return { code: 'AUTH_REQUIRED', message: detail || 'GitHub authentication is required', retryable: false };
|
|
@@ -54,7 +71,7 @@ function classifyGitHubFailure(result) {
|
|
|
54
71
|
if (/\b422\b|validation failed/.test(lower)) {
|
|
55
72
|
return { code: 'PLATFORM_REQUEST_INVALID', message: detail || 'GitHub rejected the request', retryable: false };
|
|
56
73
|
}
|
|
57
|
-
if (
|
|
74
|
+
if (errorCode === 'ENOENT') {
|
|
58
75
|
return { code: 'PLATFORM_DEPENDENCY_MISSING', message: detail || 'GitHub CLI is unavailable', retryable: false };
|
|
59
76
|
}
|
|
60
77
|
return { code: 'PLATFORM_REQUEST_FAILED', message: detail || 'GitHub request failed', retryable: false };
|
|
@@ -85,7 +85,7 @@ registerPlatformCapabilities('github', {
|
|
|
85
85
|
inspectRequiredChecks({ client, repository, number, cwd }) {
|
|
86
86
|
const github = client || createGitHubClient();
|
|
87
87
|
const inspected = github.json([
|
|
88
|
-
'pr', 'checks', String(number), '--repo', repository,
|
|
88
|
+
'pr', 'checks', String(number), '--repo', repository,
|
|
89
89
|
'--json', 'name,state,bucket,link,workflow,startedAt,completedAt'
|
|
90
90
|
], { cwd });
|
|
91
91
|
return inspected.ok
|
|
@@ -11,6 +11,8 @@ import { inspectPlatformIssue } from './issues.js';
|
|
|
11
11
|
import { planPullRequestMetadata } from './pull-request-metadata.js';
|
|
12
12
|
import { platformResult } from './types.js';
|
|
13
13
|
import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
|
|
14
|
+
import { inspectCreatePrCommitGate } from '../task/commit-finalization.js';
|
|
15
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
|
|
14
16
|
function result(status, taskId, issueNumber, prNumber, overrides = {}) {
|
|
15
17
|
return {
|
|
16
18
|
...platformResult(status),
|
|
@@ -612,7 +614,7 @@ function resolveExternalPullRequest(taskRef, options) {
|
|
|
612
614
|
error: null
|
|
613
615
|
}), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
|
|
614
616
|
}
|
|
615
|
-
function
|
|
617
|
+
function createPlatformPullRequestUnlocked(taskRef, options) {
|
|
616
618
|
const base = resolvedContext(taskRef, options);
|
|
617
619
|
if (!base.ok)
|
|
618
620
|
return base.output;
|
|
@@ -622,6 +624,17 @@ function createPlatformPullRequest(taskRef, options) {
|
|
|
622
624
|
});
|
|
623
625
|
if (base.prNumber)
|
|
624
626
|
return inspectPlatformPullRequest(taskRef, options);
|
|
627
|
+
const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
|
|
628
|
+
if (!gate.allowed)
|
|
629
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
630
|
+
platform: base.context.platform,
|
|
631
|
+
capabilities: base.context.capabilities,
|
|
632
|
+
error: {
|
|
633
|
+
code: gate.code,
|
|
634
|
+
message: `${gate.message}; action=${gate.action}`,
|
|
635
|
+
retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
|
|
636
|
+
}
|
|
637
|
+
});
|
|
625
638
|
const located = locatePullRequest(base, options.head, options.base);
|
|
626
639
|
if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
|
|
627
640
|
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
@@ -689,6 +702,39 @@ function createPlatformPullRequest(taskRef, options) {
|
|
|
689
702
|
operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }], error: null
|
|
690
703
|
});
|
|
691
704
|
}
|
|
705
|
+
function createPlatformPullRequest(taskRef, options) {
|
|
706
|
+
const base = resolvedContext(taskRef, options);
|
|
707
|
+
if (!base.ok)
|
|
708
|
+
return base.output;
|
|
709
|
+
if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
|
|
710
|
+
return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
|
|
711
|
+
error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
if (base.prNumber)
|
|
715
|
+
return inspectPlatformPullRequest(taskRef, options);
|
|
716
|
+
try {
|
|
717
|
+
return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, options));
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
if (error instanceof TaskExecutionLockError) {
|
|
721
|
+
return result('blocked', base.resolved.taskId, base.issueNumber, null, {
|
|
722
|
+
platform: base.context.platform,
|
|
723
|
+
capabilities: base.context.capabilities,
|
|
724
|
+
error: { code: error.code, message: error.message, retryable: true }
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return result('failed', base.resolved.taskId, base.issueNumber, null, {
|
|
728
|
+
platform: base.context.platform,
|
|
729
|
+
capabilities: base.context.capabilities,
|
|
730
|
+
error: {
|
|
731
|
+
code: 'PR_CREATE_FAILED',
|
|
732
|
+
message: error instanceof Error ? error.message : String(error),
|
|
733
|
+
retryable: false
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
692
738
|
function syncPlatformPullRequest(taskRef, options) {
|
|
693
739
|
const base = resolvedContext(taskRef, options);
|
|
694
740
|
if (!base.ok)
|
|
@@ -55,9 +55,17 @@ function reconcileReleaseMilestones(version, options = {}) {
|
|
|
55
55
|
const major = Number(match[1]);
|
|
56
56
|
const minor = Number(match[2]);
|
|
57
57
|
const patch = Number(match[3]);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const nextPatch = `${major}.${minor}.${patch + 1}`;
|
|
59
|
+
const currentLine = `${major}.${minor}.x`;
|
|
60
|
+
const ensure = [
|
|
61
|
+
{ title: nextPatch, description: `Issues that we want to release in v${nextPatch}.` },
|
|
62
|
+
{ title: currentLine, description: `Issues that we want to resolve in ${major}.${minor} line.` }
|
|
63
|
+
];
|
|
64
|
+
if (patch === 0) {
|
|
65
|
+
const nextMinor = `${major}.${minor + 1}.0`;
|
|
66
|
+
const nextLine = `${major}.${minor + 1}.x`;
|
|
67
|
+
ensure.push({ title: nextMinor, description: `Issues that we want to release in v${nextMinor}.` }, { title: nextLine, description: `Issues that we want to resolve in ${major}.${minor + 1} line.` });
|
|
68
|
+
}
|
|
61
69
|
const operations = [];
|
|
62
70
|
const current = milestones.find((item) => item.title === version);
|
|
63
71
|
if (current && current.state !== 'closed') {
|
|
@@ -66,11 +74,11 @@ function reconcileReleaseMilestones(version, options = {}) {
|
|
|
66
74
|
}
|
|
67
75
|
else
|
|
68
76
|
operations.push({ name: 'close-milestone', title: version, status: 'no-op' });
|
|
69
|
-
for (const title of ensure) {
|
|
77
|
+
for (const { title, description } of ensure) {
|
|
70
78
|
if (milestones.some((item) => item.title === title))
|
|
71
79
|
operations.push({ name: 'ensure-milestone', title, status: 'no-op' });
|
|
72
80
|
else {
|
|
73
|
-
const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`], { cwd: options.cwd });
|
|
81
|
+
const created = client.json(['api', '--method', 'POST', `repos/${context.platform.repository}/milestones`, '-f', `title=${title}`, '-f', `description=${description}`], { cwd: options.cwd });
|
|
74
82
|
operations.push({ name: 'ensure-milestone', title, status: created.ok ? 'applied' : 'failed' });
|
|
75
83
|
}
|
|
76
84
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { detectRepoRoot } from '../task/resolve-ref.js';
|
|
5
|
+
import { normalizeObjects } from './normalize.js';
|
|
6
|
+
import { reconcileRecords } from './reconcile.js';
|
|
7
|
+
import { readAppliedOverlays, repairSnapshot } from './repair.js';
|
|
8
|
+
import { collectGitHubObjects, collectLocalObjects } from './sources.js';
|
|
9
|
+
import { canonicalJsonBytes, createObjectStore, publishSnapshot, verifySnapshot } from './store.js';
|
|
10
|
+
const USAGE = `Usage: ai data <command> [options]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
capture [--source all|local|github] [--root <dir>] [--include-excerpts]
|
|
14
|
+
verify <snapshot-id> [--root <dir>]
|
|
15
|
+
audit <snapshot-id> [--root <dir>] [--format json|text]
|
|
16
|
+
repair <snapshot-id> [--root <dir>] [--apply]
|
|
17
|
+
export <snapshot-id> [--root <dir>] [--repairs none|applied] [--output <file|->]
|
|
18
|
+
`;
|
|
19
|
+
function parseArgs(args, valueOptions, booleanOptions) {
|
|
20
|
+
const positionals = [];
|
|
21
|
+
const options = new Map();
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
const arg = args[index];
|
|
24
|
+
if (!arg.startsWith('--')) {
|
|
25
|
+
positionals.push(arg);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (booleanOptions.has(arg)) {
|
|
29
|
+
if (options.has(arg))
|
|
30
|
+
throw new Error(`duplicate option: ${arg}`);
|
|
31
|
+
options.set(arg, true);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!valueOptions.has(arg))
|
|
35
|
+
throw new Error(`unknown option: ${arg}`);
|
|
36
|
+
const value = args[index + 1];
|
|
37
|
+
if (!value || value.startsWith('--'))
|
|
38
|
+
throw new Error(`missing value for ${arg}`);
|
|
39
|
+
if (options.has(arg))
|
|
40
|
+
throw new Error(`duplicate option: ${arg}`);
|
|
41
|
+
options.set(arg, value);
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return { positionals, options };
|
|
45
|
+
}
|
|
46
|
+
function dataRoot(repoRoot, requested) {
|
|
47
|
+
const workspace = path.resolve(repoRoot, '.agents', 'workspace');
|
|
48
|
+
const resolved = path.resolve(repoRoot, requested ?? path.join('.agents', 'workspace', 'process-data'));
|
|
49
|
+
const relative = path.relative(repoRoot, resolved);
|
|
50
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
51
|
+
throw new Error('data root must be inside the repository');
|
|
52
|
+
const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
|
|
53
|
+
if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
|
|
54
|
+
throw new Error('data root cannot be the workspace root or a task directory');
|
|
55
|
+
}
|
|
56
|
+
let current = resolved;
|
|
57
|
+
while (current !== repoRoot) {
|
|
58
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink())
|
|
59
|
+
throw new Error('data root cannot traverse a symlink');
|
|
60
|
+
current = path.dirname(current);
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
function repositoryIdentity(repoRoot) {
|
|
65
|
+
try {
|
|
66
|
+
return execFileSync('git', ['config', '--get', 'remote.origin.url'], { cwd: repoRoot, encoding: 'utf8' }).trim() || path.basename(repoRoot);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return path.basename(repoRoot);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function writeJson(value) {
|
|
73
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
74
|
+
}
|
|
75
|
+
function optionString(parsed, name) {
|
|
76
|
+
const value = parsed.options.get(name);
|
|
77
|
+
return typeof value === 'string' ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
function capture(args) {
|
|
80
|
+
const parsed = parseArgs(args, new Set(['--source', '--root']), new Set(['--include-excerpts']));
|
|
81
|
+
if (parsed.positionals.length > 0)
|
|
82
|
+
throw new Error('capture does not accept positional arguments');
|
|
83
|
+
const scope = optionString(parsed, '--source') ?? 'all';
|
|
84
|
+
if (!['all', 'local', 'github'].includes(scope))
|
|
85
|
+
throw new Error('--source must be all, local, or github');
|
|
86
|
+
const repoRoot = detectRepoRoot();
|
|
87
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
88
|
+
const observedFrom = new Date().toISOString();
|
|
89
|
+
const objects = [];
|
|
90
|
+
const endpoints = [];
|
|
91
|
+
let repository = repositoryIdentity(repoRoot);
|
|
92
|
+
if (scope === 'all' || scope === 'local') {
|
|
93
|
+
const local = collectLocalObjects(repoRoot, { includeExcerpts: parsed.options.has('--include-excerpts') });
|
|
94
|
+
if (!local.ok) {
|
|
95
|
+
writeJson({ ok: false, error: local.error });
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
objects.push(...local.value);
|
|
99
|
+
}
|
|
100
|
+
if (scope === 'all' || scope === 'github') {
|
|
101
|
+
const github = collectGitHubObjects(repoRoot);
|
|
102
|
+
if (!github.ok) {
|
|
103
|
+
writeJson({ ok: false, error: github.error });
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
objects.push(...github.value.objects);
|
|
107
|
+
endpoints.push(...github.value.endpoints);
|
|
108
|
+
repository = github.value.repository;
|
|
109
|
+
}
|
|
110
|
+
const store = createObjectStore(root);
|
|
111
|
+
for (const object of objects) {
|
|
112
|
+
if (object.content !== undefined && object.disposition?.state === 'included') {
|
|
113
|
+
const stored = store.put(Buffer.from(object.content, 'utf8'));
|
|
114
|
+
if (stored.sha256 !== object.sha256)
|
|
115
|
+
throw new Error(`source hash mismatch: ${object.sourceIdentity}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const records = normalizeObjects(objects);
|
|
119
|
+
const reconciliation = reconcileRecords(records, scope);
|
|
120
|
+
const observedTo = new Date().toISOString();
|
|
121
|
+
const published = publishSnapshot(root, {
|
|
122
|
+
scope: scope,
|
|
123
|
+
repository,
|
|
124
|
+
observedFrom,
|
|
125
|
+
observedTo,
|
|
126
|
+
objects: objects.map(({ content: _content, ...object }) => object),
|
|
127
|
+
endpoints,
|
|
128
|
+
excerptsEnabled: parsed.options.has('--include-excerpts'),
|
|
129
|
+
records,
|
|
130
|
+
quality: reconciliation.findings,
|
|
131
|
+
repairs: reconciliation.repairs
|
|
132
|
+
});
|
|
133
|
+
writeJson({ ok: true, snapshotId: published.snapshotId, scope, created: published.created, objects: objects.length, records: records.length });
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
function verify(args) {
|
|
137
|
+
const parsed = parseArgs(args, new Set(['--root']), new Set());
|
|
138
|
+
if (parsed.positionals.length !== 1)
|
|
139
|
+
throw new Error('verify requires one snapshot id');
|
|
140
|
+
const repoRoot = detectRepoRoot();
|
|
141
|
+
const result = verifySnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0]);
|
|
142
|
+
writeJson(result);
|
|
143
|
+
return result.ok ? 0 : 1;
|
|
144
|
+
}
|
|
145
|
+
function audit(args) {
|
|
146
|
+
const parsed = parseArgs(args, new Set(['--root', '--format']), new Set());
|
|
147
|
+
if (parsed.positionals.length !== 1)
|
|
148
|
+
throw new Error('audit requires one snapshot id');
|
|
149
|
+
const format = optionString(parsed, '--format') ?? 'text';
|
|
150
|
+
if (!['json', 'text'].includes(format))
|
|
151
|
+
throw new Error('--format must be json or text');
|
|
152
|
+
const repoRoot = detectRepoRoot();
|
|
153
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
154
|
+
const verified = verifySnapshot(root, parsed.positionals[0]);
|
|
155
|
+
if (!verified.ok || !verified.snapshotPath || !verified.manifest) {
|
|
156
|
+
writeJson(verified);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
const quality = JSON.parse(fs.readFileSync(path.join(verified.snapshotPath, 'quality.json'), 'utf8'));
|
|
160
|
+
const records = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
161
|
+
const { schema: _schema, ...record } = JSON.parse(line);
|
|
162
|
+
return record;
|
|
163
|
+
});
|
|
164
|
+
const recomputed = reconcileRecords(records, verified.manifest.scope);
|
|
165
|
+
if (!canonicalJsonBytes(recomputed.findings).equals(canonicalJsonBytes(quality.findings))) {
|
|
166
|
+
writeJson({ ok: false, error: { code: 'QUALITY_MISMATCH', message: 'stored quality baseline does not match recomputed findings' } });
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
if (format === 'json')
|
|
170
|
+
writeJson({ ok: true, snapshotId: parsed.positionals[0], findings: recomputed.findings });
|
|
171
|
+
else
|
|
172
|
+
process.stdout.write(`Snapshot ${parsed.positionals[0]}: ${quality.findings.length} findings\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
function repair(args) {
|
|
176
|
+
const parsed = parseArgs(args, new Set(['--root']), new Set(['--apply']));
|
|
177
|
+
if (parsed.positionals.length !== 1)
|
|
178
|
+
throw new Error('repair requires one snapshot id');
|
|
179
|
+
const repoRoot = detectRepoRoot();
|
|
180
|
+
const result = repairSnapshot(dataRoot(repoRoot, optionString(parsed, '--root')), parsed.positionals[0], parsed.options.has('--apply'));
|
|
181
|
+
writeJson({ ok: true, ...result });
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
function exportSnapshot(args) {
|
|
185
|
+
const parsed = parseArgs(args, new Set(['--root', '--repairs', '--output']), new Set());
|
|
186
|
+
if (parsed.positionals.length !== 1)
|
|
187
|
+
throw new Error('export requires one snapshot id');
|
|
188
|
+
const repairMode = optionString(parsed, '--repairs') ?? 'none';
|
|
189
|
+
if (!['none', 'applied'].includes(repairMode))
|
|
190
|
+
throw new Error('--repairs must be none or applied');
|
|
191
|
+
const output = optionString(parsed, '--output') ?? '-';
|
|
192
|
+
const repoRoot = detectRepoRoot();
|
|
193
|
+
const root = dataRoot(repoRoot, optionString(parsed, '--root'));
|
|
194
|
+
const verified = verifySnapshot(root, parsed.positionals[0]);
|
|
195
|
+
if (!verified.ok || !verified.snapshotPath) {
|
|
196
|
+
writeJson(verified);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
let content = fs.readFileSync(path.join(verified.snapshotPath, 'normalized.jsonl'), 'utf8');
|
|
200
|
+
if (repairMode === 'applied')
|
|
201
|
+
content += readAppliedOverlays(root, parsed.positionals[0]);
|
|
202
|
+
if (output === '-')
|
|
203
|
+
process.stdout.write(content);
|
|
204
|
+
else {
|
|
205
|
+
const destination = path.resolve(repoRoot, output);
|
|
206
|
+
const relative = path.relative(repoRoot, destination);
|
|
207
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
208
|
+
throw new Error('export output must be inside the repository');
|
|
209
|
+
fs.writeFileSync(destination, content, { flag: 'wx', mode: 0o600 });
|
|
210
|
+
writeJson({ ok: true, snapshotId: parsed.positionals[0], output: relative });
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
async function cmdData(args) {
|
|
215
|
+
const [command, ...rest] = args;
|
|
216
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
217
|
+
process.stdout.write(USAGE);
|
|
218
|
+
return command ? 0 : 1;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
switch (command) {
|
|
222
|
+
case 'capture': return capture(rest);
|
|
223
|
+
case 'verify': return verify(rest);
|
|
224
|
+
case 'audit': return audit(rest);
|
|
225
|
+
case 'repair': return repair(rest);
|
|
226
|
+
case 'export': return exportSnapshot(rest);
|
|
227
|
+
default:
|
|
228
|
+
process.stderr.write(`Unknown data command: ${command}\n`);
|
|
229
|
+
process.stdout.write(USAGE);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
writeJson({ ok: false, error: { code: 'DATA_COMMAND_FAILED', message: error instanceof Error ? error.message : String(error) } });
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export { cmdData, dataRoot };
|
|
239
|
+
//# sourceMappingURL=index.js.map
|