@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
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
|
});
|
|
@@ -761,9 +795,12 @@ function terminalResult(
|
|
|
761
795
|
const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
|
|
762
796
|
const warnings = openWarnings(receipt);
|
|
763
797
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
764
|
-
const
|
|
798
|
+
const terminalVerificationPending = receipt.lifecycle === 'done' && receipt.verification === 'pending';
|
|
799
|
+
const resultError = error ?? (terminalVerificationPending ? steps.verification?.error : null) ?? null;
|
|
800
|
+
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID'
|
|
801
|
+
|| terminalVerificationPending;
|
|
765
802
|
return {
|
|
766
|
-
status: hardError ? (
|
|
803
|
+
status: hardError ? (resultError?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
767
804
|
changed,
|
|
768
805
|
taskId,
|
|
769
806
|
backfill: steps.backfill ?? null,
|
|
@@ -774,7 +811,7 @@ function terminalResult(
|
|
|
774
811
|
completedSteps: completedSteps(receipt),
|
|
775
812
|
pendingSteps: pending,
|
|
776
813
|
result: hardError
|
|
777
|
-
? (
|
|
814
|
+
? (resultError?.retryable ? 'blocked' : 'failed')
|
|
778
815
|
: postLifecyclePending
|
|
779
816
|
? 'completed_with_warnings'
|
|
780
817
|
: pending.length === 0
|
|
@@ -783,7 +820,7 @@ function terminalResult(
|
|
|
783
820
|
? 'blocked'
|
|
784
821
|
: 'failed',
|
|
785
822
|
warnings,
|
|
786
|
-
error: hardError ?
|
|
823
|
+
error: hardError ? resultError : postLifecyclePending ? null : resultError
|
|
787
824
|
};
|
|
788
825
|
}
|
|
789
826
|
|
|
@@ -814,7 +851,8 @@ async function prepareUnderLock(
|
|
|
814
851
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
815
852
|
const backfill = options.backfill ?? backfillCompletionComments;
|
|
816
853
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
817
|
-
const
|
|
854
|
+
const issueSync = options.issueSync ?? syncPlatformIssue;
|
|
855
|
+
const issueInspect = options.issueInspect ?? inspectPlatformIssue;
|
|
818
856
|
const consumedCapabilities = new Set<string>();
|
|
819
857
|
let receipt: TaskFinalizationReceipt;
|
|
820
858
|
try {
|
|
@@ -858,6 +896,91 @@ async function prepareUnderLock(
|
|
|
858
896
|
}
|
|
859
897
|
}
|
|
860
898
|
|
|
899
|
+
if (preflightState.ok && preflightState.state === 'active') {
|
|
900
|
+
const recovery = await recoverPlatformOperations(taskId, 'all', { agent: request.agent, cwd: repoRoot });
|
|
901
|
+
if (recovery.status !== 'applied' && recovery.status !== 'no-op') {
|
|
902
|
+
return failed(taskId, recovery.error ?? {
|
|
903
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
904
|
+
message: 'Pending platform operations remain unresolved',
|
|
905
|
+
retryable: true
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const taskContent = fs.readFileSync(preflightState.taskMdPath, 'utf8');
|
|
909
|
+
const taskFrontmatter = parseTaskFrontmatter(taskContent);
|
|
910
|
+
const issueIdentity = taskIssueIdentity(taskFrontmatter);
|
|
911
|
+
if (issueIdentity) {
|
|
912
|
+
const base = typeof taskFrontmatter.delivery_base_ref === 'string' ? taskFrontmatter.delivery_base_ref.trim() : '';
|
|
913
|
+
let synced: IssueResult;
|
|
914
|
+
try {
|
|
915
|
+
synced = await issueSync(taskId, {
|
|
916
|
+
agent: request.agent, cwd: repoRoot, requirements: true, issueType: true, fields: true,
|
|
917
|
+
...(base ? { inLabels: 'from-diff', base } : {}), dependency: 'required'
|
|
918
|
+
} satisfies IssueSyncOptions);
|
|
919
|
+
} catch (error) {
|
|
920
|
+
const detail = errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true);
|
|
921
|
+
return failed(taskId, detail);
|
|
922
|
+
}
|
|
923
|
+
const issueOperations = synced.operations as Array<{ name: string; status: string; reasonCode: string | null; value?: unknown }>;
|
|
924
|
+
const badOperation = issueOperations?.find((operation) =>
|
|
925
|
+
['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
|
|
926
|
+
&& operation.status !== 'applied' && operation.status !== 'no-op'
|
|
927
|
+
&& !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR')
|
|
928
|
+
);
|
|
929
|
+
if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
|
|
930
|
+
const error = synced.error ?? {
|
|
931
|
+
code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
932
|
+
message: `Required Issue metadata operation '${badOperation?.name ?? 'sync'}' was not confirmed`,
|
|
933
|
+
retryable: true
|
|
934
|
+
};
|
|
935
|
+
return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
|
|
936
|
+
}
|
|
937
|
+
let inspected: IssueResult;
|
|
938
|
+
try { inspected = await issueInspect(taskId, { cwd: repoRoot }); }
|
|
939
|
+
catch (error) { return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true)); }
|
|
940
|
+
if (inspected.status !== 'no-op' || inspected.error || !inspected.issue
|
|
941
|
+
|| !resourceIdentityEquals(inspected.issue.identity, issueIdentity)) {
|
|
942
|
+
return failed(taskId, inspected.error ?? {
|
|
943
|
+
code: 'FINALIZATION_ISSUE_IDENTITY_UNCONFIRMED',
|
|
944
|
+
message: 'Required Issue metadata could not be reread under the bound Issue identity',
|
|
945
|
+
retryable: true
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
for (const name of ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])]) {
|
|
949
|
+
const operation = issueOperations?.find((candidate) => candidate.name === name);
|
|
950
|
+
if (!operation) return failed(taskId, {
|
|
951
|
+
code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
952
|
+
message: `Required Issue metadata operation '${name}' was not reported`, retryable: true
|
|
953
|
+
});
|
|
954
|
+
const before = synced.issue;
|
|
955
|
+
const remote = inspected.issue;
|
|
956
|
+
if (name === 'requirements') {
|
|
957
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.body;
|
|
958
|
+
if (expected === undefined || remote.body !== expected) return failed(taskId, {
|
|
959
|
+
code: 'FINALIZATION_ISSUE_REQUIREMENTS_UNCONFIRMED', message: 'Issue requirements did not match the verified target', retryable: true
|
|
960
|
+
});
|
|
961
|
+
} else if (name === 'issue-type') {
|
|
962
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.issueType;
|
|
963
|
+
if (!expected || remote.issueType !== expected) return failed(taskId, {
|
|
964
|
+
code: 'FINALIZATION_ISSUE_TYPE_UNCONFIRMED', message: 'Issue Type did not match the verified target', retryable: true
|
|
965
|
+
});
|
|
966
|
+
} else if (name === 'labels:in') {
|
|
967
|
+
const expected = Array.isArray(operation.value) ? operation.value as string[] : before?.labels;
|
|
968
|
+
if (!expected || expected.filter((label) => label.startsWith('in:')).sort().join('\0')
|
|
969
|
+
!== remote.labels.filter((label) => label.startsWith('in:')).sort().join('\0')) return failed(taskId, {
|
|
970
|
+
code: 'FINALIZATION_IN_LABELS_UNCONFIRMED', message: 'Issue in-labels did not match the verified target', retryable: true
|
|
971
|
+
});
|
|
972
|
+
} else {
|
|
973
|
+
const expected = operation.value && typeof operation.value === 'object'
|
|
974
|
+
? operation.value as Record<string, string | number>
|
|
975
|
+
: before?.fields;
|
|
976
|
+
if (!expected || Object.entries(expected).some(([field, value]) => remote.fields[field] !== value)) return failed(taskId, {
|
|
977
|
+
code: 'FINALIZATION_ISSUE_FIELDS_UNCONFIRMED', message: 'Pinned Issue fields did not match the verified target', retryable: true
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
861
984
|
let changed = false;
|
|
862
985
|
let backfillResult: TaskFinalizationStep | null = null;
|
|
863
986
|
let runBackfill = shouldRunBackfill(receipt);
|
|
@@ -921,9 +1044,10 @@ async function prepareUnderLock(
|
|
|
921
1044
|
} else backfillResult = { status: 'no-op', changed: false, error: null };
|
|
922
1045
|
|
|
923
1046
|
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
1047
|
+
let projection: TaskCompletionProjection | undefined;
|
|
924
1048
|
try {
|
|
925
1049
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
926
|
-
if (
|
|
1050
|
+
if (resolved.ok && resolved.state === 'completed') {
|
|
927
1051
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
928
1052
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
929
1053
|
} else {
|
|
@@ -937,6 +1061,12 @@ async function prepareUnderLock(
|
|
|
937
1061
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
938
1062
|
}
|
|
939
1063
|
changed = changed || result.changed;
|
|
1064
|
+
if (result.timestamp && result.agentInfraVersion) {
|
|
1065
|
+
projection = previewTaskCompletion(taskId, request.agent, {
|
|
1066
|
+
repoRoot,
|
|
1067
|
+
metadataProvider: () => ({ timestamp: result.timestamp!, agentInfraVersion: result.agentInfraVersion! })
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
940
1070
|
}
|
|
941
1071
|
} catch (error) {
|
|
942
1072
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
@@ -946,85 +1076,16 @@ async function prepareUnderLock(
|
|
|
946
1076
|
|
|
947
1077
|
let taskComment: TaskFinalizationStep | null = null;
|
|
948
1078
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1079
|
+
const currentProjection = (): TaskCompletionProjection | undefined => projection
|
|
1080
|
+
? previewTaskCompletion(taskId, request.agent, {
|
|
1081
|
+
repoRoot,
|
|
1082
|
+
metadataProvider: () => ({ timestamp: projection!.timestamp, agentInfraVersion: projection!.agentInfraVersion })
|
|
1083
|
+
})
|
|
1084
|
+
: undefined;
|
|
949
1085
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
} else {
|
|
954
|
-
try {
|
|
955
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
956
|
-
const verificationState = resolveTaskRef(taskId, { repoRoot });
|
|
957
|
-
const verificationEvent = verificationState.ok && verificationState.state === 'completed'
|
|
958
|
-
? 'complete-task.completed'
|
|
959
|
-
: 'complete-task.prepared';
|
|
960
|
-
const result = await verify(
|
|
961
|
-
{ taskRef: taskId, event: verificationEvent },
|
|
962
|
-
{ repoRoot }
|
|
963
|
-
);
|
|
964
|
-
verification = verificationStep(result);
|
|
965
|
-
if (result.status === 'pass') {
|
|
966
|
-
const resolvedTaskWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
967
|
-
const warnings = applyVerificationWarnings(receipt, result);
|
|
968
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
969
|
-
verification: 'done', taskComment: resolvedTaskWarning ? 'pending' : receipt.taskComment,
|
|
970
|
-
warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null
|
|
971
|
-
});
|
|
972
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
973
|
-
const finalComment = await syncPendingTaskComment({
|
|
974
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
975
|
-
});
|
|
976
|
-
receipt = finalComment.receipt;
|
|
977
|
-
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
978
|
-
changed = changed || finalComment.changed;
|
|
979
|
-
if (finalComment.error) {
|
|
980
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
981
|
-
}
|
|
982
|
-
} else {
|
|
983
|
-
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
984
|
-
const warnings = applyVerificationWarnings(receipt, result);
|
|
985
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
986
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
987
|
-
});
|
|
988
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
989
|
-
const warningComment = await syncPendingTaskComment({
|
|
990
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
991
|
-
});
|
|
992
|
-
receipt = warningComment.receipt;
|
|
993
|
-
taskComment = warningComment.step;
|
|
994
|
-
changed = changed || warningComment.changed;
|
|
995
|
-
if (warningComment.error) {
|
|
996
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, warningComment.error);
|
|
997
|
-
}
|
|
998
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
999
|
-
}
|
|
1000
|
-
} catch (error) {
|
|
1001
|
-
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
1002
|
-
try {
|
|
1003
|
-
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
1004
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
1005
|
-
verification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail
|
|
1006
|
-
});
|
|
1007
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1008
|
-
} catch (writeError) {
|
|
1009
|
-
const persistence = errorOf(writeError, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1010
|
-
const hardError: FinalizationError = {
|
|
1011
|
-
code: 'FINALIZATION_RECEIPT_WRITE_FAILED',
|
|
1012
|
-
message: `Unable to persist verification failure (${detail.code}: ${detail.message}): ${persistence.message}`,
|
|
1013
|
-
retryable: true
|
|
1014
|
-
};
|
|
1015
|
-
return terminalResult(taskId, receipt, {
|
|
1016
|
-
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
1017
|
-
}, changed, hardError);
|
|
1018
|
-
}
|
|
1019
|
-
try {
|
|
1020
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
1021
|
-
receipt = warningComment.receipt;
|
|
1022
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
1023
|
-
changed = changed || warningComment.changed;
|
|
1024
|
-
} catch { /* preserve the primary error */ }
|
|
1025
|
-
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1086
|
+
const verification: TaskFinalizationStep = receipt.verification === 'done'
|
|
1087
|
+
? { status: 'no-op', changed: false, error: null }
|
|
1088
|
+
: { status: 'skipped', changed: false, error: null };
|
|
1028
1089
|
|
|
1029
1090
|
let summary = receipt.summary === 'pending'
|
|
1030
1091
|
? await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync })
|
|
@@ -1038,7 +1099,7 @@ async function prepareUnderLock(
|
|
|
1038
1099
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
1039
1100
|
if (receipt.taskComment === 'pending') {
|
|
1040
1101
|
const finalComment = await syncPendingTaskComment({
|
|
1041
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1102
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
1042
1103
|
});
|
|
1043
1104
|
receipt = finalComment.receipt;
|
|
1044
1105
|
taskComment = finalComment.step;
|
|
@@ -1075,7 +1136,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1075
1136
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot });
|
|
1076
1137
|
if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
|
|
1077
1138
|
try {
|
|
1078
|
-
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
|
|
1139
|
+
return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', async () => {
|
|
1079
1140
|
if (options.controlBinding && options.manifestPath) {
|
|
1080
1141
|
try {
|
|
1081
1142
|
const manifest = readSandboxControlManifest(options.manifestPath);
|
|
@@ -1122,7 +1183,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1122
1183
|
if (!receipt) return failed(resolved.taskId, {
|
|
1123
1184
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
|
|
1124
1185
|
});
|
|
1125
|
-
if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
|
|
1186
|
+
if (receipt.lifecycle === 'done' && receipt.verification !== 'pending' && resolved.state === 'completed') {
|
|
1126
1187
|
return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
|
|
1127
1188
|
}
|
|
1128
1189
|
if (options.controlBinding) {
|
|
@@ -1158,7 +1219,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1158
1219
|
}
|
|
1159
1220
|
}
|
|
1160
1221
|
}
|
|
1161
|
-
if (receipt.taskComment === 'pending' || receipt.
|
|
1222
|
+
if (receipt.taskComment === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
|
|
1162
1223
|
return failed(resolved.taskId, {
|
|
1163
1224
|
code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
|
|
1164
1225
|
}, { completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt) });
|
|
@@ -1169,6 +1230,7 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1169
1230
|
message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
|
|
1170
1231
|
retryable: false
|
|
1171
1232
|
});
|
|
1233
|
+
receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
|
|
1172
1234
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
1173
1235
|
const result = lifecycle(
|
|
1174
1236
|
{ taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
|
|
@@ -1179,8 +1241,82 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
|
|
|
1179
1241
|
const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
|
|
1180
1242
|
return terminalResult(resolved.taskId, updated, { lifecycle: step }, result.changed, step.error);
|
|
1181
1243
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1244
|
+
|
|
1245
|
+
let verificationResult: TaskVerificationResult;
|
|
1246
|
+
try {
|
|
1247
|
+
verificationResult = await (options.verify ?? verifyTaskEvent)(
|
|
1248
|
+
{ taskRef: resolved.taskId, event: 'complete-task.completed' },
|
|
1249
|
+
{ repoRoot }
|
|
1250
|
+
);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
1253
|
+
const warnings = replaceWarning(receipt, warningFromError('verification', detail), 'open');
|
|
1254
|
+
let updated: TaskFinalizationReceipt;
|
|
1255
|
+
try {
|
|
1256
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1257
|
+
lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
|
|
1258
|
+
warnings, lastError: detail
|
|
1259
|
+
});
|
|
1260
|
+
} catch (persistCause) {
|
|
1261
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1262
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1263
|
+
persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
|
|
1264
|
+
return failed(resolved.taskId, persistError, {
|
|
1265
|
+
changed: result.changed,
|
|
1266
|
+
lifecycle: step,
|
|
1267
|
+
verification: { status: 'blocked', changed: false, error: persistError },
|
|
1268
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const verification: TaskFinalizationStep = { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail };
|
|
1272
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const verification = verificationStep(verificationResult);
|
|
1276
|
+
if (verificationResult.status === 'pass') {
|
|
1277
|
+
const resolvedVerificationWarning = receipt.warnings.some((warning) => warning.step === 'verification' && warning.status === 'open');
|
|
1278
|
+
const warnings = applyVerificationWarnings(receipt, verificationResult);
|
|
1279
|
+
const hasOpenWarnings = warnings.some((warning) => warning.status === 'open');
|
|
1280
|
+
let updated: TaskFinalizationReceipt;
|
|
1281
|
+
try {
|
|
1282
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1283
|
+
lifecycle: 'done', verification: 'done',
|
|
1284
|
+
taskComment: resolvedVerificationWarning ? 'pending' : receipt.taskComment,
|
|
1285
|
+
warningProjection: hasOpenWarnings ? 'pending' : 'done', warnings, lastError: null
|
|
1286
|
+
});
|
|
1287
|
+
} catch (persistCause) {
|
|
1288
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1289
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1290
|
+
return failed(resolved.taskId, persistError, {
|
|
1291
|
+
changed: result.changed,
|
|
1292
|
+
lifecycle: step,
|
|
1293
|
+
verification,
|
|
1294
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
|
|
1301
|
+
const warnings = applyVerificationWarnings(receipt, verificationResult);
|
|
1302
|
+
let updated: TaskFinalizationReceipt;
|
|
1303
|
+
try {
|
|
1304
|
+
updated = updateReceipt(repoRoot, receipt, {
|
|
1305
|
+
lifecycle: 'done', verification: 'pending', taskComment: 'pending', warningProjection: 'pending',
|
|
1306
|
+
warnings, lastError: detail
|
|
1307
|
+
});
|
|
1308
|
+
} catch (persistCause) {
|
|
1309
|
+
const persistError = errorOf(persistCause, 'FINALIZATION_RECEIPT_WRITE_FAILED', true);
|
|
1310
|
+
persistError.code = 'FINALIZATION_RECEIPT_WRITE_FAILED';
|
|
1311
|
+
persistError.message = `${persistError.message}; verification also failed: ${detail.code}: ${detail.message}`;
|
|
1312
|
+
return failed(resolved.taskId, persistError, {
|
|
1313
|
+
changed: result.changed,
|
|
1314
|
+
lifecycle: step,
|
|
1315
|
+
verification: { status: 'blocked', changed: false, error: persistError },
|
|
1316
|
+
completedSteps: completedSteps(receipt), pendingSteps: pendingSteps(receipt), warnings: openWarnings(receipt)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return terminalResult(resolved.taskId, updated, { lifecycle: step, verification }, result.changed, detail);
|
|
1184
1320
|
});
|
|
1185
1321
|
} catch (error) {
|
|
1186
1322
|
const detail = error instanceof TaskExecutionLockError
|
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
|
};
|