@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
package/lib/task/finalization.ts
CHANGED
|
@@ -7,13 +7,19 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
|
7
7
|
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.ts';
|
|
8
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
9
9
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
10
|
+
import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
|
|
11
|
+
import type { IssueResult, SyncOptions as IssueSyncOptions } from '../platform/issues.ts';
|
|
12
|
+
import { resourceIdentityEquals } from '../platform/resource-identity.ts';
|
|
10
13
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
14
|
+
import { recoverPlatformOperations } from './platform-operation-recovery.ts';
|
|
11
15
|
import {
|
|
12
16
|
applyTaskLifecycle,
|
|
13
17
|
inspectTaskLifecycleProgress,
|
|
14
18
|
type TaskLifecycleOptions,
|
|
15
19
|
type TaskLifecycleRequest,
|
|
16
|
-
type TaskLifecycleResult
|
|
20
|
+
type TaskLifecycleResult,
|
|
21
|
+
type TaskCompletionProjection,
|
|
22
|
+
previewTaskCompletion
|
|
17
23
|
} from './lifecycle.ts';
|
|
18
24
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
19
25
|
import { inspectShortIdRegistry } from './short-id.ts';
|
|
@@ -71,6 +77,8 @@ type TaskFinalizationOptions = Readonly<{
|
|
|
71
77
|
lifecycle?: typeof applyTaskLifecycle;
|
|
72
78
|
backfill?: typeof backfillCompletionComments;
|
|
73
79
|
commentSync?: typeof syncPlatformComment;
|
|
80
|
+
issueSync?: typeof syncPlatformIssue;
|
|
81
|
+
issueInspect?: typeof inspectPlatformIssue;
|
|
74
82
|
verify?: typeof verifyTaskEvent;
|
|
75
83
|
preflight?: typeof verifyTaskEvent;
|
|
76
84
|
}>;
|
|
@@ -471,13 +479,13 @@ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_S
|
|
|
471
479
|
return error;
|
|
472
480
|
}
|
|
473
481
|
|
|
474
|
-
function
|
|
482
|
+
function hasFinalizationTaskDocument(repoRoot: string, taskId: string): boolean {
|
|
475
483
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
476
|
-
if (!resolved.ok) return false;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
catch { return
|
|
484
|
+
if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed')) return false;
|
|
485
|
+
try {
|
|
486
|
+
const journal = JSON.parse(fs.readFileSync(path.join(resolved.taskDir, '.task-lifecycle.json'), 'utf8')) as Record<string, unknown>;
|
|
487
|
+
return journal.taskId === taskId && journal.intent === 'complete';
|
|
488
|
+
} catch { return resolved.state === 'completed'; }
|
|
481
489
|
}
|
|
482
490
|
|
|
483
491
|
function mutationKeys(value: FinalizationMutation): string[] {
|
|
@@ -525,7 +533,14 @@ function mutationPatch(current: TaskFinalizationReceipt, mutation: FinalizationM
|
|
|
525
533
|
if (mutation.scope === 'task-comment') {
|
|
526
534
|
if (mutation.operation === 'succeeded') {
|
|
527
535
|
const warnings = resolveStepWarnings(current, 'task-comment');
|
|
528
|
-
|
|
536
|
+
const retryWithResolvedWarning = mutation.state === 'done'
|
|
537
|
+
&& current.warnings.some((warning) => warning.step === 'task-comment' && warning.status === 'open');
|
|
538
|
+
return {
|
|
539
|
+
taskComment: retryWithResolvedWarning ? 'pending' : mutation.state,
|
|
540
|
+
warningProjection: warnings.length > 0 || retryWithResolvedWarning ? 'pending' : 'done',
|
|
541
|
+
warnings,
|
|
542
|
+
lastError: null
|
|
543
|
+
};
|
|
529
544
|
}
|
|
530
545
|
const warnings = replaceWarning(current, warningFromError('task-comment', mutation.error), 'open');
|
|
531
546
|
return { taskComment: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
|
|
@@ -557,8 +572,8 @@ function applyFinalizationReceiptMutationUnderLock(
|
|
|
557
572
|
mutation: FinalizationMutation,
|
|
558
573
|
consumed: Set<string>
|
|
559
574
|
): TaskFinalizationReceipt {
|
|
560
|
-
if (!
|
|
561
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active
|
|
575
|
+
if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
|
|
576
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
|
|
562
577
|
}
|
|
563
578
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
564
579
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -576,8 +591,8 @@ function applyFinalizationReceiptMutation(
|
|
|
576
591
|
capability: FinalizationCapability,
|
|
577
592
|
mutation: FinalizationMutation
|
|
578
593
|
): TaskFinalizationReceipt {
|
|
579
|
-
if (!
|
|
580
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active
|
|
594
|
+
if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
|
|
595
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
|
|
581
596
|
}
|
|
582
597
|
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () =>
|
|
583
598
|
applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
|
|
@@ -627,6 +642,8 @@ async function syncPendingTaskComment(input: {
|
|
|
627
642
|
taskId: string;
|
|
628
643
|
agent: string;
|
|
629
644
|
receipt: TaskFinalizationReceipt;
|
|
645
|
+
projection?: TaskCompletionProjection;
|
|
646
|
+
refreshProjection?: () => TaskCompletionProjection | undefined;
|
|
630
647
|
commentSync: typeof syncPlatformComment;
|
|
631
648
|
consumedCapabilities: Set<string>;
|
|
632
649
|
}): Promise<TaskCommentSyncOutcome> {
|
|
@@ -641,7 +658,15 @@ async function syncPendingTaskComment(input: {
|
|
|
641
658
|
};
|
|
642
659
|
}
|
|
643
660
|
try {
|
|
644
|
-
const result = await commentSync(taskId, {
|
|
661
|
+
const result = await commentSync(taskId, {
|
|
662
|
+
kind: 'task', agent, cwd: repoRoot, dependency: 'required',
|
|
663
|
+
...(input.projection ? {
|
|
664
|
+
taskProjection: {
|
|
665
|
+
content: input.projection.content,
|
|
666
|
+
sha256: createHash('sha256').update(input.projection.content).digest('hex')
|
|
667
|
+
}
|
|
668
|
+
} : {})
|
|
669
|
+
});
|
|
645
670
|
const step = commentStep(result);
|
|
646
671
|
if (result.status === 'applied' || result.status === 'no-op') {
|
|
647
672
|
const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
|
|
@@ -650,6 +675,14 @@ async function syncPendingTaskComment(input: {
|
|
|
650
675
|
scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
|
|
651
676
|
}, consumedCapabilities);
|
|
652
677
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
678
|
+
if (!skipped && receipt.taskComment === 'pending') {
|
|
679
|
+
const retry = await syncPendingTaskComment({
|
|
680
|
+
...input,
|
|
681
|
+
receipt,
|
|
682
|
+
projection: input.refreshProjection?.() ?? input.projection
|
|
683
|
+
});
|
|
684
|
+
return { ...retry, changed: result.changed || retry.changed };
|
|
685
|
+
}
|
|
653
686
|
return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
|
|
654
687
|
}
|
|
655
688
|
const detail = step.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
|
|
@@ -702,7 +735,7 @@ async function syncPendingSummary(input: {
|
|
|
702
735
|
let { receipt } = input;
|
|
703
736
|
try {
|
|
704
737
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
705
|
-
if (!resolved.ok ||
|
|
738
|
+
if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed')) throw Object.assign(new Error('task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
706
739
|
if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
|
|
707
740
|
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
|
|
708
741
|
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
@@ -716,7 +749,8 @@ async function syncPendingSummary(input: {
|
|
|
716
749
|
const summary = readDeliverySummary(resolved.taskDir, input.taskId);
|
|
717
750
|
const result = await input.commentSync(input.taskId, {
|
|
718
751
|
kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
|
|
719
|
-
summaryAuthorization: { sha256: summary.sha256 }
|
|
752
|
+
summaryAuthorization: { sha256: summary.sha256 },
|
|
753
|
+
dependency: 'required'
|
|
720
754
|
});
|
|
721
755
|
const step = commentStep(result);
|
|
722
756
|
if (result.status === 'applied' || result.status === 'no-op') {
|
|
@@ -726,7 +760,7 @@ async function syncPendingSummary(input: {
|
|
|
726
760
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
727
761
|
summary: skipped ? 'skipped' : 'done',
|
|
728
762
|
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
729
|
-
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
763
|
+
warningProjection: warnings.some((warning) => warning.status === 'open') || resolvedWarning ? 'pending' : 'done',
|
|
730
764
|
warnings,
|
|
731
765
|
lastError: null
|
|
732
766
|
});
|
|
@@ -814,6 +848,8 @@ async function prepareUnderLock(
|
|
|
814
848
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
815
849
|
const backfill = options.backfill ?? backfillCompletionComments;
|
|
816
850
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
851
|
+
const issueSync = options.issueSync ?? syncPlatformIssue;
|
|
852
|
+
const issueInspect = options.issueInspect ?? inspectPlatformIssue;
|
|
817
853
|
const verify = options.verify ?? verifyTaskEvent;
|
|
818
854
|
const consumedCapabilities = new Set<string>();
|
|
819
855
|
let receipt: TaskFinalizationReceipt;
|
|
@@ -858,6 +894,90 @@ async function prepareUnderLock(
|
|
|
858
894
|
}
|
|
859
895
|
}
|
|
860
896
|
|
|
897
|
+
if (preflightState.ok && preflightState.state === 'active') {
|
|
898
|
+
const recovery = await recoverPlatformOperations(taskId, 'all', { agent: request.agent, cwd: repoRoot });
|
|
899
|
+
if (recovery.status !== 'applied' && recovery.status !== 'no-op') {
|
|
900
|
+
return failed(taskId, recovery.error ?? {
|
|
901
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
902
|
+
message: 'Pending platform operations remain unresolved',
|
|
903
|
+
retryable: true
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
const taskContent = fs.readFileSync(preflightState.taskMdPath, 'utf8');
|
|
907
|
+
const taskFrontmatter = parseTaskFrontmatter(taskContent);
|
|
908
|
+
const issueIdentity = taskIssueIdentity(taskFrontmatter);
|
|
909
|
+
if (issueIdentity) {
|
|
910
|
+
const base = typeof taskFrontmatter.delivery_base_ref === 'string' ? taskFrontmatter.delivery_base_ref.trim() : '';
|
|
911
|
+
let synced: IssueResult;
|
|
912
|
+
try {
|
|
913
|
+
synced = await issueSync(taskId, {
|
|
914
|
+
agent: request.agent, cwd: repoRoot, requirements: true, issueType: true, fields: true,
|
|
915
|
+
...(base ? { inLabels: 'from-diff', base } : {}), dependency: 'required'
|
|
916
|
+
} satisfies IssueSyncOptions);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const detail = errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true);
|
|
919
|
+
return failed(taskId, detail);
|
|
920
|
+
}
|
|
921
|
+
const issueOperations = synced.operations as Array<{ name: string; status: string; reasonCode: string | null; value?: unknown }>;
|
|
922
|
+
const badOperation = issueOperations?.find((operation) =>
|
|
923
|
+
['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
|
|
924
|
+
&& operation.status !== 'applied' && operation.status !== 'no-op'
|
|
925
|
+
);
|
|
926
|
+
if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
|
|
927
|
+
const error = synced.error ?? {
|
|
928
|
+
code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
929
|
+
message: `Required Issue metadata operation '${badOperation?.name ?? 'sync'}' was not confirmed`,
|
|
930
|
+
retryable: true
|
|
931
|
+
};
|
|
932
|
+
return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
|
|
933
|
+
}
|
|
934
|
+
let inspected: IssueResult;
|
|
935
|
+
try { inspected = await issueInspect(taskId, { cwd: repoRoot }); }
|
|
936
|
+
catch (error) { return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true)); }
|
|
937
|
+
if (inspected.status !== 'no-op' || inspected.error || !inspected.issue
|
|
938
|
+
|| !resourceIdentityEquals(inspected.issue.identity, issueIdentity)) {
|
|
939
|
+
return failed(taskId, inspected.error ?? {
|
|
940
|
+
code: 'FINALIZATION_ISSUE_IDENTITY_UNCONFIRMED',
|
|
941
|
+
message: 'Required Issue metadata could not be reread under the bound Issue identity',
|
|
942
|
+
retryable: true
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
for (const name of ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])]) {
|
|
946
|
+
const operation = issueOperations?.find((candidate) => candidate.name === name);
|
|
947
|
+
if (!operation) return failed(taskId, {
|
|
948
|
+
code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
949
|
+
message: `Required Issue metadata operation '${name}' was not reported`, retryable: true
|
|
950
|
+
});
|
|
951
|
+
const before = synced.issue;
|
|
952
|
+
const remote = inspected.issue;
|
|
953
|
+
if (name === 'requirements') {
|
|
954
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.body;
|
|
955
|
+
if (expected === undefined || remote.body !== expected) return failed(taskId, {
|
|
956
|
+
code: 'FINALIZATION_ISSUE_REQUIREMENTS_UNCONFIRMED', message: 'Issue requirements did not match the verified target', retryable: true
|
|
957
|
+
});
|
|
958
|
+
} else if (name === 'issue-type') {
|
|
959
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.issueType;
|
|
960
|
+
if (!expected || remote.issueType !== expected) return failed(taskId, {
|
|
961
|
+
code: 'FINALIZATION_ISSUE_TYPE_UNCONFIRMED', message: 'Issue Type did not match the verified target', retryable: true
|
|
962
|
+
});
|
|
963
|
+
} else if (name === 'labels:in') {
|
|
964
|
+
const expected = Array.isArray(operation.value) ? operation.value as string[] : before?.labels;
|
|
965
|
+
if (!expected || expected.filter((label) => label.startsWith('in:')).sort().join('\0')
|
|
966
|
+
!== remote.labels.filter((label) => label.startsWith('in:')).sort().join('\0')) return failed(taskId, {
|
|
967
|
+
code: 'FINALIZATION_IN_LABELS_UNCONFIRMED', message: 'Issue in-labels did not match the verified target', retryable: true
|
|
968
|
+
});
|
|
969
|
+
} else {
|
|
970
|
+
const expected = operation.value && typeof operation.value === 'object'
|
|
971
|
+
? operation.value as Record<string, string | number>
|
|
972
|
+
: before?.fields;
|
|
973
|
+
if (!expected || Object.entries(expected).some(([field, value]) => remote.fields[field] !== value)) return failed(taskId, {
|
|
974
|
+
code: 'FINALIZATION_ISSUE_FIELDS_UNCONFIRMED', message: 'Pinned Issue fields did not match the verified target', retryable: true
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
861
981
|
let changed = false;
|
|
862
982
|
let backfillResult: TaskFinalizationStep | null = null;
|
|
863
983
|
let runBackfill = shouldRunBackfill(receipt);
|
|
@@ -921,9 +1041,10 @@ async function prepareUnderLock(
|
|
|
921
1041
|
} else backfillResult = { status: 'no-op', changed: false, error: null };
|
|
922
1042
|
|
|
923
1043
|
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
1044
|
+
let projection: TaskCompletionProjection | undefined;
|
|
924
1045
|
try {
|
|
925
1046
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
926
|
-
if (
|
|
1047
|
+
if (resolved.ok && resolved.state === 'completed') {
|
|
927
1048
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
928
1049
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
929
1050
|
} else {
|
|
@@ -937,6 +1058,12 @@ async function prepareUnderLock(
|
|
|
937
1058
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
938
1059
|
}
|
|
939
1060
|
changed = changed || result.changed;
|
|
1061
|
+
if (result.timestamp && result.agentInfraVersion) {
|
|
1062
|
+
projection = previewTaskCompletion(taskId, request.agent, {
|
|
1063
|
+
repoRoot,
|
|
1064
|
+
metadataProvider: () => ({ timestamp: result.timestamp!, agentInfraVersion: result.agentInfraVersion! })
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
940
1067
|
}
|
|
941
1068
|
} catch (error) {
|
|
942
1069
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
@@ -946,6 +1073,12 @@ async function prepareUnderLock(
|
|
|
946
1073
|
|
|
947
1074
|
let taskComment: TaskFinalizationStep | null = null;
|
|
948
1075
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1076
|
+
const currentProjection = (): TaskCompletionProjection | undefined => projection
|
|
1077
|
+
? previewTaskCompletion(taskId, request.agent, {
|
|
1078
|
+
repoRoot,
|
|
1079
|
+
metadataProvider: () => ({ timestamp: projection!.timestamp, agentInfraVersion: projection!.agentInfraVersion })
|
|
1080
|
+
})
|
|
1081
|
+
: undefined;
|
|
949
1082
|
|
|
950
1083
|
let verification: TaskFinalizationStep | null = null;
|
|
951
1084
|
if (receipt.verification !== 'pending') {
|
|
@@ -971,7 +1104,7 @@ async function prepareUnderLock(
|
|
|
971
1104
|
});
|
|
972
1105
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
973
1106
|
const finalComment = await syncPendingTaskComment({
|
|
974
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1107
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
975
1108
|
});
|
|
976
1109
|
receipt = finalComment.receipt;
|
|
977
1110
|
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
@@ -987,7 +1120,7 @@ async function prepareUnderLock(
|
|
|
987
1120
|
});
|
|
988
1121
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
989
1122
|
const warningComment = await syncPendingTaskComment({
|
|
990
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1123
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
991
1124
|
});
|
|
992
1125
|
receipt = warningComment.receipt;
|
|
993
1126
|
taskComment = warningComment.step;
|
|
@@ -1017,7 +1150,7 @@ async function prepareUnderLock(
|
|
|
1017
1150
|
}, changed, hardError);
|
|
1018
1151
|
}
|
|
1019
1152
|
try {
|
|
1020
|
-
|
|
1153
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities });
|
|
1021
1154
|
receipt = warningComment.receipt;
|
|
1022
1155
|
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
1023
1156
|
changed = changed || warningComment.changed;
|
|
@@ -1038,7 +1171,7 @@ async function prepareUnderLock(
|
|
|
1038
1171
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1039
1172
|
if (receipt.taskComment === 'pending') {
|
|
1040
1173
|
const finalComment = await syncPendingTaskComment({
|
|
1041
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1174
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
1042
1175
|
});
|
|
1043
1176
|
receipt = finalComment.receipt;
|
|
1044
1177
|
taskComment = finalComment.step;
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -5,7 +5,8 @@ import path from 'node:path';
|
|
|
5
5
|
import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
|
|
6
6
|
import { artifactFamilyCatalog, inspectArtifactDirectory } from './artifact-lifecycle.ts';
|
|
7
7
|
import { parseArtifactName } from './artifact-name.ts';
|
|
8
|
-
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
8
|
+
import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.ts';
|
|
9
|
+
import { upsertSection } from './sections.ts';
|
|
9
10
|
import { locateHotTaskDirs, resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
|
|
10
11
|
import {
|
|
11
12
|
configuredShortIdLength, executeShortIdCommand, loadShortIdByTaskId,
|
|
@@ -112,6 +113,8 @@ type TaskLifecycleOptions = {
|
|
|
112
113
|
prepareOnly?: boolean;
|
|
113
114
|
};
|
|
114
115
|
|
|
116
|
+
type TaskCompletionProjection = Readonly<{ taskId: string; content: string; timestamp: string; agentInfraVersion: string }>;
|
|
117
|
+
|
|
115
118
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
116
119
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
117
120
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -304,6 +307,25 @@ function mutationsFor(request: TaskLifecycleRequest, content: string, metadata:
|
|
|
304
307
|
return mutations;
|
|
305
308
|
}
|
|
306
309
|
|
|
310
|
+
function previewTaskCompletion(taskRef: string, agent: string, options: Pick<TaskLifecycleOptions, 'repoRoot' | 'metadataProvider'> = {}): TaskCompletionProjection {
|
|
311
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
312
|
+
if (!resolved.ok) throw Object.assign(new Error(resolved.message), { code: resolved.code });
|
|
313
|
+
if (resolved.state !== 'active') throw Object.assign(new Error(`task ${resolved.taskId} is ${resolved.state}, expected active`), { code: 'TASK_STATE_MISMATCH' });
|
|
314
|
+
const original = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
315
|
+
const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
|
|
316
|
+
if (!isValidAgentInfraVersion(metadata.agentInfraVersion)) throw Object.assign(new Error('metadata agentInfraVersion is invalid'), { code: 'LIFECYCLE_METADATA_FAILED' });
|
|
317
|
+
const mutations = mutationsFor({ taskRef: resolved.taskId, intent: 'complete', agent }, original, metadata, 0);
|
|
318
|
+
if (!Array.isArray(mutations)) throw Object.assign(new Error(mutations.message), { code: mutations.code });
|
|
319
|
+
let content = original;
|
|
320
|
+
for (const mutation of mutations) {
|
|
321
|
+
if (mutation.kind === 'frontmatter') content = updateTaskFrontmatter(content, mutation.set, mutation.remove);
|
|
322
|
+
else if (mutation.kind === 'section') content = upsertSection(content, mutation).content;
|
|
323
|
+
else throw Object.assign(new Error('completion projection contains an unsupported mutation'), { code: 'LIFECYCLE_PROJECTION_INVALID' });
|
|
324
|
+
}
|
|
325
|
+
content = updateTaskFrontmatter(content, { updated_at: metadata.timestamp, agent_infra_version: metadata.agentInfraVersion });
|
|
326
|
+
return { taskId: resolved.taskId, content, timestamp: metadata.timestamp, agentInfraVersion: metadata.agentInfraVersion };
|
|
327
|
+
}
|
|
328
|
+
|
|
307
329
|
function writeJournal(file: string, journal: LifecycleJournal, io: LifecycleFileSystem, create = false): void {
|
|
308
330
|
const temporary = `${file}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
309
331
|
io.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, 'wx');
|
|
@@ -590,6 +612,16 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
590
612
|
if (completed.has('directory-moved')) completed.add('registry-committed');
|
|
591
613
|
}
|
|
592
614
|
let taskOperations: readonly TaskOperationSummary[] = [];
|
|
615
|
+
if (options.prepareOnly && !matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) {
|
|
616
|
+
return {
|
|
617
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
618
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
619
|
+
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
620
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
621
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
622
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
623
|
+
};
|
|
624
|
+
}
|
|
593
625
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
594
626
|
if (!completed.has('task-written')) {
|
|
595
627
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
@@ -687,8 +719,8 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
687
719
|
};
|
|
688
720
|
}
|
|
689
721
|
|
|
690
|
-
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
|
|
722
|
+
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle, previewTaskCompletion };
|
|
691
723
|
export type {
|
|
692
724
|
TaskLifecycleIntent, TaskLifecycleRequest, TaskLifecycleResult, TaskLifecycleOptions,
|
|
693
|
-
LifecycleError, LifecycleJournal, LifecycleStep
|
|
725
|
+
LifecycleError, LifecycleJournal, LifecycleStep, TaskCompletionProjection
|
|
694
726
|
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import { isResourceIdentity } from '../platform/resource-identity.ts';
|
|
6
|
+
import type { ResourceIdentity } from '../platform/resource-identity.ts';
|
|
7
|
+
import { resolveTaskRef } from './resolve-ref.ts';
|
|
8
|
+
|
|
9
|
+
const JOURNAL_FILE = '.platform-operations.json';
|
|
10
|
+
const MAX_ATTEMPTS = 3;
|
|
11
|
+
|
|
12
|
+
type PlatformOperationKind = 'task-comment' | 'artifact-comment' | 'summary-comment' | 'cancel-comment' | 'issue-create' | 'issue-metadata' | 'pull-request' | 'pull-request-summary' | 'pull-request-review';
|
|
13
|
+
type PlatformOperationState = 'queued' | 'pending' | 'unknown' | 'succeeded' | 'failed';
|
|
14
|
+
type PlatformIssueMetadataIntent = Readonly<{
|
|
15
|
+
requirements: boolean;
|
|
16
|
+
issueType: boolean;
|
|
17
|
+
fields: boolean;
|
|
18
|
+
status?: string;
|
|
19
|
+
assignees?: 'current' | 'none';
|
|
20
|
+
milestone?: 'initial' | 'specific' | 'none';
|
|
21
|
+
inLabels?: 'from-diff' | 'none';
|
|
22
|
+
base?: string;
|
|
23
|
+
fromDiffFiles?: readonly string[];
|
|
24
|
+
inLabelMappingDigest?: string;
|
|
25
|
+
state?: 'open' | 'closed';
|
|
26
|
+
closeReason?: 'completed' | 'not_planned';
|
|
27
|
+
}>;
|
|
28
|
+
type PlatformPullRequestIntent = Readonly<{
|
|
29
|
+
action: 'create' | 'bind' | 'sync';
|
|
30
|
+
baseRef?: string;
|
|
31
|
+
headRef?: string;
|
|
32
|
+
prToken?: string;
|
|
33
|
+
metadata?: boolean;
|
|
34
|
+
closingIssue?: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
type PlatformIssueCreateIntent = Readonly<{ title: string; bodyDigest: string }>;
|
|
37
|
+
type PlatformPullRequestSummaryIntent = Readonly<{ body: string; changeReportFile: string }>;
|
|
38
|
+
type PlatformPullRequestReviewIntent = Readonly<{
|
|
39
|
+
prNumber: string;
|
|
40
|
+
resource: ResourceIdentity;
|
|
41
|
+
providerScopeId: string;
|
|
42
|
+
scope: string;
|
|
43
|
+
round: number;
|
|
44
|
+
commitSha: string;
|
|
45
|
+
event: 'COMMENT' | 'APPROVE' | 'REQUEST_CHANGES';
|
|
46
|
+
artifactFile: string;
|
|
47
|
+
bodyDigest: string;
|
|
48
|
+
}>;
|
|
49
|
+
type PlatformOperation = Readonly<{
|
|
50
|
+
id: string;
|
|
51
|
+
kind: PlatformOperationKind;
|
|
52
|
+
target: string;
|
|
53
|
+
expectedDigest: string;
|
|
54
|
+
dependency: 'deferred' | 'required';
|
|
55
|
+
state: PlatformOperationState;
|
|
56
|
+
attempts: number;
|
|
57
|
+
maxAttempts: typeof MAX_ATTEMPTS;
|
|
58
|
+
lastCode: string | null;
|
|
59
|
+
issueMetadata?: PlatformIssueMetadataIntent;
|
|
60
|
+
issueCreate?: PlatformIssueCreateIntent;
|
|
61
|
+
pullRequest?: PlatformPullRequestIntent;
|
|
62
|
+
pullRequestSummary?: PlatformPullRequestSummaryIntent;
|
|
63
|
+
pullRequestReview?: PlatformPullRequestReviewIntent;
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
}>;
|
|
66
|
+
type PlatformOperationJournal = Readonly<{
|
|
67
|
+
version: 1;
|
|
68
|
+
taskId: string;
|
|
69
|
+
operations: readonly PlatformOperation[];
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
type RecordOperationInput = Readonly<{
|
|
73
|
+
taskRef: string;
|
|
74
|
+
kind: PlatformOperationKind;
|
|
75
|
+
target: string;
|
|
76
|
+
expectedDigest: string;
|
|
77
|
+
dependency: 'deferred' | 'required';
|
|
78
|
+
state: PlatformOperationState;
|
|
79
|
+
lastCode?: string | null;
|
|
80
|
+
issueMetadata?: PlatformIssueMetadataIntent;
|
|
81
|
+
issueCreate?: PlatformIssueCreateIntent;
|
|
82
|
+
pullRequest?: PlatformPullRequestIntent;
|
|
83
|
+
pullRequestSummary?: PlatformPullRequestSummaryIntent;
|
|
84
|
+
pullRequestReview?: PlatformPullRequestReviewIntent;
|
|
85
|
+
cwd?: string;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
function operationId(input: Pick<RecordOperationInput, 'kind' | 'target' | 'expectedDigest'>): string {
|
|
89
|
+
return createHash('sha256')
|
|
90
|
+
.update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
|
|
91
|
+
.digest('hex');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function journalPath(taskDir: string): string {
|
|
95
|
+
return path.join(taskDir, JOURNAL_FILE);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseJournal(file: string, taskId: string): PlatformOperationJournal {
|
|
99
|
+
let value: unknown;
|
|
100
|
+
try { value = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
101
|
+
catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { version: 1, taskId, operations: [] };
|
|
103
|
+
throw Object.assign(new Error(`Platform operation journal is unreadable: ${String(error)}`), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
104
|
+
}
|
|
105
|
+
if (!value || typeof value !== 'object') throw Object.assign(new Error('Platform operation journal is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
106
|
+
const journal = value as Partial<PlatformOperationJournal>;
|
|
107
|
+
if (journal.version !== 1 || journal.taskId !== taskId || !Array.isArray(journal.operations)) {
|
|
108
|
+
throw Object.assign(new Error('Platform operation journal identity or version is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
109
|
+
}
|
|
110
|
+
for (const item of journal.operations) {
|
|
111
|
+
if (!item || typeof item !== 'object' || !/^[a-f0-9]{64}$/u.test(item.id)
|
|
112
|
+
|| !['task-comment', 'artifact-comment', 'summary-comment', 'cancel-comment', 'issue-create', 'issue-metadata', 'pull-request', 'pull-request-summary', 'pull-request-review'].includes(item.kind)
|
|
113
|
+
|| typeof item.target !== 'string' || !item.target
|
|
114
|
+
|| !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
|
|
115
|
+
|| !['deferred', 'required'].includes(item.dependency)
|
|
116
|
+
|| !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
|
|
117
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0 || item.attempts > MAX_ATTEMPTS
|
|
118
|
+
|| item.maxAttempts !== MAX_ATTEMPTS
|
|
119
|
+
|| !(item.lastCode === null || typeof item.lastCode === 'string')
|
|
120
|
+
|| (item.kind === 'issue-metadata'
|
|
121
|
+
? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
|
|
122
|
+
|| typeof item.issueMetadata.issueType !== 'boolean' || typeof item.issueMetadata.fields !== 'boolean'
|
|
123
|
+
|| (item.issueMetadata.status !== undefined && typeof item.issueMetadata.status !== 'string')
|
|
124
|
+
|| (item.issueMetadata.assignees !== undefined && !['current', 'none'].includes(item.issueMetadata.assignees))
|
|
125
|
+
|| (item.issueMetadata.milestone !== undefined && !['initial', 'specific', 'none'].includes(item.issueMetadata.milestone))
|
|
126
|
+
|| (item.issueMetadata.inLabels !== undefined && !['from-diff', 'none'].includes(item.issueMetadata.inLabels))
|
|
127
|
+
|| (item.issueMetadata.base !== undefined && (typeof item.issueMetadata.base !== 'string' || !item.issueMetadata.base.trim()))
|
|
128
|
+
|| (item.issueMetadata.fromDiffFiles !== undefined && (!Array.isArray(item.issueMetadata.fromDiffFiles)
|
|
129
|
+
|| item.issueMetadata.fromDiffFiles.some((file: unknown) => typeof file !== 'string')))
|
|
130
|
+
|| (item.issueMetadata.inLabelMappingDigest !== undefined && !/^[a-f0-9]{64}$/u.test(item.issueMetadata.inLabelMappingDigest))
|
|
131
|
+
|| (item.issueMetadata.state !== undefined && !['open', 'closed'].includes(item.issueMetadata.state))
|
|
132
|
+
|| (item.issueMetadata.closeReason !== undefined && !['completed', 'not_planned'].includes(item.issueMetadata.closeReason))
|
|
133
|
+
|| (!item.issueMetadata.requirements && !item.issueMetadata.issueType && !item.issueMetadata.fields
|
|
134
|
+
&& item.issueMetadata.status === undefined && item.issueMetadata.assignees === undefined
|
|
135
|
+
&& item.issueMetadata.milestone === undefined && item.issueMetadata.inLabels === undefined && item.issueMetadata.state === undefined)
|
|
136
|
+
: item.issueMetadata !== undefined)
|
|
137
|
+
|| (item.kind === 'pull-request'
|
|
138
|
+
? !item.pullRequest || !['create', 'bind', 'sync'].includes(item.pullRequest.action)
|
|
139
|
+
|| Object.entries(item.pullRequest).some(([key, field]) => key !== 'action'
|
|
140
|
+
&& key !== 'metadata' && key !== 'closingIssue' && (typeof field !== 'string' || !field.trim()))
|
|
141
|
+
|| (item.pullRequest.metadata !== undefined && typeof item.pullRequest.metadata !== 'boolean')
|
|
142
|
+
|| (item.pullRequest.closingIssue !== undefined && typeof item.pullRequest.closingIssue !== 'boolean')
|
|
143
|
+
: item.pullRequest !== undefined)
|
|
144
|
+
|| (item.kind === 'issue-create'
|
|
145
|
+
? !item.issueCreate || typeof item.issueCreate.title !== 'string' || !item.issueCreate.title.trim()
|
|
146
|
+
|| !/^[a-f0-9]{64}$/u.test(item.issueCreate.bodyDigest)
|
|
147
|
+
: item.issueCreate !== undefined)
|
|
148
|
+
|| (item.kind === 'pull-request-summary'
|
|
149
|
+
? !item.pullRequestSummary || typeof item.pullRequestSummary.body !== 'string'
|
|
150
|
+
|| !item.pullRequestSummary.body.trim() || typeof item.pullRequestSummary.changeReportFile !== 'string'
|
|
151
|
+
|| !item.pullRequestSummary.changeReportFile.trim()
|
|
152
|
+
: item.pullRequestSummary !== undefined)
|
|
153
|
+
|| (item.kind === 'pull-request-review'
|
|
154
|
+
? !item.pullRequestReview || typeof item.pullRequestReview.prNumber !== 'string'
|
|
155
|
+
|| !isResourceIdentity(item.pullRequestReview.resource)
|
|
156
|
+
|| typeof item.pullRequestReview.providerScopeId !== 'string' || !item.pullRequestReview.providerScopeId.trim()
|
|
157
|
+
|| !item.pullRequestReview.prNumber.trim() || !/^TASK-\d{8}-\d{6}$|^pr\d+$/u.test(item.pullRequestReview.scope)
|
|
158
|
+
|| !Number.isSafeInteger(item.pullRequestReview.round) || item.pullRequestReview.round <= 0
|
|
159
|
+
|| !/^[0-9a-f]{7,40}$/iu.test(item.pullRequestReview.commitSha)
|
|
160
|
+
|| !['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(item.pullRequestReview.event)
|
|
161
|
+
|| typeof item.pullRequestReview.artifactFile !== 'string'
|
|
162
|
+
|| !/^pr-review(?:-r\d+)?\.md$/u.test(item.pullRequestReview.artifactFile)
|
|
163
|
+
|| (item.pullRequestReview.artifactFile === 'pr-review.md'
|
|
164
|
+
? item.pullRequestReview.round !== 1
|
|
165
|
+
: item.pullRequestReview.artifactFile !== `pr-review-r${item.pullRequestReview.round}.md`)
|
|
166
|
+
|| !/^[a-f0-9]{64}$/u.test(item.pullRequestReview.bodyDigest)
|
|
167
|
+
: item.pullRequestReview !== undefined)
|
|
168
|
+
|| typeof item.updatedAt !== 'string') {
|
|
169
|
+
throw Object.assign(new Error('Platform operation journal contains an invalid operation'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return journal as PlatformOperationJournal;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeJournal(file: string, journal: PlatformOperationJournal): void {
|
|
176
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
177
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
178
|
+
try {
|
|
179
|
+
fs.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
180
|
+
fs.renameSync(temporary, file);
|
|
181
|
+
} finally {
|
|
182
|
+
try { fs.unlinkSync(temporary); } catch { /* renamed or already removed */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveJournal(taskRef: string, cwd?: string): { taskId: string; file: string } {
|
|
187
|
+
const resolved = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
188
|
+
if (!resolved.ok) throw Object.assign(new Error(resolved.message), { code: resolved.code });
|
|
189
|
+
return { taskId: resolved.taskId, file: journalPath(resolved.taskDir) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function recordPlatformOperation(input: RecordOperationInput): PlatformOperation {
|
|
193
|
+
if (!/^[a-f0-9]{64}$/u.test(input.expectedDigest) || !input.target.trim()) {
|
|
194
|
+
throw Object.assign(new Error('Platform operation requires a stable target and SHA-256 digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
195
|
+
}
|
|
196
|
+
const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
|
|
197
|
+
const journal = parseJournal(file, taskId);
|
|
198
|
+
const id = operationId(input);
|
|
199
|
+
const previous = journal.operations.find((item) => item.id === id);
|
|
200
|
+
const next: PlatformOperation = {
|
|
201
|
+
id,
|
|
202
|
+
kind: input.kind,
|
|
203
|
+
target: input.target,
|
|
204
|
+
expectedDigest: input.expectedDigest,
|
|
205
|
+
dependency: input.dependency,
|
|
206
|
+
state: input.state,
|
|
207
|
+
attempts: Math.min(MAX_ATTEMPTS, (previous?.attempts ?? 0) + (input.state === 'pending' ? 1 : 0)),
|
|
208
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
209
|
+
lastCode: input.lastCode ?? null,
|
|
210
|
+
...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
|
|
211
|
+
...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
|
|
212
|
+
...(input.pullRequest ? { pullRequest: input.pullRequest } : {}),
|
|
213
|
+
...(input.pullRequestSummary ? { pullRequestSummary: input.pullRequestSummary } : {}),
|
|
214
|
+
...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
|
|
215
|
+
updatedAt: new Date().toISOString()
|
|
216
|
+
};
|
|
217
|
+
const operations = previous
|
|
218
|
+
? journal.operations.map((item) => item.id === id ? next : item)
|
|
219
|
+
: [...journal.operations, next];
|
|
220
|
+
writeJournal(file, { version: 1, taskId, operations });
|
|
221
|
+
return next;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readPlatformOperationJournal(taskRef: string, cwd?: string): PlatformOperationJournal {
|
|
225
|
+
const { taskId, file } = resolveJournal(taskRef, cwd);
|
|
226
|
+
return parseJournal(file, taskId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, MAX_ATTEMPTS as PLATFORM_OPERATION_MAX_ATTEMPTS, operationId, recordPlatformOperation, readPlatformOperationJournal };
|
|
230
|
+
export type { PlatformIssueCreateIntent, PlatformIssueMetadataIntent, PlatformOperation, PlatformOperationJournal, PlatformOperationKind, PlatformOperationState, PlatformPullRequestIntent, PlatformPullRequestReviewIntent, PlatformPullRequestSummaryIntent, RecordOperationInput };
|