@fitlab-ai/agent-infra 0.11.7 → 0.11.9
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 +1 -1
- package/README.zh-CN.md +1 -1
- 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/agent-clients/custom-tool-invocations.js +92 -0
- package/dist/lib/agent-clients/next-steps.js +1 -1
- package/dist/lib/agent-clients/reconcile.js +3 -3
- package/dist/lib/agent-clients/schema.js +18 -10
- package/dist/lib/defaults.json +6 -3
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/agent-client.js +3 -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 +12 -22
- package/dist/lib/sandbox/removal.js +58 -20
- package/dist/lib/sandbox/tools.js +31 -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 +243 -113
- 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/verification.js +0 -1
- package/dist/lib/task/workflow-warning-intents.js +7 -1
- package/dist/lib/update.js +2 -2
- 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/agent-clients/custom-tool-invocations.ts +129 -0
- package/lib/agent-clients/next-steps.ts +3 -3
- package/lib/agent-clients/reconcile.ts +3 -3
- package/lib/agent-clients/schema.ts +18 -10
- package/lib/defaults.json +6 -3
- package/lib/init.ts +2 -2
- package/lib/internal/agent-client.ts +3 -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 -25
- package/lib/sandbox/removal.ts +57 -19
- package/lib/sandbox/tool-types.ts +2 -1
- package/lib/sandbox/tools.ts +38 -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 +242 -106
- 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/verification.ts +1 -2
- package/lib/task/workflow-warning-intents.ts +9 -1
- package/lib/update.ts +2 -3
- package/package.json +1 -1
- package/templates/.agents/README.en.md +20 -66
- package/templates/.agents/README.zh-CN.md +19 -65
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +1 -1
- package/templates/.agents/rules/next-step-output.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/scripts/lib/agent-infra-package.js +0 -7
- 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/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- 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/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- 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-code/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +15 -6
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +15 -6
- 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/SKILL.en.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +21 -274
- package/dist/lib/agent-clients/custom-tuis.js +0 -99
- package/lib/agent-clients/custom-tuis.ts +0 -149
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { parseTaskFrontmatter } from '../task/frontmatter.js';
|
|
5
5
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
6
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
6
7
|
import { resolvePlatformProviderContext } from './context.js';
|
|
7
8
|
import { platformResult } from './types.js';
|
|
8
9
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
|
|
@@ -29,6 +30,8 @@ const ARTIFACT_TITLES = {
|
|
|
29
30
|
'review-plan': '技术方案审查',
|
|
30
31
|
code: '实现报告',
|
|
31
32
|
'review-code': '代码审查',
|
|
33
|
+
'manual-validation': '人工验证',
|
|
34
|
+
'validation-run': '验证运行证据',
|
|
32
35
|
'pr-review': 'PR 审查报告'
|
|
33
36
|
};
|
|
34
37
|
function normalizeCommentContent(content) {
|
|
@@ -98,7 +101,7 @@ function taskCommentLanguage(repoRoot) {
|
|
|
98
101
|
}
|
|
99
102
|
function artifactIdentity(artifact) {
|
|
100
103
|
const stem = path.basename(artifact, '.md');
|
|
101
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
104
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
|
|
102
105
|
if (!match)
|
|
103
106
|
throw new Error(`unsupported artifact '${artifact}'`);
|
|
104
107
|
const base = ARTIFACT_TITLES[match[1]];
|
|
@@ -430,7 +433,7 @@ function writeComment(client, repo, issue, cwd, body, id) {
|
|
|
430
433
|
const endpoint = id ? `repos/${repo}/issues/comments/${id}` : `repos/${repo}/issues/${issue}/comments`;
|
|
431
434
|
return client.json(['api', endpoint, '-X', id ? 'PATCH' : 'POST', '--input', '-'], { cwd, method: id ? 'PATCH' : 'POST', input: JSON.stringify({ body }) });
|
|
432
435
|
}
|
|
433
|
-
async function
|
|
436
|
+
async function syncPlatformCommentImpl(taskRef, options) {
|
|
434
437
|
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
435
438
|
if (!resolved.ok) {
|
|
436
439
|
return platformResult('failed', {
|
|
@@ -452,7 +455,12 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
452
455
|
}
|
|
453
456
|
let desired;
|
|
454
457
|
try {
|
|
455
|
-
|
|
458
|
+
const projectedContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
459
|
+
if (options.kind === 'task' && options.taskProjection
|
|
460
|
+
&& createHash('sha256').update(projectedContent).digest('hex') !== options.taskProjection.sha256) {
|
|
461
|
+
throw Object.assign(new Error('finalization task projection digest does not match content'), { code: 'FINALIZATION_PROJECTION_INVALID' });
|
|
462
|
+
}
|
|
463
|
+
desired = expectedComments(resolved.taskId, projectedContent, resolved.taskDir, resolved.repoRoot, options);
|
|
456
464
|
}
|
|
457
465
|
catch (error) {
|
|
458
466
|
return platformResult('failed', {
|
|
@@ -644,6 +652,52 @@ async function syncPlatformComment(taskRef, options) {
|
|
|
644
652
|
});
|
|
645
653
|
return result;
|
|
646
654
|
}
|
|
655
|
+
async function syncPlatformComment(taskRef, options) {
|
|
656
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
657
|
+
if (!resolved.ok)
|
|
658
|
+
return syncPlatformCommentImpl(taskRef, options);
|
|
659
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
660
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent)))
|
|
661
|
+
return syncPlatformCommentImpl(taskRef, options);
|
|
662
|
+
let operation;
|
|
663
|
+
try {
|
|
664
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
665
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
666
|
+
const content = desired.map((chunk) => chunk.content).join('\0');
|
|
667
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
668
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
669
|
+
const target = options.kind === 'artifact' ? options.artifact : options.kind;
|
|
670
|
+
operation = { kind, target, expectedDigest: createHash('sha256').update(content).digest('hex') };
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return syncPlatformCommentImpl(taskRef, options);
|
|
674
|
+
}
|
|
675
|
+
const execute = () => syncPlatformCommentImpl(taskRef, options);
|
|
676
|
+
if (options.skipQueue)
|
|
677
|
+
return execute();
|
|
678
|
+
return coordinatePlatformWrite({
|
|
679
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, dependency: options.dependency ?? 'deferred' },
|
|
680
|
+
agent: options.agent,
|
|
681
|
+
execute,
|
|
682
|
+
block: (error) => platformResult('blocked', { error }),
|
|
683
|
+
persistenceFailure: (error) => platformResult('failed', { error })
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function inspectPlatformCommentOperation(taskRef, options) {
|
|
687
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
688
|
+
if (!resolved.ok)
|
|
689
|
+
return null;
|
|
690
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
691
|
+
if (!taskIssueIdentity(parseTaskFrontmatter(taskContent)))
|
|
692
|
+
return null;
|
|
693
|
+
const sourceContent = options.kind === 'task' && options.taskProjection ? options.taskProjection.content : taskContent;
|
|
694
|
+
const desired = expectedComments(resolved.taskId, sourceContent, resolved.taskDir, resolved.repoRoot, options);
|
|
695
|
+
const kind = options.kind === 'task' ? 'task-comment' : options.kind === 'summary' ? 'summary-comment'
|
|
696
|
+
: options.kind === 'cancel' ? 'cancel-comment' : 'artifact-comment';
|
|
697
|
+
const target = options.kind === 'artifact' ? options.artifact : options.kind;
|
|
698
|
+
const expectedDigest = createHash('sha256').update(desired.map((chunk) => chunk.content).join('\0')).digest('hex');
|
|
699
|
+
return { kind, target, expectedDigest, id: createHash('sha256').update(`${kind}\0${target}\0${expectedDigest}`).digest('hex') };
|
|
700
|
+
}
|
|
647
701
|
async function listPlatformComments(issue, cwd = process.cwd(), client) {
|
|
648
702
|
const loaded = await resolvePlatformProviderContext({ cwd, client });
|
|
649
703
|
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
@@ -708,5 +762,5 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
|
|
|
708
762
|
}
|
|
709
763
|
return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
|
|
710
764
|
}
|
|
711
|
-
export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
|
|
765
|
+
export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, inspectPlatformCommentOperation, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, renderTaskCommentResult, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
|
|
712
766
|
//# sourceMappingURL=issue-comments.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { buildDefaultBody } from '../task/commands/issue-body.js';
|
|
5
6
|
import { extractTitle, parseTaskFrontmatter } from '../task/frontmatter.js';
|
|
@@ -10,10 +11,11 @@ import { writeTask } from '../task/write.js';
|
|
|
10
11
|
import { resolvePlatformProviderContext } from './context.js';
|
|
11
12
|
import { DEFAULT_REQUIREMENT_SECTION_ANCHORS, chooseMilestone, desiredIssueType, normalizeOption, planIssueMetadata } from './issue-metadata.js';
|
|
12
13
|
import { platformResult } from './types.js';
|
|
13
|
-
import { labelDelta, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
|
|
14
|
+
import { labelDelta, inLabelMappingDigest, planInLabelUpdate, syncLabelDelta, validateInLabelMapping, validateRepositoryLabelPayload } from './in-label-sync.js';
|
|
14
15
|
import { providerError, providerOperationContext, providerStatus, providerResourceIdentity, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
|
|
15
16
|
import { resourceIdentityEquals, resourceIdentityNumber, resourceIdentityString, serializeResourceIdentity } from './resource-identity.js';
|
|
16
17
|
import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
|
|
18
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
17
19
|
function result(status, taskId, issueNumber, overrides = {}) {
|
|
18
20
|
return {
|
|
19
21
|
...platformResult(status),
|
|
@@ -247,7 +249,7 @@ async function createPlatformIssue(taskRef, options) {
|
|
|
247
249
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
248
250
|
operations: [{ name: 'issue:create', status: 'planned', reasonCode: null }], error: null
|
|
249
251
|
});
|
|
250
|
-
{
|
|
252
|
+
const execute = async () => {
|
|
251
253
|
const created = base.provider.issues?.create
|
|
252
254
|
? await base.provider.issues.create({
|
|
253
255
|
context: providerOperationContext(base.loadedContext),
|
|
@@ -290,7 +292,7 @@ async function createPlatformIssue(taskRef, options) {
|
|
|
290
292
|
return result('failed', base.resolved.taskId, null, {
|
|
291
293
|
platform: base.context.platform, capabilities: base.context.capabilities,
|
|
292
294
|
resource: { kind: 'issue', number: issueNumber, identity: inspected.value.identity || createdIdentity },
|
|
293
|
-
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable:
|
|
295
|
+
error: { code: 'ISSUE_CREATED_BIND_FAILED', message: written.error.message, retryable: true }
|
|
294
296
|
});
|
|
295
297
|
return result('applied', base.resolved.taskId, issueNumber, {
|
|
296
298
|
changed: true,
|
|
@@ -301,7 +303,26 @@ async function createPlatformIssue(taskRef, options) {
|
|
|
301
303
|
issue: normalizeProviderIssue(inspected.value, repository, issueNumber),
|
|
302
304
|
error: null
|
|
303
305
|
});
|
|
304
|
-
}
|
|
306
|
+
};
|
|
307
|
+
const issueCreate = { title: payload.title, bodyDigest: createHash('sha256').update(payload.body).digest('hex') };
|
|
308
|
+
const operation = {
|
|
309
|
+
kind: 'issue-create',
|
|
310
|
+
target: `task:${base.resolved.taskId}`,
|
|
311
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
312
|
+
taskId: base.resolved.taskId,
|
|
313
|
+
title: payload.title,
|
|
314
|
+
body: createHash('sha256').update(payload.body).digest('hex'),
|
|
315
|
+
labels: payload.labels || [], assignees: payload.assignees,
|
|
316
|
+
milestone: typeof payload.milestone === 'string' ? payload.milestone : null
|
|
317
|
+
})).digest('hex')
|
|
318
|
+
};
|
|
319
|
+
return coordinatePlatformWrite({
|
|
320
|
+
operation: { taskRef: base.resolved.taskId, cwd: base.resolved.repoRoot, ...operation, issueCreate, dependency: 'required' },
|
|
321
|
+
agent: options.agent,
|
|
322
|
+
execute,
|
|
323
|
+
block: (error) => result('blocked', base.resolved.taskId, null, { platform: base.context.platform, capabilities: base.context.capabilities, error }),
|
|
324
|
+
persistenceFailure: (error) => result('failed', base.resolved.taskId, null, { error })
|
|
325
|
+
});
|
|
305
326
|
}
|
|
306
327
|
async function bindPlatformIssue(taskRef, options) {
|
|
307
328
|
const base = await resolvedContext(taskRef, options);
|
|
@@ -435,7 +456,7 @@ function desiredFieldValues(frontmatter, fields) {
|
|
|
435
456
|
}
|
|
436
457
|
return desired;
|
|
437
458
|
}
|
|
438
|
-
async function
|
|
459
|
+
async function syncPlatformIssueImpl(taskRef, options) {
|
|
439
460
|
const base = await resolvedContext(taskRef, options);
|
|
440
461
|
if (!base.ok)
|
|
441
462
|
return base.output;
|
|
@@ -582,5 +603,84 @@ async function syncPlatformIssue(taskRef, options) {
|
|
|
582
603
|
operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
|
|
583
604
|
});
|
|
584
605
|
}
|
|
606
|
+
async function syncPlatformIssue(taskRef, options) {
|
|
607
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
608
|
+
if (!resolved.ok || options.dryRun)
|
|
609
|
+
return syncPlatformIssueImpl(taskRef, options);
|
|
610
|
+
let frontmatter;
|
|
611
|
+
let taskContent;
|
|
612
|
+
try {
|
|
613
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
614
|
+
frontmatter = parseTaskFrontmatter(taskContent);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return syncPlatformIssueImpl(taskRef, options);
|
|
618
|
+
}
|
|
619
|
+
const identity = taskIssueIdentity(frontmatter);
|
|
620
|
+
if (!identity || ![
|
|
621
|
+
options.requirements, options.issueType, options.fields, options.status !== undefined,
|
|
622
|
+
options.assignees !== undefined, options.milestone !== undefined,
|
|
623
|
+
options.inLabels !== undefined, options.state !== undefined
|
|
624
|
+
].some(Boolean))
|
|
625
|
+
return syncPlatformIssueImpl(taskRef, options);
|
|
626
|
+
let fromDiffFiles;
|
|
627
|
+
let mappingDigest;
|
|
628
|
+
if (options.inLabels === 'from-diff') {
|
|
629
|
+
const taskBase = typeof frontmatter.delivery_base_ref === 'string' ? frontmatter.delivery_base_ref.trim() : '';
|
|
630
|
+
if (taskBase && (!options.base || options.base === taskBase)) {
|
|
631
|
+
try {
|
|
632
|
+
fromDiffFiles = execFileSync('git', ['diff', `${taskBase}...HEAD`, '--name-only'], {
|
|
633
|
+
cwd: resolved.repoRoot, encoding: 'utf8'
|
|
634
|
+
}).trim().split(/\r?\n/).filter(Boolean).sort();
|
|
635
|
+
const config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
636
|
+
const mapping = inLabelMappingDigest(config.labels?.in);
|
|
637
|
+
if (!mapping.ok)
|
|
638
|
+
return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
639
|
+
error: { code: mapping.error.code, message: mapping.error.message, retryable: false }
|
|
640
|
+
});
|
|
641
|
+
mappingDigest = mapping.digest;
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
return result('failed', resolved.taskId, resourceIdentityNumber(identity), {
|
|
645
|
+
error: { code: 'IN_LABEL_SYNC_EVIDENCE_UNAVAILABLE', message: 'Unable to derive the current changed-file set for Issue metadata recovery', retryable: false }
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const issueMetadata = {
|
|
651
|
+
requirements: options.requirements === true,
|
|
652
|
+
issueType: options.issueType === true,
|
|
653
|
+
fields: options.fields === true,
|
|
654
|
+
...(options.status !== undefined ? { status: options.status } : {}),
|
|
655
|
+
...(options.assignees !== undefined ? { assignees: options.assignees } : {}),
|
|
656
|
+
...(options.milestone !== undefined ? { milestone: options.milestone } : {}),
|
|
657
|
+
...(options.inLabels ? { inLabels: options.inLabels } : {}),
|
|
658
|
+
...(options.base ? { base: options.base } : {}),
|
|
659
|
+
...(fromDiffFiles ? { fromDiffFiles } : {}),
|
|
660
|
+
...(mappingDigest ? { inLabelMappingDigest: mappingDigest } : {}),
|
|
661
|
+
...(options.state ? { state: options.state } : {}),
|
|
662
|
+
...(options.closeReason ? { closeReason: options.closeReason } : {})
|
|
663
|
+
};
|
|
664
|
+
const operation = {
|
|
665
|
+
kind: 'issue-metadata',
|
|
666
|
+
target: JSON.stringify(identity),
|
|
667
|
+
expectedDigest: createHash('sha256').update(JSON.stringify({
|
|
668
|
+
...issueMetadata,
|
|
669
|
+
task: resolved.taskId,
|
|
670
|
+
taskContent: createHash('sha256').update(taskContent).digest('hex')
|
|
671
|
+
})).digest('hex')
|
|
672
|
+
};
|
|
673
|
+
const dependency = options.dependency ?? (options.state === 'closed' && options.closeReason === 'not_planned' ? 'required' : 'deferred');
|
|
674
|
+
const execute = () => syncPlatformIssueImpl(taskRef, options);
|
|
675
|
+
if (options.skipQueue)
|
|
676
|
+
return execute();
|
|
677
|
+
return coordinatePlatformWrite({
|
|
678
|
+
operation: { taskRef: resolved.taskId, cwd: resolved.repoRoot, ...operation, issueMetadata, dependency },
|
|
679
|
+
agent: options.agent,
|
|
680
|
+
execute,
|
|
681
|
+
block: (error) => result('blocked', resolved.taskId, resourceIdentityNumber(identity), { error }),
|
|
682
|
+
persistenceFailure: (error) => result('failed', resolved.taskId, resourceIdentityNumber(identity), { error })
|
|
683
|
+
});
|
|
684
|
+
}
|
|
585
685
|
export { bindPlatformIssue, createPlatformIssue, inspectPlatformIssue, requirementSectionAnchors, syncPlatformIssue };
|
|
586
686
|
//# sourceMappingURL=issues.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { recoverPlatformOperations } from '../task/platform-operation-recovery.js';
|
|
2
|
+
import { recordPlatformOperation } from '../task/platform-operation-journal.js';
|
|
3
|
+
async function coordinatePlatformWrite(input) {
|
|
4
|
+
let operationId;
|
|
5
|
+
try {
|
|
6
|
+
const queued = recordPlatformOperation({ ...input.operation, state: 'queued' });
|
|
7
|
+
operationId = queued.id;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const value = error;
|
|
11
|
+
return input.persistenceFailure({
|
|
12
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
13
|
+
message: value.message || 'Unable to queue platform operation',
|
|
14
|
+
retryable: true
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const selection = input.operation.kind === 'issue-metadata' && input.operation.issueMetadata?.state === 'closed'
|
|
18
|
+
? 'all' : input.operation.dependency;
|
|
19
|
+
const drained = input.drain
|
|
20
|
+
? await input.drain(input.operation.taskRef, operationId, input.agent, input.operation.cwd)
|
|
21
|
+
: await recoverPlatformOperations(input.operation.taskRef, selection, {
|
|
22
|
+
agent: input.agent,
|
|
23
|
+
client: input.client,
|
|
24
|
+
cwd: input.operation.cwd,
|
|
25
|
+
excludeId: operationId
|
|
26
|
+
});
|
|
27
|
+
if (drained.status !== 'applied' && drained.status !== 'no-op') {
|
|
28
|
+
return input.block(drained.error ?? {
|
|
29
|
+
code: 'PLATFORM_OPERATION_QUEUE_BLOCKED',
|
|
30
|
+
message: 'A previous platform operation is still unresolved; the current write remains queued',
|
|
31
|
+
retryable: true
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
recordPlatformOperation({ ...input.operation, state: 'pending' });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const value = error;
|
|
39
|
+
return input.persistenceFailure({
|
|
40
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
41
|
+
message: value.message || 'Unable to mark platform operation as started',
|
|
42
|
+
retryable: true
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
let output;
|
|
46
|
+
try {
|
|
47
|
+
output = await input.execute();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const value = error;
|
|
51
|
+
output = input.block({ code: value.code || 'PLATFORM_WRITE_FAILED', message: value.message || String(error), retryable: true });
|
|
52
|
+
}
|
|
53
|
+
const succeeded = (output.status === 'applied' || output.status === 'no-op')
|
|
54
|
+
&& !output.error && !(output.warnings?.length);
|
|
55
|
+
const state = succeeded ? 'succeeded' : output.status === 'failed' && output.error?.retryable === false ? 'failed' : 'unknown';
|
|
56
|
+
try {
|
|
57
|
+
recordPlatformOperation({
|
|
58
|
+
...input.operation,
|
|
59
|
+
state,
|
|
60
|
+
lastCode: output.error?.code ?? output.warnings?.[0]?.code ?? null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const value = error;
|
|
65
|
+
return input.persistenceFailure({
|
|
66
|
+
code: value.code || 'PLATFORM_OPERATION_JOURNAL_WRITE_FAILED',
|
|
67
|
+
message: value.message || 'Unable to persist platform operation outcome',
|
|
68
|
+
retryable: true
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
73
|
+
export { coordinatePlatformWrite };
|
|
74
|
+
//# sourceMappingURL=operation-coordinator.js.map
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { coordinatePlatformWrite } from './operation-coordinator.js';
|
|
1
5
|
import { resolvePlatformProviderContext } from './context.js';
|
|
2
6
|
import { platformResult } from './types.js';
|
|
3
7
|
import { providerError, providerOperationContext, providerStatus, providerResourceToken, unsupportedProviderOperation } from './provider-bridge.js';
|
|
4
|
-
import { isResourceIdentity, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
|
|
8
|
+
import { isResourceIdentity, resourceIdentityEquals, resourceIdentityNumber, reviewMarker as resourceReviewMarker } from './resource-identity.js';
|
|
9
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
5
10
|
const REVIEW_EVENTS = ['COMMENT', 'APPROVE', 'REQUEST_CHANGES'];
|
|
6
11
|
// Marker-safe scope: a task id (`TASK-YYYYMMDD-HHMMSS`) or a bare PR number
|
|
7
12
|
// (`pr{N}`). Any other value (e.g. one containing `\r\n` or `-->`) would break
|
|
@@ -19,6 +24,34 @@ export function reviewedCommitMarker(sha) {
|
|
|
19
24
|
function normalizeBody(body) {
|
|
20
25
|
return String(body || '').replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
|
|
21
26
|
}
|
|
27
|
+
function canonicalReviewBody(body) {
|
|
28
|
+
return normalizeBody(body).replace(/\n$/u, '');
|
|
29
|
+
}
|
|
30
|
+
function reviewArtifactName(round, artifactFile) {
|
|
31
|
+
return path.basename(artifactFile) === artifactFile
|
|
32
|
+
&& (artifactFile === 'pr-review.md' && round === 1 || artifactFile === `pr-review-r${round}.md`);
|
|
33
|
+
}
|
|
34
|
+
export function readReviewBodyFile(taskRef, round, artifactFile, bodyDigest, cwd) {
|
|
35
|
+
if (!/^TASK-\d{8}-\d{6}$/u.test(taskRef) || path.basename(artifactFile) !== artifactFile
|
|
36
|
+
|| !reviewArtifactName(round, artifactFile)) {
|
|
37
|
+
throw Object.assign(new Error('Pull-request review artifact identity is invalid'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
38
|
+
}
|
|
39
|
+
const task = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
40
|
+
if (!task.ok)
|
|
41
|
+
throw Object.assign(new Error(task.message), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
42
|
+
const bodyFile = artifactFile.replace(/^pr-review/u, 'pr-review-body');
|
|
43
|
+
const report = fs.readFileSync(path.join(task.taskDir, artifactFile), 'utf8');
|
|
44
|
+
const bodyReferences = [...report.matchAll(/^\*\*(?:正文文件|Body file)\*\*(?::|:)\s*`([^`]+)`[^\r\n]*$/gmu)]
|
|
45
|
+
.map((match) => match[1]);
|
|
46
|
+
if (bodyReferences.length !== 1 || bodyReferences[0] !== bodyFile) {
|
|
47
|
+
throw Object.assign(new Error('Pull-request review artifact does not reference its canonical body file'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
48
|
+
}
|
|
49
|
+
const body = canonicalReviewBody(fs.readFileSync(path.join(task.taskDir, bodyFile), 'utf8'));
|
|
50
|
+
if (!body || createHash('sha256').update(body).digest('hex') !== bodyDigest) {
|
|
51
|
+
throw Object.assign(new Error('Canonical pull-request review body does not match its persisted digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
52
|
+
}
|
|
53
|
+
return body;
|
|
54
|
+
}
|
|
22
55
|
function firstLine(body) {
|
|
23
56
|
return normalizeBody(String(body || '')).split('\n', 1)[0] || '';
|
|
24
57
|
}
|
|
@@ -74,6 +107,13 @@ export async function publishPrReview(options) {
|
|
|
74
107
|
if (!hasUsableContext(context)) {
|
|
75
108
|
return platformResult(context.status, { platform: context.platform, capabilities: context.capabilities, error: context.error });
|
|
76
109
|
}
|
|
110
|
+
const providerScopeId = loaded.ok ? loaded.value.snapshot.scope.id : null;
|
|
111
|
+
if (options.expectedProviderScopeId && providerScopeId !== options.expectedProviderScopeId) {
|
|
112
|
+
return platformResult('failed', {
|
|
113
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
114
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current provider scope differs from the persisted operation target', retryable: false }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
77
117
|
const identity = loaded.ok ? (() => { try {
|
|
78
118
|
return providerResourceToken(loaded.value.provider, 'pull-request', String(options.prNumber));
|
|
79
119
|
}
|
|
@@ -87,6 +127,13 @@ export async function publishPrReview(options) {
|
|
|
87
127
|
});
|
|
88
128
|
}
|
|
89
129
|
const identityNumber = resourceIdentityNumber(identity);
|
|
130
|
+
if (options.expectedResource && !resourceIdentityEquals(identity, options.expectedResource)) {
|
|
131
|
+
return platformResult('failed', {
|
|
132
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
133
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
134
|
+
error: { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Current pull request identity differs from the persisted operation target', retryable: false }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
90
137
|
if (!REVIEW_EVENTS.includes(options.event)) {
|
|
91
138
|
return platformResult('failed', {
|
|
92
139
|
platform: context.platform, capabilities: context.capabilities,
|
|
@@ -101,6 +148,65 @@ export async function publishPrReview(options) {
|
|
|
101
148
|
error: { code: 'REVIEW_IDENTITY_INVALID', message: 'review scope must be a task id or pr{N}; commitSha is required', retryable: false }
|
|
102
149
|
});
|
|
103
150
|
}
|
|
151
|
+
if (/^TASK-\d{8}-\d{6}$/u.test(options.identity.scope) && !options.skipQueue && !options.dryRun) {
|
|
152
|
+
if (!options.agent)
|
|
153
|
+
return platformResult('failed', {
|
|
154
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
155
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
156
|
+
error: { code: 'PR_REVIEW_AGENT_REQUIRED', message: 'task-scoped review publication requires an agent token', retryable: false }
|
|
157
|
+
});
|
|
158
|
+
if (!options.recoveryArtifact || !reviewArtifactName(options.identity.round, options.recoveryArtifact))
|
|
159
|
+
return platformResult('failed', {
|
|
160
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
161
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
162
|
+
error: { code: 'PR_REVIEW_ARTIFACT_REQUIRED', message: 'task-scoped review publication requires its canonical review artifact', retryable: false }
|
|
163
|
+
});
|
|
164
|
+
const body = canonicalReviewBody(options.body);
|
|
165
|
+
const bodyDigest = createHash('sha256').update(body).digest('hex');
|
|
166
|
+
try {
|
|
167
|
+
if (readReviewBodyFile(options.identity.scope, options.identity.round, options.recoveryArtifact, bodyDigest, options.cwd) !== body)
|
|
168
|
+
throw new Error('body mismatch');
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return platformResult('failed', {
|
|
172
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
173
|
+
resource: { kind: 'pull-request', number: identityNumber, identity },
|
|
174
|
+
error: { code: 'PR_REVIEW_ARTIFACT_MISMATCH', message: 'Review body must match the canonical review body file', retryable: false }
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const pullRequestReview = {
|
|
178
|
+
prNumber: String(options.prNumber),
|
|
179
|
+
resource: identity,
|
|
180
|
+
providerScopeId: providerScopeId,
|
|
181
|
+
scope: options.identity.scope,
|
|
182
|
+
round: options.identity.round,
|
|
183
|
+
commitSha: options.identity.commitSha,
|
|
184
|
+
event: options.event,
|
|
185
|
+
artifactFile: options.recoveryArtifact,
|
|
186
|
+
bodyDigest
|
|
187
|
+
};
|
|
188
|
+
return coordinatePlatformWrite({
|
|
189
|
+
operation: {
|
|
190
|
+
taskRef: options.identity.scope,
|
|
191
|
+
cwd: options.cwd || process.cwd(),
|
|
192
|
+
kind: 'pull-request-review',
|
|
193
|
+
target: JSON.stringify(identity),
|
|
194
|
+
expectedDigest: createHash('sha256').update(JSON.stringify(pullRequestReview)).digest('hex'),
|
|
195
|
+
dependency: 'deferred',
|
|
196
|
+
pullRequestReview
|
|
197
|
+
},
|
|
198
|
+
agent: options.agent,
|
|
199
|
+
execute: () => publishPrReview({ ...options, expectedProviderScopeId: providerScopeId, skipQueue: true }),
|
|
200
|
+
block: (error) => platformResult('blocked', {
|
|
201
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
202
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
203
|
+
}),
|
|
204
|
+
persistenceFailure: (error) => platformResult('failed', {
|
|
205
|
+
platform: context.platform, capabilities: context.capabilities,
|
|
206
|
+
resource: { kind: 'pull-request', number: identityNumber, identity }, error
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
104
210
|
const marker = reviewMarker({ ...options.identity, resource: identity });
|
|
105
211
|
const listed = await listPrReviews(options.prNumber, { cwd: options.cwd, client: options.client });
|
|
106
212
|
if (listed.status === 'failed' || listed.status === 'blocked')
|