@fitlab-ai/agent-infra 0.11.7 → 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/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 +96 -4
- 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/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/task/activity-intent.js +4 -3
- package/dist/lib/task/create-service.js +5 -3
- package/dist/lib/task/create.js +22 -5
- package/dist/lib/task/finalization-artifacts.js +4 -1
- package/dist/lib/task/finalization.js +157 -24
- package/dist/lib/task/lifecycle.js +38 -2
- package/dist/lib/task/platform-operation-journal.js +152 -0
- package/dist/lib/task/platform-operation-recovery.js +459 -0
- 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/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 +105 -5
- 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/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/task/activity-intent.ts +4 -3
- package/lib/task/create-service.ts +5 -3
- package/lib/task/create.ts +17 -5
- package/lib/task/finalization-artifacts.ts +4 -1
- package/lib/task/finalization.ts +154 -21
- package/lib/task/lifecycle.ts +35 -3
- package/lib/task/platform-operation-journal.ts +230 -0
- package/lib/task/platform-operation-recovery.ts +435 -0
- 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 +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +12 -0
- package/templates/.agents/rules/task-management.zh-CN.md +12 -0
- package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- 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-pr/SKILL.en.md +11 -11
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +11 -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/update-agent-infra/scripts/sync-templates.js +19 -6
|
@@ -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 };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.ts';
|
|
2
|
+
import { recordPlatformOperation } from '../task/platform-operation-journal.ts';
|
|
3
|
+
import type { RecordOperationInput } from '../task/platform-operation-journal.ts';
|
|
4
|
+
import type { PlatformError, PlatformStatus } from './types.ts';
|
|
5
|
+
import type { PlatformClient } from './context.ts';
|
|
6
|
+
|
|
7
|
+
type OperationResult = Readonly<{
|
|
8
|
+
status: PlatformStatus;
|
|
9
|
+
error?: PlatformError | null;
|
|
10
|
+
warnings?: readonly { code: string }[];
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type CoordinateInput<T extends OperationResult> = Readonly<{
|
|
14
|
+
operation: Omit<RecordOperationInput, 'state' | 'lastCode'>;
|
|
15
|
+
agent: string;
|
|
16
|
+
client?: PlatformClient;
|
|
17
|
+
drain?: (taskRef: string, excludeId: string, agent: string, cwd?: string) => Promise<{
|
|
18
|
+
status: 'applied' | 'no-op' | 'blocked' | 'failed';
|
|
19
|
+
error: PlatformError | null;
|
|
20
|
+
}>;
|
|
21
|
+
execute: () => Promise<T>;
|
|
22
|
+
block: (error: PlatformError) => T;
|
|
23
|
+
persistenceFailure: (error: PlatformError) => T;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
async function coordinatePlatformWrite<T extends OperationResult>(input: CoordinateInput<T>): Promise<T> {
|
|
27
|
+
let operationId: string;
|
|
28
|
+
try {
|
|
29
|
+
const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
|
|
30
|
+
operationId = queued.id;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const value = error as { code?: string; message?: string };
|
|
33
|
+
return input.persistenceFailure({
|
|
34
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
35
|
+
message: value.message || 'Unable to queue platform operation',
|
|
36
|
+
retryable: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
|
|
41
|
+
? 'all' : input.operation.dependency;
|
|
42
|
+
const drained = input.drain
|
|
43
|
+
? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
|
|
44
|
+
: await recoverPlatformOperations(input.operation.taskRef, selection, {
|
|
45
|
+
agent: input.agent,
|
|
46
|
+
client: input.client,
|
|
47
|
+
cwd: input.operation.cwd,
|
|
48
|
+
excludeId: operationId
|
|
49
|
+
});
|
|
50
|
+
if (drained.status !== 'applied' && drained.status !== 'no-op') {
|
|
51
|
+
return input.block(drained.error ?? {
|
|
52
|
+
code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
|
|
53
|
+
message: 'A previous platform operation is still unresolved; the current write remains queued',
|
|
54
|
+
retryable: true
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
recordPlatformOperation({ ...input.operation, state: 'pending' });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const value = error as { code?: string; message?: string };
|
|
62
|
+
return input.persistenceFailure({
|
|
63
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
64
|
+
message: value.message || 'Unable to mark platform operation as started',
|
|
65
|
+
retryable: true
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let output: T;
|
|
70
|
+
try {
|
|
71
|
+
output = await input.execute();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const value = error as { code?: string; message?: string };
|
|
74
|
+
output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
75
|
+
}
|
|
76
|
+
const succeeded = (output.status === 'applied' || output.status === 'no-op')
|
|
77
|
+
&& !output.error && !(output.warnings?.length);
|
|
78
|
+
const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
|
|
79
|
+
try {
|
|
80
|
+
recordPlatformOperation({
|
|
81
|
+
...input.operation,
|
|
82
|
+
state,
|
|
83
|
+
lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const value = error as { code?: string; message?: string };
|
|
87
|
+
return input.persistenceFailure({
|
|
88
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
89
|
+
message: value.message || 'Unable to persist platform operation outcome',
|
|
90
|
+
retryable: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { coordinatePlatformWrite };
|