@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
|
@@ -5,8 +5,11 @@ import { syncPlatformComment } from '../platform/issue-comments.js';
|
|
|
5
5
|
import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
|
|
6
6
|
import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
|
|
7
7
|
import { taskIssueIdentity } from '../platform/task-identities.js';
|
|
8
|
+
import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
|
|
9
|
+
import { resourceIdentityEquals } from '../platform/resource-identity.js';
|
|
8
10
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
|
-
import {
|
|
11
|
+
import { recoverPlatformOperations } from './platform-operation-recovery.js';
|
|
12
|
+
import { applyTaskLifecycle, inspectTaskLifecycleProgress, previewTaskCompletion } from './lifecycle.js';
|
|
10
13
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
11
14
|
import { inspectShortIdRegistry } from './short-id.js';
|
|
12
15
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
@@ -329,19 +332,16 @@ function capabilityError(code, message) {
|
|
|
329
332
|
Object.assign(error, { code });
|
|
330
333
|
return error;
|
|
331
334
|
}
|
|
332
|
-
function
|
|
335
|
+
function hasFinalizationTaskDocument(repoRoot, taskId) {
|
|
333
336
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
334
|
-
if (!resolved.ok)
|
|
335
|
-
return false;
|
|
336
|
-
if (resolved.state === 'completed')
|
|
337
|
-
return true;
|
|
338
|
-
if (resolved.state !== 'active')
|
|
337
|
+
if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed'))
|
|
339
338
|
return false;
|
|
340
339
|
try {
|
|
341
|
-
|
|
340
|
+
const journal = JSON.parse(fs.readFileSync(path.join(resolved.taskDir, '.task-lifecycle.json'), 'utf8'));
|
|
341
|
+
return journal.taskId === taskId && journal.intent === 'complete';
|
|
342
342
|
}
|
|
343
343
|
catch {
|
|
344
|
-
return
|
|
344
|
+
return resolved.state === 'completed';
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
function mutationKeys(value) {
|
|
@@ -388,7 +388,14 @@ function mutationPatch(current, mutation) {
|
|
|
388
388
|
if (mutation.scope === 'task-comment') {
|
|
389
389
|
if (mutation.operation === 'succeeded') {
|
|
390
390
|
const warnings = resolveStepWarnings(current, 'task-comment');
|
|
391
|
-
|
|
391
|
+
const retryWithResolvedWarning = mutation.state === 'done'
|
|
392
|
+
&& current.warnings.some((warning) => warning.step === 'task-comment' && warning.status === 'open');
|
|
393
|
+
return {
|
|
394
|
+
taskComment: retryWithResolvedWarning ? 'pending' : mutation.state,
|
|
395
|
+
warningProjection: warnings.length > 0 || retryWithResolvedWarning ? 'pending' : 'done',
|
|
396
|
+
warnings,
|
|
397
|
+
lastError: null
|
|
398
|
+
};
|
|
392
399
|
}
|
|
393
400
|
const warnings = replaceWarning(current, warningFromError('task-comment', mutation.error), 'open');
|
|
394
401
|
return { taskComment: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
|
|
@@ -413,8 +420,8 @@ function mutationPatch(current, mutation) {
|
|
|
413
420
|
: { warningProjection: 'pending', lastError: mutation.error };
|
|
414
421
|
}
|
|
415
422
|
function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, consumed) {
|
|
416
|
-
if (!
|
|
417
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active
|
|
423
|
+
if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
|
|
424
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
|
|
418
425
|
}
|
|
419
426
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
420
427
|
if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
|
|
@@ -426,8 +433,8 @@ function applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability
|
|
|
426
433
|
return next;
|
|
427
434
|
}
|
|
428
435
|
function applyFinalizationReceiptMutation(repoRoot, receipt, capability, mutation) {
|
|
429
|
-
if (!
|
|
430
|
-
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active
|
|
436
|
+
if (!hasFinalizationTaskDocument(repoRoot, receipt.taskId)) {
|
|
437
|
+
throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires an active task or a completed task');
|
|
431
438
|
}
|
|
432
439
|
return withTaskExecutionLock(repoRoot, receipt.taskId, 'task-finalization.receipt-mutation', () => applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set()));
|
|
433
440
|
}
|
|
@@ -483,7 +490,15 @@ async function syncPendingTaskComment(input) {
|
|
|
483
490
|
};
|
|
484
491
|
}
|
|
485
492
|
try {
|
|
486
|
-
const result = await commentSync(taskId, {
|
|
493
|
+
const result = await commentSync(taskId, {
|
|
494
|
+
kind: 'task', agent, cwd: repoRoot, dependency: 'required',
|
|
495
|
+
...(input.projection ? {
|
|
496
|
+
taskProjection: {
|
|
497
|
+
content: input.projection.content,
|
|
498
|
+
sha256: createHash('sha256').update(input.projection.content).digest('hex')
|
|
499
|
+
}
|
|
500
|
+
} : {})
|
|
501
|
+
});
|
|
487
502
|
const step = commentStep(result);
|
|
488
503
|
if (result.status === 'applied' || result.status === 'no-op') {
|
|
489
504
|
const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
|
|
@@ -492,6 +507,14 @@ async function syncPendingTaskComment(input) {
|
|
|
492
507
|
scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
|
|
493
508
|
}, consumedCapabilities);
|
|
494
509
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
510
|
+
if (!skipped && receipt.taskComment === 'pending') {
|
|
511
|
+
const retry = await syncPendingTaskComment({
|
|
512
|
+
...input,
|
|
513
|
+
receipt,
|
|
514
|
+
projection: input.refreshProjection?.() ?? input.projection
|
|
515
|
+
});
|
|
516
|
+
return { ...retry, changed: result.changed || retry.changed };
|
|
517
|
+
}
|
|
495
518
|
return { receipt, step: skipped ? { ...step, status: 'skipped' } : step, changed: result.changed, error: null };
|
|
496
519
|
}
|
|
497
520
|
const detail = step.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
|
|
@@ -540,8 +563,8 @@ async function syncPendingSummary(input) {
|
|
|
540
563
|
let { receipt } = input;
|
|
541
564
|
try {
|
|
542
565
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
543
|
-
if (!resolved.ok ||
|
|
544
|
-
throw Object.assign(new Error('
|
|
566
|
+
if (!resolved.ok || (resolved.state !== 'active' && resolved.state !== 'completed'))
|
|
567
|
+
throw Object.assign(new Error('task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
545
568
|
if (!fs.existsSync(path.join(resolved.taskDir, '.delivery-summary.json'))) {
|
|
546
569
|
receipt = updateReceipt(input.repoRoot, receipt, { summary: 'skipped', lastError: null });
|
|
547
570
|
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
@@ -555,7 +578,8 @@ async function syncPendingSummary(input) {
|
|
|
555
578
|
const summary = readDeliverySummary(resolved.taskDir, input.taskId);
|
|
556
579
|
const result = await input.commentSync(input.taskId, {
|
|
557
580
|
kind: 'summary', agent: input.agent, body: summary.body, cwd: input.repoRoot,
|
|
558
|
-
summaryAuthorization: { sha256: summary.sha256 }
|
|
581
|
+
summaryAuthorization: { sha256: summary.sha256 },
|
|
582
|
+
dependency: 'required'
|
|
559
583
|
});
|
|
560
584
|
const step = commentStep(result);
|
|
561
585
|
if (result.status === 'applied' || result.status === 'no-op') {
|
|
@@ -565,7 +589,7 @@ async function syncPendingSummary(input) {
|
|
|
565
589
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
566
590
|
summary: skipped ? 'skipped' : 'done',
|
|
567
591
|
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
568
|
-
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
592
|
+
warningProjection: warnings.some((warning) => warning.status === 'open') || resolvedWarning ? 'pending' : 'done',
|
|
569
593
|
warnings,
|
|
570
594
|
lastError: null
|
|
571
595
|
});
|
|
@@ -637,6 +661,8 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
637
661
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
638
662
|
const backfill = options.backfill ?? backfillCompletionComments;
|
|
639
663
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
664
|
+
const issueSync = options.issueSync ?? syncPlatformIssue;
|
|
665
|
+
const issueInspect = options.issueInspect ?? inspectPlatformIssue;
|
|
640
666
|
const verify = options.verify ?? verifyTaskEvent;
|
|
641
667
|
const consumedCapabilities = new Set();
|
|
642
668
|
let receipt;
|
|
@@ -679,6 +705,100 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
679
705
|
return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
|
|
680
706
|
}
|
|
681
707
|
}
|
|
708
|
+
if (preflightState.ok && preflightState.state === 'active') {
|
|
709
|
+
const recovery = await recoverPlatformOperations(taskId, 'all', { agent: request.agent, cwd: repoRoot });
|
|
710
|
+
if (recovery.status !== 'applied' && recovery.status !== 'no-op') {
|
|
711
|
+
return failed(taskId, recovery.error ?? {
|
|
712
|
+
code: 'PLATFORM_OPERATION_RECOVERY_PENDING',
|
|
713
|
+
message: 'Pending platform operations remain unresolved',
|
|
714
|
+
retryable: true
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const taskContent = fs.readFileSync(preflightState.taskMdPath, 'utf8');
|
|
718
|
+
const taskFrontmatter = parseTaskFrontmatter(taskContent);
|
|
719
|
+
const issueIdentity = taskIssueIdentity(taskFrontmatter);
|
|
720
|
+
if (issueIdentity) {
|
|
721
|
+
const base = typeof taskFrontmatter.delivery_base_ref === 'string' ? taskFrontmatter.delivery_base_ref.trim() : '';
|
|
722
|
+
let synced;
|
|
723
|
+
try {
|
|
724
|
+
synced = await issueSync(taskId, {
|
|
725
|
+
agent: request.agent, cwd: repoRoot, requirements: true, issueType: true, fields: true,
|
|
726
|
+
...(base ? { inLabels: 'from-diff', base } : {}), dependency: 'required'
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
const detail = errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true);
|
|
731
|
+
return failed(taskId, detail);
|
|
732
|
+
}
|
|
733
|
+
const issueOperations = synced.operations;
|
|
734
|
+
const badOperation = issueOperations?.find((operation) => ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])].includes(operation.name)
|
|
735
|
+
&& operation.status !== 'applied' && operation.status !== 'no-op');
|
|
736
|
+
if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
|
|
737
|
+
const error = synced.error ?? {
|
|
738
|
+
code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
739
|
+
message: `Required Issue metadata operation '${badOperation?.name ?? 'sync'}' was not confirmed`,
|
|
740
|
+
retryable: true
|
|
741
|
+
};
|
|
742
|
+
return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
|
|
743
|
+
}
|
|
744
|
+
let inspected;
|
|
745
|
+
try {
|
|
746
|
+
inspected = await issueInspect(taskId, { cwd: repoRoot });
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true));
|
|
750
|
+
}
|
|
751
|
+
if (inspected.status !== 'no-op' || inspected.error || !inspected.issue
|
|
752
|
+
|| !resourceIdentityEquals(inspected.issue.identity, issueIdentity)) {
|
|
753
|
+
return failed(taskId, inspected.error ?? {
|
|
754
|
+
code: 'FINALIZATION_ISSUE_IDENTITY_UNCONFIRMED',
|
|
755
|
+
message: 'Required Issue metadata could not be reread under the bound Issue identity',
|
|
756
|
+
retryable: true
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
for (const name of ['requirements', 'issue-type', 'fields', ...(base ? ['labels:in'] : [])]) {
|
|
760
|
+
const operation = issueOperations?.find((candidate) => candidate.name === name);
|
|
761
|
+
if (!operation)
|
|
762
|
+
return failed(taskId, {
|
|
763
|
+
code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
|
|
764
|
+
message: `Required Issue metadata operation '${name}' was not reported`, retryable: true
|
|
765
|
+
});
|
|
766
|
+
const before = synced.issue;
|
|
767
|
+
const remote = inspected.issue;
|
|
768
|
+
if (name === 'requirements') {
|
|
769
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.body;
|
|
770
|
+
if (expected === undefined || remote.body !== expected)
|
|
771
|
+
return failed(taskId, {
|
|
772
|
+
code: 'FINALIZATION_ISSUE_REQUIREMENTS_UNCONFIRMED', message: 'Issue requirements did not match the verified target', retryable: true
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else if (name === 'issue-type') {
|
|
776
|
+
const expected = typeof operation.value === 'string' ? operation.value : before?.issueType;
|
|
777
|
+
if (!expected || remote.issueType !== expected)
|
|
778
|
+
return failed(taskId, {
|
|
779
|
+
code: 'FINALIZATION_ISSUE_TYPE_UNCONFIRMED', message: 'Issue Type did not match the verified target', retryable: true
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
else if (name === 'labels:in') {
|
|
783
|
+
const expected = Array.isArray(operation.value) ? operation.value : before?.labels;
|
|
784
|
+
if (!expected || expected.filter((label) => label.startsWith('in:')).sort().join('\0')
|
|
785
|
+
!== remote.labels.filter((label) => label.startsWith('in:')).sort().join('\0'))
|
|
786
|
+
return failed(taskId, {
|
|
787
|
+
code: 'FINALIZATION_IN_LABELS_UNCONFIRMED', message: 'Issue in-labels did not match the verified target', retryable: true
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
const expected = operation.value && typeof operation.value === 'object'
|
|
792
|
+
? operation.value
|
|
793
|
+
: before?.fields;
|
|
794
|
+
if (!expected || Object.entries(expected).some(([field, value]) => remote.fields[field] !== value))
|
|
795
|
+
return failed(taskId, {
|
|
796
|
+
code: 'FINALIZATION_ISSUE_FIELDS_UNCONFIRMED', message: 'Pinned Issue fields did not match the verified target', retryable: true
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
682
802
|
let changed = false;
|
|
683
803
|
let backfillResult = null;
|
|
684
804
|
let runBackfill = shouldRunBackfill(receipt);
|
|
@@ -748,9 +868,10 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
748
868
|
else
|
|
749
869
|
backfillResult = { status: 'no-op', changed: false, error: null };
|
|
750
870
|
let lifecycleResult = null;
|
|
871
|
+
let projection;
|
|
751
872
|
try {
|
|
752
873
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
753
|
-
if (
|
|
874
|
+
if (resolved.ok && resolved.state === 'completed') {
|
|
754
875
|
lifecycleResult = { status: 'no-op', changed: false, error: null };
|
|
755
876
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
756
877
|
}
|
|
@@ -762,6 +883,12 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
762
883
|
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
763
884
|
}
|
|
764
885
|
changed = changed || result.changed;
|
|
886
|
+
if (result.timestamp && result.agentInfraVersion) {
|
|
887
|
+
projection = previewTaskCompletion(taskId, request.agent, {
|
|
888
|
+
repoRoot,
|
|
889
|
+
metadataProvider: () => ({ timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion })
|
|
890
|
+
});
|
|
891
|
+
}
|
|
765
892
|
}
|
|
766
893
|
}
|
|
767
894
|
catch (error) {
|
|
@@ -774,6 +901,12 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
774
901
|
}
|
|
775
902
|
let taskComment = null;
|
|
776
903
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
904
|
+
const currentProjection = () => projection
|
|
905
|
+
? previewTaskCompletion(taskId, request.agent, {
|
|
906
|
+
repoRoot,
|
|
907
|
+
metadataProvider: () => ({ timestamp: projection.timestamp, agentInfraVersion: projection.agentInfraVersion })
|
|
908
|
+
})
|
|
909
|
+
: undefined;
|
|
777
910
|
let verification = null;
|
|
778
911
|
if (receipt.verification !== 'pending') {
|
|
779
912
|
verification = { status: receipt.verification === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null };
|
|
@@ -796,7 +929,7 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
796
929
|
});
|
|
797
930
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
798
931
|
const finalComment = await syncPendingTaskComment({
|
|
799
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
932
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
800
933
|
});
|
|
801
934
|
receipt = finalComment.receipt;
|
|
802
935
|
if (finalComment.step.status !== 'no-op')
|
|
@@ -814,7 +947,7 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
814
947
|
});
|
|
815
948
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
816
949
|
const warningComment = await syncPendingTaskComment({
|
|
817
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
950
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
818
951
|
});
|
|
819
952
|
receipt = warningComment.receipt;
|
|
820
953
|
taskComment = warningComment.step;
|
|
@@ -846,7 +979,7 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
846
979
|
}, changed, hardError);
|
|
847
980
|
}
|
|
848
981
|
try {
|
|
849
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
982
|
+
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities });
|
|
850
983
|
receipt = warningComment.receipt;
|
|
851
984
|
if (warningComment.step.status !== 'no-op')
|
|
852
985
|
taskComment = warningComment.step;
|
|
@@ -868,7 +1001,7 @@ async function prepareUnderLock(request, taskId, options) {
|
|
|
868
1001
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
869
1002
|
if (receipt.taskComment === 'pending') {
|
|
870
1003
|
const finalComment = await syncPendingTaskComment({
|
|
871
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
1004
|
+
repoRoot, taskId, agent: request.agent, receipt, projection: currentProjection(), refreshProjection: currentProjection, commentSync, consumedCapabilities
|
|
872
1005
|
});
|
|
873
1006
|
receipt = finalComment.receipt;
|
|
874
1007
|
taskComment = finalComment.step;
|
|
@@ -4,7 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { appendActivityEntry, locateActivityLog } from './activity-log.js';
|
|
5
5
|
import { artifactFamilyCatalog, inspectArtifactDirectory } from './artifact-lifecycle.js';
|
|
6
6
|
import { parseArtifactName } from './artifact-name.js';
|
|
7
|
-
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
|
+
import { parseTypedTaskFrontmatter, updateTaskFrontmatter } from './frontmatter.js';
|
|
8
|
+
import { upsertSection } from './sections.js';
|
|
8
9
|
import { locateHotTaskDirs, resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
|
|
9
10
|
import { configuredShortIdLength, executeShortIdCommand, loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
|
|
10
11
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
@@ -261,6 +262,31 @@ function mutationsFor(request, content, metadata, restoredFiles, allowMissingAct
|
|
|
261
262
|
}
|
|
262
263
|
return mutations;
|
|
263
264
|
}
|
|
265
|
+
function previewTaskCompletion(taskRef, agent, options = {}) {
|
|
266
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
267
|
+
if (!resolved.ok)
|
|
268
|
+
throw Object.assign(new Error(resolved.message), { code: resolved.code });
|
|
269
|
+
if (resolved.state !== 'active')
|
|
270
|
+
throw Object.assign(new Error(`task ${resolved.taskId} is ${resolved.state}, expected active`), { code: 'TASK_STATE_MISMATCH' });
|
|
271
|
+
const original = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
272
|
+
const metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
|
|
273
|
+
if (!isValidAgentInfraVersion(metadata.agentInfraVersion))
|
|
274
|
+
throw Object.assign(new Error('metadata agentInfraVersion is invalid'), { code: 'LIFECYCLE_METADATA_FAILED' });
|
|
275
|
+
const mutations = mutationsFor({ taskRef: resolved.taskId, intent: 'complete', agent }, original, metadata, 0);
|
|
276
|
+
if (!Array.isArray(mutations))
|
|
277
|
+
throw Object.assign(new Error(mutations.message), { code: mutations.code });
|
|
278
|
+
let content = original;
|
|
279
|
+
for (const mutation of mutations) {
|
|
280
|
+
if (mutation.kind === 'frontmatter')
|
|
281
|
+
content = updateTaskFrontmatter(content, mutation.set, mutation.remove);
|
|
282
|
+
else if (mutation.kind === 'section')
|
|
283
|
+
content = upsertSection(content, mutation).content;
|
|
284
|
+
else
|
|
285
|
+
throw Object.assign(new Error('completion projection contains an unsupported mutation'), { code: 'LIFECYCLE_PROJECTION_INVALID' });
|
|
286
|
+
}
|
|
287
|
+
content = updateTaskFrontmatter(content, { updated_at: metadata.timestamp, agent_infra_version: metadata.agentInfraVersion });
|
|
288
|
+
return { taskId: resolved.taskId, content, timestamp: metadata.timestamp, agentInfraVersion: metadata.agentInfraVersion };
|
|
289
|
+
}
|
|
264
290
|
function writeJournal(file, journal, io, create = false) {
|
|
265
291
|
const temporary = `${file}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
|
|
266
292
|
io.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, 'wx');
|
|
@@ -593,6 +619,16 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
593
619
|
completed.add('registry-committed');
|
|
594
620
|
}
|
|
595
621
|
let taskOperations = [];
|
|
622
|
+
if (options.prepareOnly && !matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) {
|
|
623
|
+
return {
|
|
624
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
625
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
626
|
+
task: { operations: [] }, directory: { effect: 'unchanged', changed: false },
|
|
627
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
628
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
629
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
630
|
+
};
|
|
631
|
+
}
|
|
596
632
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles))
|
|
597
633
|
completed.add('task-written');
|
|
598
634
|
if (!completed.has('task-written')) {
|
|
@@ -724,5 +760,5 @@ function applyTaskLifecycle(requestInput, options = {}) {
|
|
|
724
760
|
journalPath, completedSteps: [...completed], pendingSteps: []
|
|
725
761
|
};
|
|
726
762
|
}
|
|
727
|
-
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
|
|
763
|
+
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle, previewTaskCompletion };
|
|
728
764
|
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { isResourceIdentity } from '../platform/resource-identity.js';
|
|
5
|
+
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
|
+
const JOURNAL_FILE = '.platform-operations.json';
|
|
7
|
+
const MAX_ATTEMPTS = 3;
|
|
8
|
+
function operationId(input) {
|
|
9
|
+
return createHash('sha256')
|
|
10
|
+
.update(`${input.kind}\0${input.target}\0${input.expectedDigest}`)
|
|
11
|
+
.digest('hex');
|
|
12
|
+
}
|
|
13
|
+
function journalPath(taskDir) {
|
|
14
|
+
return path.join(taskDir, JOURNAL_FILE);
|
|
15
|
+
}
|
|
16
|
+
function parseJournal(file, taskId) {
|
|
17
|
+
let value;
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === 'ENOENT')
|
|
23
|
+
return { version: 1, taskId, operations: [] };
|
|
24
|
+
throw Object.assign(new Error(`Platform operation journal is unreadable: ${String(error)}`), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
25
|
+
}
|
|
26
|
+
if (!value || typeof value !== 'object')
|
|
27
|
+
throw Object.assign(new Error('Platform operation journal is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
28
|
+
const journal = value;
|
|
29
|
+
if (journal.version !== 1 || journal.taskId !== taskId || !Array.isArray(journal.operations)) {
|
|
30
|
+
throw Object.assign(new Error('Platform operation journal identity or version is invalid'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
31
|
+
}
|
|
32
|
+
for (const item of journal.operations) {
|
|
33
|
+
if (!item || typeof item !== 'object' || !/^[a-f0-9]{64}$/u.test(item.id)
|
|
34
|
+
|| !['task-comment', 'artifact-comment', 'summary-comment', 'cancel-comment', 'issue-create', 'issue-metadata', 'pull-request', 'pull-request-summary', 'pull-request-review'].includes(item.kind)
|
|
35
|
+
|| typeof item.target !== 'string' || !item.target
|
|
36
|
+
|| !/^[a-f0-9]{64}$/u.test(item.expectedDigest)
|
|
37
|
+
|| !['deferred', 'required'].includes(item.dependency)
|
|
38
|
+
|| !['queued', 'pending', 'unknown', 'succeeded', 'failed'].includes(item.state)
|
|
39
|
+
|| !Number.isSafeInteger(item.attempts) || item.attempts < 0 || item.attempts > MAX_ATTEMPTS
|
|
40
|
+
|| item.maxAttempts !== MAX_ATTEMPTS
|
|
41
|
+
|| !(item.lastCode === null || typeof item.lastCode === 'string')
|
|
42
|
+
|| (item.kind === 'issue-metadata'
|
|
43
|
+
? !item.issueMetadata || typeof item.issueMetadata.requirements !== 'boolean'
|
|
44
|
+
|| typeof item.issueMetadata.issueType !== 'boolean' || typeof item.issueMetadata.fields !== 'boolean'
|
|
45
|
+
|| (item.issueMetadata.status !== undefined && typeof item.issueMetadata.status !== 'string')
|
|
46
|
+
|| (item.issueMetadata.assignees !== undefined && !['current', 'none'].includes(item.issueMetadata.assignees))
|
|
47
|
+
|| (item.issueMetadata.milestone !== undefined && !['initial', 'specific', 'none'].includes(item.issueMetadata.milestone))
|
|
48
|
+
|| (item.issueMetadata.inLabels !== undefined && !['from-diff', 'none'].includes(item.issueMetadata.inLabels))
|
|
49
|
+
|| (item.issueMetadata.base !== undefined && (typeof item.issueMetadata.base !== 'string' || !item.issueMetadata.base.trim()))
|
|
50
|
+
|| (item.issueMetadata.fromDiffFiles !== undefined && (!Array.isArray(item.issueMetadata.fromDiffFiles)
|
|
51
|
+
|| item.issueMetadata.fromDiffFiles.some((file) => typeof file !== 'string')))
|
|
52
|
+
|| (item.issueMetadata.inLabelMappingDigest !== undefined && !/^[a-f0-9]{64}$/u.test(item.issueMetadata.inLabelMappingDigest))
|
|
53
|
+
|| (item.issueMetadata.state !== undefined && !['open', 'closed'].includes(item.issueMetadata.state))
|
|
54
|
+
|| (item.issueMetadata.closeReason !== undefined && !['completed', 'not_planned'].includes(item.issueMetadata.closeReason))
|
|
55
|
+
|| (!item.issueMetadata.requirements && !item.issueMetadata.issueType && !item.issueMetadata.fields
|
|
56
|
+
&& item.issueMetadata.status === undefined && item.issueMetadata.assignees === undefined
|
|
57
|
+
&& item.issueMetadata.milestone === undefined && item.issueMetadata.inLabels === undefined && item.issueMetadata.state === undefined)
|
|
58
|
+
: item.issueMetadata !== undefined)
|
|
59
|
+
|| (item.kind === 'pull-request'
|
|
60
|
+
? !item.pullRequest || !['create', 'bind', 'sync'].includes(item.pullRequest.action)
|
|
61
|
+
|| Object.entries(item.pullRequest).some(([key, field]) => key !== 'action'
|
|
62
|
+
&& key !== 'metadata' && key !== 'closingIssue' && (typeof field !== 'string' || !field.trim()))
|
|
63
|
+
|| (item.pullRequest.metadata !== undefined && typeof item.pullRequest.metadata !== 'boolean')
|
|
64
|
+
|| (item.pullRequest.closingIssue !== undefined && typeof item.pullRequest.closingIssue !== 'boolean')
|
|
65
|
+
: item.pullRequest !== undefined)
|
|
66
|
+
|| (item.kind === 'issue-create'
|
|
67
|
+
? !item.issueCreate || typeof item.issueCreate.title !== 'string' || !item.issueCreate.title.trim()
|
|
68
|
+
|| !/^[a-f0-9]{64}$/u.test(item.issueCreate.bodyDigest)
|
|
69
|
+
: item.issueCreate !== undefined)
|
|
70
|
+
|| (item.kind === 'pull-request-summary'
|
|
71
|
+
? !item.pullRequestSummary || typeof item.pullRequestSummary.body !== 'string'
|
|
72
|
+
|| !item.pullRequestSummary.body.trim() || typeof item.pullRequestSummary.changeReportFile !== 'string'
|
|
73
|
+
|| !item.pullRequestSummary.changeReportFile.trim()
|
|
74
|
+
: item.pullRequestSummary !== undefined)
|
|
75
|
+
|| (item.kind === 'pull-request-review'
|
|
76
|
+
? !item.pullRequestReview || typeof item.pullRequestReview.prNumber !== 'string'
|
|
77
|
+
|| !isResourceIdentity(item.pullRequestReview.resource)
|
|
78
|
+
|| typeof item.pullRequestReview.providerScopeId !== 'string' || !item.pullRequestReview.providerScopeId.trim()
|
|
79
|
+
|| !item.pullRequestReview.prNumber.trim() || !/^TASK-\d{8}-\d{6}$|^pr\d+$/u.test(item.pullRequestReview.scope)
|
|
80
|
+
|| !Number.isSafeInteger(item.pullRequestReview.round) || item.pullRequestReview.round <= 0
|
|
81
|
+
|| !/^[0-9a-f]{7,40}$/iu.test(item.pullRequestReview.commitSha)
|
|
82
|
+
|| !['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(item.pullRequestReview.event)
|
|
83
|
+
|| typeof item.pullRequestReview.artifactFile !== 'string'
|
|
84
|
+
|| !/^pr-review(?:-r\d+)?\.md$/u.test(item.pullRequestReview.artifactFile)
|
|
85
|
+
|| (item.pullRequestReview.artifactFile === 'pr-review.md'
|
|
86
|
+
? item.pullRequestReview.round !== 1
|
|
87
|
+
: item.pullRequestReview.artifactFile !== `pr-review-r${item.pullRequestReview.round}.md`)
|
|
88
|
+
|| !/^[a-f0-9]{64}$/u.test(item.pullRequestReview.bodyDigest)
|
|
89
|
+
: item.pullRequestReview !== undefined)
|
|
90
|
+
|| typeof item.updatedAt !== 'string') {
|
|
91
|
+
throw Object.assign(new Error('Platform operation journal contains an invalid operation'), { code: 'PLATFORM_OPERATION_JOURNAL_INVALID' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return journal;
|
|
95
|
+
}
|
|
96
|
+
function writeJournal(file, journal) {
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(temporary, `${JSON.stringify(journal, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
101
|
+
fs.renameSync(temporary, file);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
try {
|
|
105
|
+
fs.unlinkSync(temporary);
|
|
106
|
+
}
|
|
107
|
+
catch { /* renamed or already removed */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resolveJournal(taskRef, cwd) {
|
|
111
|
+
const resolved = resolveTaskRef(taskRef, cwd ? { repoRoot: cwd } : {});
|
|
112
|
+
if (!resolved.ok)
|
|
113
|
+
throw Object.assign(new Error(resolved.message), { code: resolved.code });
|
|
114
|
+
return { taskId: resolved.taskId, file: journalPath(resolved.taskDir) };
|
|
115
|
+
}
|
|
116
|
+
function recordPlatformOperation(input) {
|
|
117
|
+
if (!/^[a-f0-9]{64}$/u.test(input.expectedDigest) || !input.target.trim()) {
|
|
118
|
+
throw Object.assign(new Error('Platform operation requires a stable target and SHA-256 digest'), { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID' });
|
|
119
|
+
}
|
|
120
|
+
const { taskId, file } = resolveJournal(input.taskRef, input.cwd);
|
|
121
|
+
const journal = parseJournal(file, taskId);
|
|
122
|
+
const id = operationId(input);
|
|
123
|
+
const previous = journal.operations.find((item) => item.id === id);
|
|
124
|
+
const next = {
|
|
125
|
+
id,
|
|
126
|
+
kind: input.kind,
|
|
127
|
+
target: input.target,
|
|
128
|
+
expectedDigest: input.expectedDigest,
|
|
129
|
+
dependency: input.dependency,
|
|
130
|
+
state: input.state,
|
|
131
|
+
attempts: Math.min(MAX_ATTEMPTS, (previous?.attempts ?? 0) + (input.state === 'pending' ? 1 : 0)),
|
|
132
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
133
|
+
lastCode: input.lastCode ?? null,
|
|
134
|
+
...(input.issueMetadata ? { issueMetadata: input.issueMetadata } : {}),
|
|
135
|
+
...(input.issueCreate ? { issueCreate: input.issueCreate } : {}),
|
|
136
|
+
...(input.pullRequest ? { pullRequest: input.pullRequest } : {}),
|
|
137
|
+
...(input.pullRequestSummary ? { pullRequestSummary: input.pullRequestSummary } : {}),
|
|
138
|
+
...(input.pullRequestReview ? { pullRequestReview: input.pullRequestReview } : {}),
|
|
139
|
+
updatedAt: new Date().toISOString()
|
|
140
|
+
};
|
|
141
|
+
const operations = previous
|
|
142
|
+
? journal.operations.map((item) => item.id === id ? next : item)
|
|
143
|
+
: [...journal.operations, next];
|
|
144
|
+
writeJournal(file, { version: 1, taskId, operations });
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
function readPlatformOperationJournal(taskRef, cwd) {
|
|
148
|
+
const { taskId, file } = resolveJournal(taskRef, cwd);
|
|
149
|
+
return parseJournal(file, taskId);
|
|
150
|
+
}
|
|
151
|
+
export { JOURNAL_FILE as PLATFORM_OPERATION_JOURNAL_FILE, MAX_ATTEMPTS as PLATFORM_OPERATION_MAX_ATTEMPTS, operationId, recordPlatformOperation, readPlatformOperationJournal };
|
|
152
|
+
//# sourceMappingURL=platform-operation-journal.js.map
|