@fitlab-ai/agent-infra 0.11.6 → 0.11.8
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/dist/bin/internal-cli.js +8 -2
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/agent-clients/config.js +11 -3
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/git/primary-worktree.js +58 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/platform-pr-review.js +20 -3
- package/dist/lib/internal/task-operation-registry.js +3 -1
- package/dist/lib/internal/task-platform-recovery.js +56 -0
- package/dist/lib/merge.js +25 -5
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/in-label-sync.js +12 -1
- package/dist/lib/platform/issue-comments.js +58 -4
- package/dist/lib/platform/issues.js +105 -5
- package/dist/lib/platform/operation-coordinator.js +74 -0
- package/dist/lib/platform/pr-review.js +107 -1
- package/dist/lib/platform/pr-summary.js +79 -57
- package/dist/lib/platform/pull-requests.js +125 -7
- package/dist/lib/sandbox/commands/create.js +6 -3
- package/dist/lib/sandbox/commands/ls.js +4 -3
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +22 -4
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/git-metadata.js +50 -0
- package/dist/lib/sandbox/index.js +1 -1
- package/dist/lib/sandbox/mounts.js +12 -0
- package/dist/lib/sandbox/recovery.js +11 -0
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +29 -7
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/activity-intent.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +11 -5
- package/dist/lib/task/create.js +60 -79
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/agent-clients/config.ts +9 -3
- package/lib/defaults.json +6 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/task-operation-registry.ts +3 -1
- package/lib/internal/task-platform-recovery.ts +53 -0
- package/lib/merge.ts +26 -6
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/in-label-sync.ts +12 -0
- package/lib/platform/issue-comments.ts +59 -3
- package/lib/platform/issues.ts +103 -5
- package/lib/platform/operation-coordinator.ts +96 -0
- package/lib/platform/pr-review.ts +113 -1
- package/lib/platform/pr-summary.ts +22 -1
- package/lib/platform/pull-requests.ts +132 -8
- package/lib/sandbox/commands/create.ts +7 -8
- package/lib/sandbox/commands/ls.ts +4 -3
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +23 -5
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/git-metadata.ts +66 -0
- package/lib/sandbox/index.ts +1 -1
- package/lib/sandbox/mounts.ts +14 -0
- package/lib/sandbox/recovery.ts +13 -0
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +1 -1
- package/lib/sandbox/tools.ts +36 -7
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/activity-intent.ts +4 -3
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +12 -6
- package/lib/task/create.ts +54 -69
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +19 -17
- package/templates/.agents/README.zh-CN.md +18 -16
- package/templates/.agents/rules/README.en.md +2 -1
- package/templates/.agents/rules/README.zh-CN.md +2 -1
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
|
@@ -167,10 +167,16 @@ function normalizeAgentClients(
|
|
|
167
167
|
}
|
|
168
168
|
if (hasOwn(input, 'tuis')) fail('INVALID_AGENT_CLIENTS', 'tuis');
|
|
169
169
|
const sandbox = isRecord(input.sandbox) ? input.sandbox : undefined;
|
|
170
|
-
if (
|
|
171
|
-
|
|
170
|
+
if (sandbox && hasOwn(sandbox, 'customTools')) {
|
|
171
|
+
fail('INVALID_AGENT_CLIENTS', 'sandbox.customTools');
|
|
172
|
+
}
|
|
173
|
+
if (sandbox && hasOwn(sandbox, 'tools')) {
|
|
174
|
+
if (!isRecord(sandbox.tools)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools');
|
|
175
|
+
const tools = sandbox.tools;
|
|
176
|
+
if (!Array.isArray(tools.ids)) fail('INVALID_AGENT_CLIENTS', 'sandbox.tools.ids');
|
|
177
|
+
for (const [index, tool] of tools.ids.entries()) {
|
|
172
178
|
if (isAgentClientId(tool)) {
|
|
173
|
-
fail('INVALID_AGENT_CLIENTS', `sandbox.tools[${index}]`);
|
|
179
|
+
fail('INVALID_AGENT_CLIENTS', `sandbox.tools.ids[${index}]`);
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
}
|
package/lib/defaults.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { sameFilesystemEntry } from './worktree-identity.ts';
|
|
5
|
+
|
|
6
|
+
function gitText(repositoryRoot: string, args: string[]): string {
|
|
7
|
+
return execFileSync('git', ['-C', repositoryRoot, ...args], {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
10
|
+
}).replace(/\r?\n$/, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPrimaryWorktree(candidate: string, expectedCommonDirectory: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const root = fs.realpathSync.native(candidate);
|
|
16
|
+
if (!fs.statSync(root).isDirectory()) return false;
|
|
17
|
+
if (gitText(root, ['rev-parse', '--show-prefix']) !== '') return false;
|
|
18
|
+
|
|
19
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
20
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
21
|
+
return sameFilesystemEntry(commonDirectory, expectedCommonDirectory) &&
|
|
22
|
+
sameFilesystemEntry(gitDirectory, expectedCommonDirectory);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function worktreePaths(repositoryRoot: string): string[] {
|
|
29
|
+
const output = execFileSync('git', ['-C', repositoryRoot, 'worktree', 'list', '--porcelain', '-z'], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return output
|
|
35
|
+
.split('\0\0')
|
|
36
|
+
.flatMap((record) => record.split('\0'))
|
|
37
|
+
.filter((field) => field.startsWith('worktree '))
|
|
38
|
+
.map((field) => field.slice('worktree '.length));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolvePrimaryWorktreeRoot(repositoryRoot: string): string {
|
|
42
|
+
try {
|
|
43
|
+
const root = fs.realpathSync.native(repositoryRoot);
|
|
44
|
+
if (!fs.statSync(root).isDirectory() || gitText(root, ['rev-parse', '--show-prefix']) !== '') {
|
|
45
|
+
throw new Error('repository root is not a worktree root');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const commonDirectory = gitText(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
49
|
+
const gitDirectory = gitText(root, ['rev-parse', '--absolute-git-dir']);
|
|
50
|
+
if (sameFilesystemEntry(gitDirectory, commonDirectory)) return root;
|
|
51
|
+
|
|
52
|
+
const matches = worktreePaths(root).filter((candidate) => isPrimaryWorktree(candidate, commonDirectory));
|
|
53
|
+
if (matches.length !== 1) {
|
|
54
|
+
throw new Error(matches.length === 0 ? 'primary worktree metadata was not found' : 'primary worktree metadata is ambiguous');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fs.realpathSync.native(matches[0]!);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
60
|
+
throw new Error(`server: unable to locate primary worktree: ${detail}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -63,6 +63,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
|
|
|
63
63
|
'task-invalidation': ['reconcile'],
|
|
64
64
|
'task-lifecycle': ['intent'],
|
|
65
65
|
'task-finalization': ['complete'],
|
|
66
|
+
'task-platform-recovery': ['inspect', 'recover'],
|
|
66
67
|
'task-short-id': ['list', 'list-verify', 'alloc', 'release', 'resolve'],
|
|
67
68
|
'task-snapshot': ['snapshot'],
|
|
68
69
|
'task-verify': ['event'],
|
|
@@ -89,6 +90,7 @@ export function internalRouteSelector(command: string, args: readonly string[]):
|
|
|
89
90
|
if (command === 'task-qualification') return args[1] ?? '';
|
|
90
91
|
if (command === 'task-lifecycle') return args[1] ? 'intent' : '';
|
|
91
92
|
if (command === 'task-finalization') return args[1] === 'complete' ? 'complete' : '';
|
|
93
|
+
if (command === 'task-platform-recovery') return args[1] ?? '';
|
|
92
94
|
if (command === 'task-event') return args[1] ? 'event' : '';
|
|
93
95
|
if (command === 'task-verify') return args[0] && args[1] ? 'event' : '';
|
|
94
96
|
if (command === 'task-snapshot') return 'snapshot';
|
|
@@ -6,14 +6,15 @@ import { listPrReviews, publishPrReview } from '../platform/pr-review.ts';
|
|
|
6
6
|
import type { PrReviewEvent } from '../platform/pr-review.ts';
|
|
7
7
|
import type { PlatformResult } from '../platform/types.ts';
|
|
8
8
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
9
|
+
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
9
10
|
|
|
10
11
|
const USAGE = `Usage: agent-infra-internal platform-pr-review inspect --pr <token> [--cwd <path>]
|
|
11
12
|
agent-infra-internal platform-pr-review list --pr <token> [--cwd <path>]
|
|
12
|
-
agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--dry-run] [--cwd <path>]
|
|
13
|
+
agent-infra-internal platform-pr-review publish --pr <token> --scope <taskId|pr{N}> --round <N> --commit <sha> --event <COMMENT|APPROVE|REQUEST_CHANGES> --body-file <path|-> [--artifact <pr-review-artifact>] [--agent <agent>] [--dry-run] [--cwd <path>]
|
|
13
14
|
`;
|
|
14
15
|
|
|
15
16
|
const BOOLEAN_FLAGS = new Set(['--dry-run']);
|
|
16
|
-
const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file']);
|
|
17
|
+
const VALUE_FLAGS = new Set(['--cwd', '--pr', '--scope', '--round', '--commit', '--event', '--body-file', '--artifact', '--agent']);
|
|
17
18
|
|
|
18
19
|
function key(flag: string): string {
|
|
19
20
|
return flag.slice(2).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
@@ -67,7 +68,7 @@ async function platformPrReview(args: string[] = []): Promise<void> {
|
|
|
67
68
|
const allowed: Record<string, string[]> = {
|
|
68
69
|
inspect: ['cwd', 'pr'],
|
|
69
70
|
list: ['cwd', 'pr'],
|
|
70
|
-
publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'dryRun']
|
|
71
|
+
publish: ['cwd', 'pr', 'scope', 'round', 'commit', 'event', 'bodyFile', 'artifact', 'agent', 'dryRun']
|
|
71
72
|
};
|
|
72
73
|
const unexpected = Object.keys(values).find((name) => !allowed[operation]!.includes(name));
|
|
73
74
|
if (unexpected) { fail(`${operation} does not accept --${unexpected}`); return; }
|
|
@@ -86,6 +87,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
|
|
|
86
87
|
const round = Number(values.round);
|
|
87
88
|
const commit = typeof values.commit === 'string' ? values.commit : '';
|
|
88
89
|
const event = values.event as string;
|
|
90
|
+
const rawAgent = typeof values.agent === 'string' ? values.agent : '';
|
|
91
|
+
const agent = rawAgent ? normalizeAgentToken(rawAgent) : null;
|
|
89
92
|
if (!scope || !/^(?:pr\d+|TASK-\d{8}-\d{6})$/.test(scope)) { fail('publish requires --scope <taskId|pr{N}>'); return; }
|
|
90
93
|
if (!internalHandlerRoute('platform-pr-review', 'publish-task', publishSelector)
|
|
91
94
|
&& !internalHandlerRoute('platform-pr-review', 'publish-pr', publishSelector)) {
|
|
@@ -95,6 +98,12 @@ async function platformPrReview(args: string[] = []): Promise<void> {
|
|
|
95
98
|
if (!/^[0-9a-f]{7,40}$/i.test(commit)) { fail('publish requires --commit <sha>'); return; }
|
|
96
99
|
if (!['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(event)) { fail('publish requires --event <COMMENT|APPROVE|REQUEST_CHANGES>'); return; }
|
|
97
100
|
if (typeof values.bodyFile !== 'string') { fail('publish requires --body-file'); return; }
|
|
101
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(scope) && typeof values.artifact !== 'string') { fail('task-scoped publish requires --artifact <pr-review-artifact>'); return; }
|
|
102
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(scope) && !agent) {
|
|
103
|
+
fail(`task-scoped publish requires --agent <agent>${rawAgent ? `: invalid '${rawAgent}'` : ''}: ${AGENT_USAGE_HINT}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (rawAgent && !agent) { fail(`invalid --agent '${rawAgent}': ${AGENT_USAGE_HINT}`); return; }
|
|
98
107
|
let body: string;
|
|
99
108
|
try {
|
|
100
109
|
body = readBodyFile(values.bodyFile, cwd);
|
|
@@ -108,6 +117,8 @@ async function platformPrReview(args: string[] = []): Promise<void> {
|
|
|
108
117
|
identity: { scope, round, commitSha: commit },
|
|
109
118
|
event: event as PrReviewEvent,
|
|
110
119
|
body,
|
|
120
|
+
...(typeof values.artifact === 'string' ? { recoveryArtifact: values.artifact } : {}),
|
|
121
|
+
...(agent ? { agent } : {}),
|
|
111
122
|
dryRun: values.dryRun === true
|
|
112
123
|
}));
|
|
113
124
|
}
|
|
@@ -145,6 +145,8 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
|
|
|
145
145
|
descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
|
|
146
146
|
descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
|
|
147
147
|
descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
|
|
148
|
+
descriptor('internal', 'task-platform-recovery', 'inspect', 'task-bound', 'diagnostic'),
|
|
149
|
+
descriptor('internal', 'task-platform-recovery', 'recover', 'task-bound', 'recovery'),
|
|
148
150
|
descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
|
|
149
151
|
descriptor('internal', 'task-short-id', 'list-verify', 'conditional', 'diagnostic'),
|
|
150
152
|
descriptor('internal', 'task-short-id', 'alloc', 'task-bound', 'progress'),
|
|
@@ -221,7 +223,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
|
|
|
221
223
|
'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
|
|
222
224
|
'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
|
|
223
225
|
'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
|
|
224
|
-
'task-finalization', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
|
|
226
|
+
'task-finalization', 'task-platform-recovery', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
|
|
225
227
|
]);
|
|
226
228
|
export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
|
|
227
229
|
'agent-client', 'cp', 'data', 'decide', 'help', 'init', 'merge', 'run', 'sandbox', 'server', 'task', 'sync', 'update', 'version'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
|
|
2
|
+
import { readPlatformOperationJournal } from '../task/platform-operation-journal.ts';
|
|
3
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
|
|
4
|
+
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
5
|
+
|
|
6
|
+
const USAGE = 'Usage: agent-infra-internal task-platform-recovery <N | TASK-id> <inspect | recover> [--agent <agent>] [--selection required|deferred|all]\n';
|
|
7
|
+
|
|
8
|
+
function option(args: readonly string[], name: string): string | undefined {
|
|
9
|
+
const index = args.indexOf(name);
|
|
10
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function taskPlatformRecovery(args: string[] = []): Promise<void> {
|
|
14
|
+
if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(USAGE); return; }
|
|
15
|
+
if (!ensureInternalHandlerRoute('task-platform-recovery', args)) return;
|
|
16
|
+
const action = args[1];
|
|
17
|
+
const registered = action === 'inspect'
|
|
18
|
+
? internalHandlerRoute('task-platform-recovery', 'inspect', action)
|
|
19
|
+
: action === 'recover' && internalHandlerRoute('task-platform-recovery', 'recover', action);
|
|
20
|
+
if (!registered) return;
|
|
21
|
+
const repoRoot = detectRepoRoot();
|
|
22
|
+
const task = resolveTaskRef(args[0] ?? '', { repoRoot });
|
|
23
|
+
if (!task.ok) {
|
|
24
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: task.code, message: task.message } })}\n`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (action === 'inspect') {
|
|
29
|
+
try {
|
|
30
|
+
const journal = readPlatformOperationJournal(task.taskId, repoRoot);
|
|
31
|
+
process.stdout.write(`${JSON.stringify({ status: 'ready', changed: false, taskId: task.taskId, journal })}\n`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const value = error as { code?: string; message?: string };
|
|
34
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: value.code || 'PLATFORM_OPERATION_JOURNAL_INVALID', message: value.message || String(error) } })}\n`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const agent = option(args, '--agent');
|
|
40
|
+
const selection = option(args, '--selection') ?? 'all';
|
|
41
|
+
if (!agent || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(agent)
|
|
42
|
+
|| !['required', 'deferred', 'all'].includes(selection)) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'PLATFORM_OPERATION_RECOVERY_PAYLOAD_INVALID', message: 'recover requires a valid --agent and --selection' } })}\n`);
|
|
44
|
+
process.stderr.write(USAGE);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const result = await recoverPlatformOperations(task.taskId, selection as 'required' | 'deferred' | 'all', { agent, cwd: repoRoot });
|
|
49
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
50
|
+
process.exitCode = result.status === 'applied' || result.status === 'no-op' ? 0 : result.status === 'blocked' ? 2 : 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { taskPlatformRecovery };
|
package/lib/merge.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
5
|
import { removeDirRecursive } from './remove-dir.ts';
|
|
@@ -424,6 +425,24 @@ function formatBackupTimestamp(date: Date): string {
|
|
|
424
425
|
].join('') + `-${String(date.getHours()).padStart(2, '0')}${String(date.getMinutes()).padStart(2, '0')}${String(date.getSeconds()).padStart(2, '0')}`;
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
function resolveRecoveryProjectKey(repoRoot: string): string {
|
|
429
|
+
let projectKey: string | undefined;
|
|
430
|
+
try {
|
|
431
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
|
|
432
|
+
if (typeof config.project === 'string' && config.project.trim() !== '') {
|
|
433
|
+
projectKey = config.project.trim();
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
// Fall back to the repository directory when project config is unavailable.
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
projectKey ??= path.basename(repoRoot);
|
|
440
|
+
if (!/^[A-Za-z0-9_.@-]+$/.test(projectKey) || projectKey === '.' || projectKey === '..') {
|
|
441
|
+
throw new Error(`Invalid project name for recovery backups: ${projectKey}`);
|
|
442
|
+
}
|
|
443
|
+
return projectKey;
|
|
444
|
+
}
|
|
445
|
+
|
|
427
446
|
function toPosixPath(relativePath: string): string {
|
|
428
447
|
return relativePath.split(path.sep).join('/');
|
|
429
448
|
}
|
|
@@ -541,7 +560,7 @@ function buildWorkspaceIndex(workspaceDir: string): Map<string, WorkspaceRecord>
|
|
|
541
560
|
function backupTaskDir(backupRoot: string, section: MutableSection, taskDir: string, taskId: string): string {
|
|
542
561
|
const backupDir = path.join(backupRoot, section, taskId);
|
|
543
562
|
fs.mkdirSync(path.dirname(backupDir), { recursive: true });
|
|
544
|
-
fs.cpSync(taskDir, backupDir, { recursive: true });
|
|
563
|
+
fs.cpSync(taskDir, backupDir, { recursive: true, verbatimSymlinks: true });
|
|
545
564
|
return backupDir;
|
|
546
565
|
}
|
|
547
566
|
|
|
@@ -1085,16 +1104,17 @@ async function cmdMerge(args: string[]): Promise<void> {
|
|
|
1085
1104
|
throw new Error(`Source path is not a directory: ${sourcePath}`);
|
|
1086
1105
|
}
|
|
1087
1106
|
|
|
1088
|
-
const
|
|
1107
|
+
const repoRoot = process.cwd();
|
|
1108
|
+
const workspaceDir = path.join(repoRoot, '.agents', 'workspace');
|
|
1089
1109
|
const archiveDir = path.join(workspaceDir, 'archive');
|
|
1090
1110
|
const backupStamp = formatBackupTimestamp(new Date());
|
|
1091
|
-
const
|
|
1092
|
-
const
|
|
1093
|
-
const
|
|
1111
|
+
const projectKey = resolveRecoveryProjectKey(repoRoot);
|
|
1112
|
+
const recoveryRoot = path.join(os.homedir(), '.agent-infra', 'recovery-backups', projectKey, 'workspace-merge');
|
|
1113
|
+
const backupRoot = path.join(recoveryRoot, backupStamp);
|
|
1114
|
+
const report = createReport(resolvedSource, backupRoot);
|
|
1094
1115
|
detectSourceMode(resolvedSource);
|
|
1095
1116
|
validateSourceWorkspace(resolvedSource);
|
|
1096
1117
|
validateArchiveSection(workspaceDir, new Map());
|
|
1097
|
-
|
|
1098
1118
|
for (const section of ALL_SECTIONS) {
|
|
1099
1119
|
fs.mkdirSync(path.join(workspaceDir, section), { recursive: true });
|
|
1100
1120
|
}
|
|
@@ -51,7 +51,7 @@ function referencedBackfillArtifact(taskDir: string, message: string): BackfillA
|
|
|
51
51
|
.map((match) => match[1]!);
|
|
52
52
|
if (names.length !== 1) return { status: 'ignored', artifact: null, error: null };
|
|
53
53
|
const parsed = parseArtifactName(names[0]!);
|
|
54
|
-
if (!parsed ||
|
|
54
|
+
if (!parsed || !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family as typeof COMPLETION_BACKFILL_FAMILIES[number])) {
|
|
55
55
|
return { status: 'ignored', artifact: null, error: null };
|
|
56
56
|
}
|
|
57
57
|
const artifactPath = path.join(taskDir, parsed.name);
|
|
@@ -141,6 +141,7 @@ async function backfillCompletionComments(
|
|
|
141
141
|
artifact: artifact.name,
|
|
142
142
|
agent: options.agent,
|
|
143
143
|
backfill: true,
|
|
144
|
+
dependency: 'required',
|
|
144
145
|
cwd: resolved.repoRoot,
|
|
145
146
|
client: options.client
|
|
146
147
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computeInLabels } from './metadata-labels.ts';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import type { PlatformError } from './types.ts';
|
|
3
4
|
|
|
4
5
|
type InLabelPlan = {
|
|
@@ -68,6 +69,16 @@ function validateInLabelMapping(value: unknown): { ok: true; value: Record<strin
|
|
|
68
69
|
return { ok: true, value: normalized };
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function inLabelMappingDigest(value: unknown): { ok: true; digest: string } | { ok: false; error: InLabelValidationError } {
|
|
73
|
+
const mapping = validateInLabelMapping(value);
|
|
74
|
+
if (!mapping.ok) return mapping;
|
|
75
|
+
const canonical = Object.fromEntries(Object.keys(mapping.value).sort().map((name) => [
|
|
76
|
+
name,
|
|
77
|
+
[...new Set(mapping.value[name]!)].sort()
|
|
78
|
+
]));
|
|
79
|
+
return { ok: true, digest: createHash('sha256').update(JSON.stringify(canonical)).digest('hex') };
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
function repositoryLabelPages(value: unknown): unknown[][] | null {
|
|
72
83
|
if (!Array.isArray(value)) return null;
|
|
73
84
|
if (value.length === 0) return [];
|
|
@@ -197,6 +208,7 @@ export {
|
|
|
197
208
|
extractRepositoryLabelNames,
|
|
198
209
|
flattenPages,
|
|
199
210
|
inLabels,
|
|
211
|
+
inLabelMappingDigest,
|
|
200
212
|
labelDelta,
|
|
201
213
|
mergeInLabels,
|
|
202
214
|
planInLabelUpdate,
|
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import { parseTaskFrontmatter } from '../task/frontmatter.ts';
|
|
6
6
|
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
7
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
7
8
|
import { resolvePlatformProviderContext } from './context.ts';
|
|
8
9
|
import type { PlatformClient } from './context.ts';
|
|
9
10
|
import { platformResult } from './types.ts';
|
|
@@ -51,6 +52,9 @@ type SyncOptions = {
|
|
|
51
52
|
client?: PlatformClient;
|
|
52
53
|
runtimeVersion?: string;
|
|
53
54
|
summaryAuthorization?: { sha256: string };
|
|
55
|
+
dependency?: 'deferred' | 'required';
|
|
56
|
+
skipQueue?: boolean;
|
|
57
|
+
taskProjection?: { content: string; sha256: string };
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
@@ -74,6 +78,8 @@ const ARTIFACT_TITLES: Record<string, string> = {
|
|
|
74
78
|
'review-plan': '技术方案审查',
|
|
75
79
|
code: '实现报告',
|
|
76
80
|
'review-code': '代码审查',
|
|
81
|
+
'manual-validation': '人工验证',
|
|
82
|
+
'validation-run': '验证运行证据',
|
|
77
83
|
'pr-review': 'PR 审查报告'
|
|
78
84
|
};
|
|
79
85
|
|
|
@@ -150,7 +156,7 @@ function taskCommentLanguage(repoRoot: string): string {
|
|
|
150
156
|
|
|
151
157
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
152
158
|
const stem = path.basename(artifact, '.md');
|
|
153
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
159
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
|
|
154
160
|
if (!match) throw new Error(`unsupported artifact '${artifact}'`);
|
|
155
161
|
const base = ARTIFACT_TITLES[match[1]!]!;
|
|
156
162
|
const round = match[2] ? Number(match[2]) : 1;
|
|
@@ -529,7 +535,7 @@ function writeComment(
|
|
|
529
535
|
);
|
|
530
536
|
}
|
|
531
537
|
|
|
532
|
-
async function
|
|
538
|
+
async function syncPlatformCommentImpl(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
533
539
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
534
540
|
if (!resolved.ok) {
|
|
535
541
|
return platformResult('failed', {
|
|
@@ -547,7 +553,12 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
547
553
|
}
|
|
548
554
|
let desired: RenderedChunk[];
|
|
549
555
|
try {
|
|
550
|
-
|
|
556
|
+
const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
557
|
+
if (options.kind === 'task' && options.taskProjection
|
|
558
|
+
&& createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
|
|
559
|
+
throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
|
|
560
|
+
}
|
|
561
|
+
desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
|
|
551
562
|
} catch (error) {
|
|
552
563
|
return platformResult('failed', {
|
|
553
564
|
resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask) },
|
|
@@ -743,6 +754,50 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
743
754
|
return result;
|
|
744
755
|
}
|
|
745
756
|
|
|
757
|
+
async function syncPlatformComment(taskRef: string, options: SyncOptions): Promise<PlatformResult> {
|
|
758
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
759
|
+
if (!resolved.ok) return syncPlatformCommentImpl(taskRef, options);
|
|
760
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
761
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent))) return syncPlatformCommentImpl(taskRef, options);
|
|
762
|
+
|
|
763
|
+
let operation: { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string };
|
|
764
|
+
try {
|
|
765
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
766
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
767
|
+
const content = desired.map((chunk) => chunk.content).join('\0');
|
|
768
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
769
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
770
|
+
const target = options.kind === 'artifact' ? options.artifact! : options.kind;
|
|
771
|
+
operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
|
|
772
|
+
} catch {
|
|
773
|
+
return syncPlatformCommentImpl(taskRef, options);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const execute = () => syncPlatformCommentImpl(taskRef, options);
|
|
777
|
+
if (options.skipQueue) return execute();
|
|
778
|
+
return coordinatePlatformWrite({
|
|
779
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
|
|
780
|
+
agent: options.agent,
|
|
781
|
+
execute,
|
|
782
|
+
block: (error) => platformResult('blocked', { error }),
|
|
783
|
+
persistenceFailure: (error) => platformResult('failed', { error })
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function inspectPlatformCommentOperation(taskRef: string, options: SyncOptions): { kind: 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment'; target: string; expectedDigest: string; id: string } | null {
|
|
788
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
789
|
+
if (!resolved.ok) return null;
|
|
790
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
791
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent))) return null;
|
|
792
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
793
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
794
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
795
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
796
|
+
const target = options.kind === 'artifact' ? options.artifact! : options.kind;
|
|
797
|
+
const expectedDigest = createHash('sha256').update(desired.map((chunk) => chunk.content).join('\0')).digest('hex');
|
|
798
|
+
return { kind, target, expectedDigest, id: createHash('sha256').update(`${kind}\0${target}\0${expectedDigest}`).digest('hex') };
|
|
799
|
+
}
|
|
800
|
+
|
|
746
801
|
async function listPlatformComments(issue: string | number, cwd = process.cwd(), client?: PlatformClient): Promise<PlatformResult & { comments?: RemoteComment[] }> {
|
|
747
802
|
const loaded = await resolvePlatformProviderContext({ cwd, client });
|
|
748
803
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
@@ -800,6 +855,7 @@ export {
|
|
|
800
855
|
COMMENT_BYTE_LIMIT,
|
|
801
856
|
MARKERS,
|
|
802
857
|
chunkArtifactComment,
|
|
858
|
+
inspectPlatformCommentOperation,
|
|
803
859
|
findMarkerComments,
|
|
804
860
|
listRemoteComments,
|
|
805
861
|
checkPlatformCommentOwner,
|
package/lib/platform/issues.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PlatformIssueSnapshot as IssueSnapshot } from './snapshots.ts';
|
|
|
2
2
|
import type { IssueFieldSchema } from './github-data.ts';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { execFileSync } from 'node:child_process';
|
|
6
7
|
|
|
7
8
|
import { buildDefaultBody } from '../task/commands/issue-body.ts';
|
|
@@ -24,6 +25,7 @@ import { platformResult } from './types.ts';
|
|
|
24
25
|
import type { PlatformResult } from './types.ts';
|
|
25
26
|
import {
|
|
26
27
|
labelDelta,
|
|
28
|
+
inLabelMappingDigest,
|
|
27
29
|
planInLabelUpdate,
|
|
28
30
|
syncLabelDelta,
|
|
29
31
|
validateInLabelMapping,
|
|
@@ -39,9 +41,11 @@ import {
|
|
|
39
41
|
} from './provider-bridge.ts';
|
|
40
42
|
import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.ts';
|
|
41
43
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.ts';
|
|
44
|
+
import { coordinatePlatformWrite } from './operation-coordinator.ts';
|
|
42
45
|
import type { ResourceIdentity } from './resource-identity.ts';
|
|
43
46
|
import type { IssueSnapshot as ProviderIssueSnapshot, RepositoryMetadataSnapshot } from './provider-contract.ts';
|
|
44
|
-
type IssueResult = PlatformResult & {
|
|
47
|
+
type IssueResult = Omit<PlatformResult, 'operations'> & {
|
|
48
|
+
operations: PlannedOperation[];
|
|
45
49
|
task: { id: string | null; issueNumber: number | null };
|
|
46
50
|
issue: IssueSnapshot | null;
|
|
47
51
|
};
|
|
@@ -61,6 +65,8 @@ type SyncOptions = SharedOptions & {
|
|
|
61
65
|
state?: 'open' | 'closed';
|
|
62
66
|
closeReason?: 'completed' | 'not_planned';
|
|
63
67
|
dryRun?: boolean;
|
|
68
|
+
dependency?: 'deferred' | 'required';
|
|
69
|
+
skipQueue?: boolean;
|
|
64
70
|
};
|
|
65
71
|
|
|
66
72
|
function result(
|
|
@@ -305,7 +311,7 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
305
311
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
306
312
|
operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
|
|
307
313
|
});
|
|
308
|
-
{
|
|
314
|
+
const execute = async (): Promise<IssueResult> => {
|
|
309
315
|
const created = base.provider.issues?.create
|
|
310
316
|
? await base.provider.issues.create({
|
|
311
317
|
context: providerOperationContext(base.loadedContext),
|
|
@@ -344,7 +350,7 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
344
350
|
if (written.status === 'failed') return result('failed', base.resolved.taskId, null, {
|
|
345
351
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
346
352
|
resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
|
|
347
|
-
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable:
|
|
353
|
+
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: true }
|
|
348
354
|
});
|
|
349
355
|
return result('applied', base.resolved.taskId, issueNumber, {
|
|
350
356
|
changed: true,
|
|
@@ -355,7 +361,26 @@ async function createPlatformIssue(taskRef: string, options: CreateOptions): Pro
|
|
|
355
361
|
issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
|
|
356
362
|
error: null
|
|
357
363
|
});
|
|
358
|
-
}
|
|
364
|
+
};
|
|
365
|
+
const issueCreate = { title: payload.title, bodyDigest: createHash('sha256').update(payload.body).digest('hex') };
|
|
366
|
+
const operation = {
|
|
367
|
+
kind: 'issue-create' as const,
|
|
368
|
+
target: `task:${base.resolved.taskId}`,
|
|
369
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
370
|
+
taskId: base.resolved.taskId,
|
|
371
|
+
title: payload.title,
|
|
372
|
+
body: createHash('sha256').update(payload.body).digest('hex'),
|
|
373
|
+
labels: payload.labels || [], assignees: payload.assignees,
|
|
374
|
+
milestone: typeof payload.milestone === 'string' ? payload.milestone : null
|
|
375
|
+
})).digest('hex')
|
|
376
|
+
};
|
|
377
|
+
return coordinatePlatformWrite({
|
|
378
|
+
operation: { taskRef: base.resolved.taskId, cwd: base.resolved.repoRoot, ...operation, issueCreate, dependency: 'required' },
|
|
379
|
+
agent: options.agent,
|
|
380
|
+
execute,
|
|
381
|
+
block: (error) => result('blocked', base.resolved.taskId, null, { platform: base.context.platform, capabilities: base.context.capabilities, error }),
|
|
382
|
+
persistenceFailure: (error) => result('failed', base.resolved.taskId, null, { error })
|
|
383
|
+
});
|
|
359
384
|
}
|
|
360
385
|
|
|
361
386
|
async function bindPlatformIssue(taskRef: string, options: BindOptions): Promise<IssueResult> {
|
|
@@ -478,7 +503,7 @@ function desiredFieldValues(frontmatter: Record<string, string>, fields: IssueFi
|
|
|
478
503
|
return desired;
|
|
479
504
|
}
|
|
480
505
|
|
|
481
|
-
async function
|
|
506
|
+
async function syncPlatformIssueImpl(taskRef: string, options: SyncOptions): Promise<IssueResult> {
|
|
482
507
|
const base = await resolvedContext(taskRef, options);
|
|
483
508
|
if (!base.ok) return base.output;
|
|
484
509
|
if (!base.issueIdentity) return result('failed', base.resolved.taskId, null, { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
|
|
@@ -617,5 +642,78 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
|
|
|
617
642
|
});
|
|
618
643
|
}
|
|
619
644
|
|
|
645
|
+
async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise<IssueResult> {
|
|
646
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
647
|
+
if (!resolved.ok || options.dryRun) return syncPlatformIssueImpl(taskRef, options);
|
|
648
|
+
let frontmatter: Record<string, string>;
|
|
649
|
+
let taskContent: string;
|
|
650
|
+
try {
|
|
651
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
652
|
+
frontmatter = parseTaskFrontmatter(taskContent);
|
|
653
|
+
}
|
|
654
|
+
catch { return syncPlatformIssueImpl(taskRef, options); }
|
|
655
|
+
const identity = taskIssueIdentity(frontmatter);
|
|
656
|
+
if (!identity || ![
|
|
657
|
+
options.requirements, options.issueType, options.fields, options.status !== undefined,
|
|
658
|
+
options.assignees !== undefined, options.milestone !== undefined,
|
|
659
|
+
options.inLabels !== undefined, options.state !== undefined
|
|
660
|
+
].some(Boolean)) return syncPlatformIssueImpl(taskRef, options);
|
|
661
|
+
let fromDiffFiles: string[] | undefined;
|
|
662
|
+
let mappingDigest: string | undefined;
|
|
663
|
+
if (options.inLabels === 'from-diff') {
|
|
664
|
+
const taskBase = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
665
|
+
if (taskBase && (!options.base || options.base === taskBase)) {
|
|
666
|
+
try {
|
|
667
|
+
fromDiffFiles = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
|
|
668
|
+
cwd: resolved.repoRoot, encoding: 'utf8'
|
|
669
|
+
}).trim().split(/\r?\n/).filter(Boolean).sort();
|
|
670
|
+
const config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8')) as { labels?: { in?: unknown } };
|
|
671
|
+
const mapping = inLabelMappingDigest(config.labels?.in);
|
|
672
|
+
if (!mapping.ok) return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
673
|
+
error: { code: mapping.error.code, message: mapping.error.message, retryable: false }
|
|
674
|
+
});
|
|
675
|
+
mappingDigest = mapping.digest;
|
|
676
|
+
} catch {
|
|
677
|
+
return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
678
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: 'Unable to derive the current changed-file set for Issue metadata recovery', retryable: false }
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const issueMetadata = {
|
|
684
|
+
requirements: options.requirements === true,
|
|
685
|
+
issueType: options.issueType === true,
|
|
686
|
+
fields: options.fields === true,
|
|
687
|
+
...(options.status !== undefined ? { status: options.status } : {}),
|
|
688
|
+
...(options.assignees !== undefined ? { assignees: options.assignees } : {}),
|
|
689
|
+
...(options.milestone !== undefined ? { milestone: options.milestone } : {}),
|
|
690
|
+
...(options.inLabels ? { inLabels: options.inLabels } : {}),
|
|
691
|
+
...(options.base ? { base: options.base } : {}),
|
|
692
|
+
...(fromDiffFiles ? { fromDiffFiles } : {}),
|
|
693
|
+
...(mappingDigest ? { inLabelMappingDigest: mappingDigest } : {}),
|
|
694
|
+
...(options.state ? { state: options.state } : {}),
|
|
695
|
+
...(options.closeReason ? { closeReason: options.closeReason } : {})
|
|
696
|
+
};
|
|
697
|
+
const operation = {
|
|
698
|
+
kind: 'issue-metadata' as const,
|
|
699
|
+
target: JSON.stringify(identity),
|
|
700
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
701
|
+
...issueMetadata,
|
|
702
|
+
task: resolved.taskId,
|
|
703
|
+
taskContent: createHash('sha256').update(taskContent).digest('hex')
|
|
704
|
+
})).digest('hex')
|
|
705
|
+
};
|
|
706
|
+
const dependency = options.dependency ?? (options.state === 'closed' && options.closeReason === 'not_planned' ? 'required' : 'deferred');
|
|
707
|
+
const execute = () => syncPlatformIssueImpl(taskRef, options);
|
|
708
|
+
if (options.skipQueue) return execute();
|
|
709
|
+
return coordinatePlatformWrite({
|
|
710
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, issueMetadata, dependency },
|
|
711
|
+
agent: options.agent,
|
|
712
|
+
execute,
|
|
713
|
+
block: (error) => result('blocked', resolved.taskId, resourceIdentityNumber(identity), { error }),
|
|
714
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, resourceIdentityNumber(identity), { error })
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
620
718
|
export { bindPlatformIssue, createPlatformIssue, inspectPlatformIssue, requirementSectionAnchors, syncPlatformIssue };
|
|
621
719
|
export type { BindOptions, CreateOptions, IssueResult, SyncOptions };
|