@fitlab-ai/agent-infra 0.11.2 → 0.11.3
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 +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +6 -2
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/finalization-response.js +3 -5
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +1 -2
- package/dist/lib/task/control-authority.js +34 -36
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +53 -126
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -6
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +7 -4
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/finalization-response.ts +3 -5
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +1 -2
- package/lib/task/control-authority.ts +38 -47
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +57 -121
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -19
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +6 -7
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -7
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
package/lib/task/finalization.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
|
|
5
5
|
import { syncPlatformComment } from '../platform/issue-comments.ts';
|
|
6
6
|
import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
|
|
7
|
+
import { backfillCompletionComments } from '../platform/completion-backfill.ts';
|
|
7
8
|
import type { PlatformResult } from '../platform/types.ts';
|
|
8
9
|
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
9
10
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
@@ -25,8 +26,8 @@ import {
|
|
|
25
26
|
type OperationWarningSeverity
|
|
26
27
|
} from './operation-outcome.ts';
|
|
27
28
|
|
|
28
|
-
const RECEIPT_VERSION =
|
|
29
|
-
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'
|
|
29
|
+
const RECEIPT_VERSION = 4 as const;
|
|
30
|
+
const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
|
|
30
31
|
type FinalizationStep = typeof FINALIZATION_STEPS[number];
|
|
31
32
|
type FinalizationStepState = 'pending' | 'done' | 'skipped';
|
|
32
33
|
type FinalizationError = { code: string; message: string; retryable: boolean };
|
|
@@ -61,6 +62,7 @@ type TaskFinalizationOptions = Readonly<{
|
|
|
61
62
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
62
63
|
metadataProvider?: TaskLifecycleOptions['metadataProvider'];
|
|
63
64
|
lifecycle?: typeof applyTaskLifecycle;
|
|
65
|
+
backfill?: typeof backfillCompletionComments;
|
|
64
66
|
commentSync?: typeof syncPlatformComment;
|
|
65
67
|
verify?: typeof verifyTaskEvent;
|
|
66
68
|
preflight?: typeof verifyTaskEvent;
|
|
@@ -76,10 +78,8 @@ type TaskFinalizationReceipt = Readonly<{
|
|
|
76
78
|
taskComment: FinalizationStepState;
|
|
77
79
|
verification: FinalizationStepState;
|
|
78
80
|
summary: FinalizationStepState;
|
|
79
|
-
postSummaryVerification: FinalizationStepState;
|
|
80
81
|
warningProjection: WarningProjectionState;
|
|
81
82
|
warnings: readonly FinalizationWarning[];
|
|
82
|
-
summarySha256: string | null;
|
|
83
83
|
controlBinding?: Readonly<{ generation: string; requestId: string }>;
|
|
84
84
|
updatedAt: string;
|
|
85
85
|
lastError: FinalizationError | null;
|
|
@@ -104,11 +104,11 @@ type TaskFinalizationResult = Readonly<{
|
|
|
104
104
|
status: 'completed' | 'failed' | 'blocked';
|
|
105
105
|
changed: boolean;
|
|
106
106
|
taskId: string | null;
|
|
107
|
+
backfill: TaskFinalizationStep | null;
|
|
107
108
|
lifecycle: TaskFinalizationStep | null;
|
|
108
109
|
taskComment: TaskFinalizationStep | null;
|
|
109
110
|
verification: TaskFinalizationStep | null;
|
|
110
111
|
summary: TaskFinalizationStep | null;
|
|
111
|
-
postSummaryVerification: TaskFinalizationStep | null;
|
|
112
112
|
completedSteps: readonly FinalizationStep[];
|
|
113
113
|
pendingSteps: readonly FinalizationStep[];
|
|
114
114
|
result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
|
|
@@ -144,11 +144,11 @@ function failed(
|
|
|
144
144
|
status: error.retryable ? 'blocked' : 'failed',
|
|
145
145
|
changed: false,
|
|
146
146
|
taskId,
|
|
147
|
+
backfill: null,
|
|
147
148
|
lifecycle: null,
|
|
148
149
|
taskComment: null,
|
|
149
150
|
verification: null,
|
|
150
151
|
summary: null,
|
|
151
|
-
postSummaryVerification: null,
|
|
152
152
|
completedSteps: [],
|
|
153
153
|
pendingSteps: [...FINALIZATION_STEPS],
|
|
154
154
|
result: error.retryable ? 'blocked' : 'failed',
|
|
@@ -169,10 +169,8 @@ function emptyReceipt(taskId: string, controlBinding?: Readonly<{ generation: st
|
|
|
169
169
|
taskComment: 'pending',
|
|
170
170
|
verification: 'pending',
|
|
171
171
|
summary: 'pending',
|
|
172
|
-
postSummaryVerification: 'pending',
|
|
173
172
|
warningProjection: 'done',
|
|
174
173
|
warnings: [],
|
|
175
|
-
summarySha256: null,
|
|
176
174
|
...(controlBinding ? { controlBinding } : {}),
|
|
177
175
|
updatedAt: now(),
|
|
178
176
|
lastError: null
|
|
@@ -208,10 +206,8 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
|
|
|
208
206
|
|| !states.includes(String(receipt.taskComment))
|
|
209
207
|
|| !states.includes(String(receipt.verification))
|
|
210
208
|
|| !states.includes(String(receipt.summary))
|
|
211
|
-
|| !states.includes(String(receipt.postSummaryVerification))
|
|
212
209
|
|| !['pending', 'done'].includes(String(receipt.warningProjection))
|
|
213
210
|
|| !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
|
|
214
|
-
|| (receipt.summarySha256 !== null && (typeof receipt.summarySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(receipt.summarySha256)))
|
|
215
211
|
|| typeof receipt.updatedAt !== 'string'
|
|
216
212
|
|| (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
|
|
217
213
|
|| (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
|
|
@@ -250,7 +246,7 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
|
|
|
250
246
|
function updateReceipt(
|
|
251
247
|
repoRoot: string,
|
|
252
248
|
receipt: TaskFinalizationReceipt,
|
|
253
|
-
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | '
|
|
249
|
+
patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'summary' | 'warningProjection' | 'warnings' | 'controlBinding' | 'lastError'>>
|
|
254
250
|
): TaskFinalizationReceipt {
|
|
255
251
|
const current = readReceipt(repoRoot, receipt.taskId);
|
|
256
252
|
if (!current || current.receiptId !== receipt.receiptId) throw capabilityError(
|
|
@@ -272,7 +268,6 @@ function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
|
|
|
272
268
|
|
|
273
269
|
function stepState(receipt: TaskFinalizationReceipt, step: FinalizationStep): FinalizationStepState {
|
|
274
270
|
if (step === 'task-comment') return receipt.taskComment;
|
|
275
|
-
if (step === 'post-summary-verification') return receipt.postSummaryVerification;
|
|
276
271
|
return receipt[step];
|
|
277
272
|
}
|
|
278
273
|
|
|
@@ -598,15 +593,12 @@ async function syncPendingSummary(input: {
|
|
|
598
593
|
commentSync: typeof syncPlatformComment;
|
|
599
594
|
}): Promise<{ receipt: TaskFinalizationReceipt; step: TaskFinalizationStep; changed: boolean; error: FinalizationError | null }> {
|
|
600
595
|
let { receipt } = input;
|
|
601
|
-
if (receipt.summary !== 'pending') {
|
|
602
|
-
return { receipt, step: { status: receipt.summary === 'skipped' ? 'skipped' : 'no-op', changed: false, error: null }, changed: false, error: null };
|
|
603
|
-
}
|
|
604
596
|
try {
|
|
605
597
|
const resolved = resolveTaskRef(input.taskId, { repoRoot: input.repoRoot });
|
|
606
598
|
if (!resolved.ok || resolved.state !== 'completed') throw Object.assign(new Error('completed task is unavailable for summary sync'), { code: 'SUMMARY_STAGING_INVALID' });
|
|
607
599
|
if (!taskIssueIdentity(parseTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8')))) {
|
|
608
600
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
609
|
-
summary: 'skipped',
|
|
601
|
+
summary: 'skipped', lastError: null
|
|
610
602
|
});
|
|
611
603
|
return { receipt, step: { status: 'skipped', changed: false, error: null }, changed: false, error: null };
|
|
612
604
|
}
|
|
@@ -621,8 +613,7 @@ async function syncPendingSummary(input: {
|
|
|
621
613
|
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'summary' && warning.status === 'open');
|
|
622
614
|
const warnings = resolveStepWarnings(receipt, 'summary');
|
|
623
615
|
receipt = updateReceipt(input.repoRoot, receipt, {
|
|
624
|
-
summary: skipped ? 'skipped' : 'done',
|
|
625
|
-
summarySha256: skipped ? null : summary.sha256,
|
|
616
|
+
summary: skipped ? 'skipped' : 'done',
|
|
626
617
|
taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
627
618
|
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done',
|
|
628
619
|
warnings,
|
|
@@ -648,20 +639,15 @@ async function syncPendingSummary(input: {
|
|
|
648
639
|
}
|
|
649
640
|
}
|
|
650
641
|
|
|
651
|
-
function clearDeliverySummary(taskDir: string): void {
|
|
652
|
-
const file = path.join(taskDir, '.delivery-summary.json');
|
|
653
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
642
|
function terminalResult(
|
|
657
643
|
taskId: string,
|
|
658
644
|
receipt: TaskFinalizationReceipt,
|
|
659
|
-
steps: Partial<Pick<TaskFinalizationResult, '
|
|
645
|
+
steps: Partial<Pick<TaskFinalizationResult, 'backfill' | 'lifecycle' | 'taskComment' | 'verification' | 'summary'>>,
|
|
660
646
|
changed: boolean,
|
|
661
647
|
error: FinalizationError | null = null
|
|
662
648
|
): TaskFinalizationResult {
|
|
663
649
|
const pending = pendingSteps(receipt);
|
|
664
|
-
const blocked = [steps.lifecycle, steps.taskComment, steps.verification, steps.summary
|
|
650
|
+
const blocked = [steps.backfill, steps.lifecycle, steps.taskComment, steps.verification, steps.summary].some((step) => step?.status === 'blocked');
|
|
665
651
|
const warnings = openWarnings(receipt);
|
|
666
652
|
const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
|
|
667
653
|
const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
|
|
@@ -669,18 +655,18 @@ function terminalResult(
|
|
|
669
655
|
status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
|
|
670
656
|
changed,
|
|
671
657
|
taskId,
|
|
658
|
+
backfill: steps.backfill ?? null,
|
|
672
659
|
lifecycle: steps.lifecycle ?? null,
|
|
673
660
|
taskComment: steps.taskComment ?? null,
|
|
674
661
|
verification: steps.verification ?? null,
|
|
675
662
|
summary: steps.summary ?? null,
|
|
676
|
-
postSummaryVerification: steps.postSummaryVerification ?? null,
|
|
677
663
|
completedSteps: completedSteps(receipt),
|
|
678
664
|
pendingSteps: pending,
|
|
679
665
|
result: hardError
|
|
680
666
|
? (error?.retryable ? 'blocked' : 'failed')
|
|
681
|
-
: postLifecyclePending
|
|
667
|
+
: postLifecyclePending
|
|
682
668
|
? 'completed_with_warnings'
|
|
683
|
-
: pending.length === 0
|
|
669
|
+
: pending.length === 0
|
|
684
670
|
? 'completed'
|
|
685
671
|
: blocked
|
|
686
672
|
? 'blocked'
|
|
@@ -697,6 +683,7 @@ async function applyUnderLock(
|
|
|
697
683
|
): Promise<TaskFinalizationResult> {
|
|
698
684
|
const repoRoot = path.resolve(options.repoRoot);
|
|
699
685
|
const lifecycle = options.lifecycle ?? applyTaskLifecycle;
|
|
686
|
+
const backfill = options.backfill ?? backfillCompletionComments;
|
|
700
687
|
const commentSync = options.commentSync ?? syncPlatformComment;
|
|
701
688
|
const verify = options.verify ?? verifyTaskEvent;
|
|
702
689
|
const consumedCapabilities = new Set<string>();
|
|
@@ -736,8 +723,36 @@ async function applyUnderLock(
|
|
|
736
723
|
}
|
|
737
724
|
}
|
|
738
725
|
|
|
739
|
-
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
740
726
|
let changed = false;
|
|
727
|
+
let backfillResult: TaskFinalizationStep | null = null;
|
|
728
|
+
try {
|
|
729
|
+
const result = await backfill(taskId, { agent: request.agent, cwd: repoRoot });
|
|
730
|
+
backfillResult = commentStep(result);
|
|
731
|
+
const issueNotLinked = result.status === 'no-op' && result.error?.code === 'ISSUE_NOT_LINKED';
|
|
732
|
+
if (!issueNotLinked && (result.status !== 'applied' && result.status !== 'no-op' || result.error)) {
|
|
733
|
+
const detail = backfillResult.error ?? {
|
|
734
|
+
code: 'COMPLETION_BACKFILL_FAILED', message: 'completion artifact backfill did not reach a successful terminal result', retryable: true
|
|
735
|
+
};
|
|
736
|
+
return failed(taskId, detail, {
|
|
737
|
+
backfill: backfillResult,
|
|
738
|
+
completedSteps: completedSteps(receipt),
|
|
739
|
+
pendingSteps: pendingSteps(receipt)
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
changed = result.changed;
|
|
743
|
+
if (result.changed && receipt.taskComment !== 'pending') {
|
|
744
|
+
receipt = updateReceipt(repoRoot, receipt, { taskComment: 'pending' });
|
|
745
|
+
}
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const detail = errorOf(error, 'COMPLETION_BACKFILL_FAILED', true);
|
|
748
|
+
return failed(taskId, detail, {
|
|
749
|
+
backfill: { status: detail.retryable ? 'blocked' : 'failed', changed: false, error: detail },
|
|
750
|
+
completedSteps: completedSteps(receipt),
|
|
751
|
+
pendingSteps: pendingSteps(receipt)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let lifecycleResult: TaskFinalizationStep | null = null;
|
|
741
756
|
try {
|
|
742
757
|
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
743
758
|
const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
|
|
@@ -749,6 +764,7 @@ async function applyUnderLock(
|
|
|
749
764
|
};
|
|
750
765
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
751
766
|
return terminalResult(taskId, receipt, {
|
|
767
|
+
backfill: backfillResult,
|
|
752
768
|
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
753
769
|
taskComment: null,
|
|
754
770
|
verification: null
|
|
@@ -763,6 +779,7 @@ async function applyUnderLock(
|
|
|
763
779
|
};
|
|
764
780
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
|
|
765
781
|
return terminalResult(taskId, receipt, {
|
|
782
|
+
backfill: backfillResult,
|
|
766
783
|
lifecycle: { status: 'failed', changed: false, error: detail },
|
|
767
784
|
taskComment: null,
|
|
768
785
|
verification: null
|
|
@@ -780,15 +797,15 @@ async function applyUnderLock(
|
|
|
780
797
|
lifecycleResult = lifecycleStep(result);
|
|
781
798
|
if (result.status !== 'applied' && result.status !== 'no-op') {
|
|
782
799
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
|
|
783
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
800
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
|
|
784
801
|
}
|
|
785
|
-
changed = result.changed;
|
|
802
|
+
changed = changed || result.changed;
|
|
786
803
|
receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
|
|
787
804
|
}
|
|
788
805
|
} catch (error) {
|
|
789
806
|
const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
|
|
790
807
|
try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
|
|
791
|
-
return failed(taskId, detail, { lifecycle: lifecycleResult });
|
|
808
|
+
return failed(taskId, detail, { backfill: backfillResult, lifecycle: lifecycleResult });
|
|
792
809
|
}
|
|
793
810
|
|
|
794
811
|
let taskComment: TaskFinalizationStep | null = null;
|
|
@@ -800,7 +817,7 @@ async function applyUnderLock(
|
|
|
800
817
|
taskComment = initialComment.step;
|
|
801
818
|
changed = changed || initialComment.changed;
|
|
802
819
|
if (initialComment.error) {
|
|
803
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
820
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: null }, changed, initialComment.error);
|
|
804
821
|
}
|
|
805
822
|
|
|
806
823
|
let verification: TaskFinalizationStep | null = null;
|
|
@@ -826,7 +843,7 @@ async function applyUnderLock(
|
|
|
826
843
|
if (finalComment.step.status !== 'no-op') taskComment = finalComment.step;
|
|
827
844
|
changed = changed || finalComment.changed;
|
|
828
845
|
if (finalComment.error) {
|
|
829
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
846
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, finalComment.error);
|
|
830
847
|
}
|
|
831
848
|
} else {
|
|
832
849
|
receipt = updateReceipt(repoRoot, receipt, { lastError: null });
|
|
@@ -850,7 +867,7 @@ async function applyUnderLock(
|
|
|
850
867
|
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
851
868
|
changed = changed || warningComment.changed;
|
|
852
869
|
}
|
|
853
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
870
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification }, changed, detail);
|
|
854
871
|
}
|
|
855
872
|
} catch (error) {
|
|
856
873
|
const detail = errorOf(error, 'VERIFY_FAILED', true);
|
|
@@ -875,7 +892,7 @@ async function applyUnderLock(
|
|
|
875
892
|
retryable: true
|
|
876
893
|
};
|
|
877
894
|
return terminalResult(taskId, receipt, {
|
|
878
|
-
lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
895
|
+
backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail }
|
|
879
896
|
}, changed, hardError);
|
|
880
897
|
}
|
|
881
898
|
try {
|
|
@@ -884,99 +901,18 @@ async function applyUnderLock(
|
|
|
884
901
|
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
885
902
|
changed = changed || warningComment.changed;
|
|
886
903
|
} catch { /* preserve the primary error */ }
|
|
887
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
904
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
888
905
|
}
|
|
889
906
|
|
|
890
|
-
|
|
907
|
+
let summary = await syncPendingSummary({ repoRoot, taskId, agent: request.agent, receipt, commentSync });
|
|
891
908
|
receipt = summary.receipt;
|
|
892
909
|
changed = changed || summary.changed;
|
|
893
910
|
if (summary.error) {
|
|
894
911
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
895
|
-
|
|
896
|
-
repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities
|
|
897
|
-
});
|
|
898
|
-
receipt = warningComment.receipt;
|
|
899
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
900
|
-
changed = changed || warningComment.changed;
|
|
901
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
let postSummaryVerification: TaskFinalizationStep | null = null;
|
|
905
|
-
if (receipt.postSummaryVerification === 'skipped') {
|
|
906
|
-
postSummaryVerification = { status: 'skipped', changed: false, error: null };
|
|
907
|
-
} else if (receipt.postSummaryVerification !== 'pending') {
|
|
908
|
-
const replay = await commentSync(taskId, {
|
|
909
|
-
kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
|
|
910
|
-
summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
|
|
911
|
-
});
|
|
912
|
-
postSummaryVerification = commentStep(replay);
|
|
913
|
-
if (replay.status !== 'applied' && replay.status !== 'no-op') {
|
|
914
|
-
const error = postSummaryVerification.error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary replay verification failed', retryable: true };
|
|
915
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
916
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
917
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
918
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
|
|
919
|
-
}
|
|
920
|
-
} else {
|
|
921
|
-
try {
|
|
922
|
-
const summaryVerification = await commentSync(taskId, {
|
|
923
|
-
kind: 'summary', agent: request.agent, body: '', cwd: repoRoot, verifyOnly: true,
|
|
924
|
-
summaryAuthorization: { sha256: receipt.summarySha256 ?? '' }
|
|
925
|
-
});
|
|
926
|
-
if (summaryVerification.status !== 'applied' && summaryVerification.status !== 'no-op') {
|
|
927
|
-
const error = commentStep(summaryVerification).error ?? { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary verification failed', retryable: true };
|
|
928
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
929
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
930
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
931
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
932
|
-
receipt = warningComment.receipt;
|
|
933
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
934
|
-
changed = changed || warningComment.changed;
|
|
935
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: commentStep(summaryVerification) }, changed, error);
|
|
936
|
-
}
|
|
937
|
-
const result = await verify({ taskRef: taskId, event: 'complete-task.completed' }, { repoRoot });
|
|
938
|
-
postSummaryVerification = verificationStep(result);
|
|
939
|
-
if (result.status === 'pass') {
|
|
940
|
-
const resolvedWarning = receipt.warnings.some((warning) => warning.step === 'post-summary-verification' && warning.status === 'open');
|
|
941
|
-
const warnings = resolveStepWarnings(receipt, 'post-summary-verification');
|
|
942
|
-
receipt = updateReceipt(repoRoot, receipt, {
|
|
943
|
-
postSummaryVerification: 'done', taskComment: resolvedWarning ? 'pending' : receipt.taskComment,
|
|
944
|
-
warningProjection: warnings.some((warning) => warning.status === 'open') ? 'pending' : 'done', warnings, lastError: null
|
|
945
|
-
});
|
|
946
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
947
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
948
|
-
receipt = warningComment.receipt;
|
|
949
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
950
|
-
changed = changed || warningComment.changed;
|
|
951
|
-
} else {
|
|
952
|
-
const error = postSummaryVerification.error ?? { code: 'VERIFY_FAILED', message: 'post-summary verification failed', retryable: true };
|
|
953
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', error), 'open');
|
|
954
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: error });
|
|
955
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
956
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
957
|
-
receipt = warningComment.receipt;
|
|
958
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
959
|
-
changed = changed || warningComment.changed;
|
|
960
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed, error);
|
|
961
|
-
}
|
|
962
|
-
} catch (error) {
|
|
963
|
-
const detail = errorOf(error, 'POST_SUMMARY_VERIFY_FAILED', true);
|
|
964
|
-
const warnings = replaceWarning(receipt, warningFromError('post-summary-verification', detail), 'open');
|
|
965
|
-
receipt = updateReceipt(repoRoot, receipt, { postSummaryVerification: 'pending', taskComment: 'pending', warningProjection: 'pending', warnings, lastError: detail });
|
|
966
|
-
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
967
|
-
const warningComment = await syncPendingTaskComment({ repoRoot, taskId, agent: request.agent, receipt, commentSync, consumedCapabilities });
|
|
968
|
-
receipt = warningComment.receipt;
|
|
969
|
-
if (warningComment.step.status !== 'no-op') taskComment = warningComment.step;
|
|
970
|
-
changed = changed || warningComment.changed;
|
|
971
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
|
|
972
|
-
}
|
|
912
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed, summary.error);
|
|
973
913
|
}
|
|
974
914
|
receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
|
|
975
|
-
|
|
976
|
-
const resolved = resolveTaskRef(taskId, { repoRoot });
|
|
977
|
-
if (resolved.ok && resolved.state === 'completed') clearDeliverySummary(resolved.taskDir);
|
|
978
|
-
}
|
|
979
|
-
return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification, summary: summary.step, postSummaryVerification }, changed);
|
|
915
|
+
return terminalResult(taskId, receipt, { backfill: backfillResult, lifecycle: lifecycleResult, taskComment, verification, summary: summary.step }, changed);
|
|
980
916
|
}
|
|
981
917
|
|
|
982
918
|
async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -58,7 +58,7 @@ type TaskLifecycleRequest =
|
|
|
58
58
|
| { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
59
59
|
| { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
60
60
|
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
61
|
-
| { taskRef: string; intent: 'recover-started'; agent: string;
|
|
61
|
+
| { taskRef: string; intent: 'recover-started'; agent: string; auto: true; dryRun?: boolean };
|
|
62
62
|
|
|
63
63
|
type HotState = 'active' | 'blocked' | 'completed';
|
|
64
64
|
type SourceState = HotState | 'staging';
|
|
@@ -209,7 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
|
|
|
209
209
|
if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
|
|
210
210
|
if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
211
211
|
if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
212
|
-
if (request.intent === 'recover-started') return { action: 'Recover Started', note:
|
|
212
|
+
if (request.intent === 'recover-started') return { action: 'Recover Started', note: 'automatic recovery' };
|
|
213
213
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const MANUAL_VALIDATION_RECEIPT_SCHEMA = 'agent-infra/manual-validation-receipt';
|
|
16
16
|
const MANUAL_VALIDATION_RECEIPT_VERSION = 1;
|
|
17
|
+
const MANUAL_VALIDATION_RECEIPT_PLACEHOLDER = '[[manual-validation-receipt]]';
|
|
17
18
|
|
|
18
19
|
type ManualValidationReceipt = Readonly<{
|
|
19
20
|
schema: typeof MANUAL_VALIDATION_RECEIPT_SCHEMA;
|
|
@@ -73,13 +74,17 @@ function manualValidationFinalSummaryDigest(body: string): string {
|
|
|
73
74
|
const projection = heading
|
|
74
75
|
? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
|
|
75
76
|
: body;
|
|
76
|
-
const preimage = projection
|
|
77
|
+
const preimage = projection
|
|
78
|
+
.replace(/\n*<!--\s*canonical-pr-change-report\s*-->\s*/gu, '')
|
|
79
|
+
.replace(/<!--\s*manual-validation-receipt:\s*[\s\S]*?-->/gu, MANUAL_VALIDATION_RECEIPT_PLACEHOLDER)
|
|
80
|
+
.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
77
81
|
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function manualValidationFinalSummaryProjectionMatches(body: string, receipt: ManualValidationReceipt): boolean {
|
|
81
85
|
const identity = `transaction=${receipt.transactionId}; receipt=${receipt.receiptDigest}; evidence=${receipt.evidenceDigest}; head=${receipt.prHeadSha}`;
|
|
82
|
-
return body.includes(identity)
|
|
86
|
+
return (body.includes(identity) || body.includes(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER))
|
|
87
|
+
&& manualValidationFinalSummaryDigest(body) === receipt.finalSummaryDigest;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
function validateManualValidationReceipt(value: unknown, expected?: ManualValidationReceiptIdentity): ManualValidationReceiptResult {
|
|
@@ -137,6 +142,7 @@ function readManualValidationReceipt(taskDir: string, expected?: ManualValidatio
|
|
|
137
142
|
export {
|
|
138
143
|
MANUAL_VALIDATION_RECEIPT_SCHEMA,
|
|
139
144
|
MANUAL_VALIDATION_RECEIPT_VERSION,
|
|
145
|
+
MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
|
|
140
146
|
createManualValidationReceipt,
|
|
141
147
|
manualValidationFinalSummaryDigest,
|
|
142
148
|
manualValidationFinalSummaryProjectionMatches,
|
|
@@ -1417,19 +1417,34 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1417
1417
|
}
|
|
1418
1418
|
}
|
|
1419
1419
|
|
|
1420
|
+
type ActivatedRecoveryEvent = Readonly<{
|
|
1421
|
+
receiptId: string;
|
|
1422
|
+
stage: DelegationStage;
|
|
1423
|
+
round: number;
|
|
1424
|
+
artifact: string;
|
|
1425
|
+
startedAgent: string;
|
|
1426
|
+
childId: string;
|
|
1427
|
+
stopRevision: number;
|
|
1428
|
+
consumer: string;
|
|
1429
|
+
consumedAt: string;
|
|
1430
|
+
}>;
|
|
1431
|
+
|
|
1432
|
+
function recoveryReceiptMatches(
|
|
1433
|
+
receipt: DelegationReceipt,
|
|
1434
|
+
taskId: string,
|
|
1435
|
+
event: ActivatedRecoveryEvent
|
|
1436
|
+
): boolean {
|
|
1437
|
+
return receipt.taskId === taskId
|
|
1438
|
+
&& receipt.stage === event.stage
|
|
1439
|
+
&& receipt.round === event.round
|
|
1440
|
+
&& receipt.artifact === event.artifact
|
|
1441
|
+
&& receipt.childId === event.childId
|
|
1442
|
+
&& normalizeAgentToken(event.startedAgent) === normalizeAgentToken(receipt.client);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1420
1445
|
function recoverActivatedOrchestrationDelegationUnderLock(
|
|
1421
1446
|
taskRef: string,
|
|
1422
|
-
event:
|
|
1423
|
-
receiptId: string;
|
|
1424
|
-
stage: DelegationStage;
|
|
1425
|
-
round: number;
|
|
1426
|
-
artifact: string;
|
|
1427
|
-
startedAgent: string;
|
|
1428
|
-
childId: string;
|
|
1429
|
-
stopRevision: number;
|
|
1430
|
-
consumer: string;
|
|
1431
|
-
consumedAt: string;
|
|
1432
|
-
}>,
|
|
1447
|
+
event: ActivatedRecoveryEvent,
|
|
1433
1448
|
options: OrchestrationOptions = {}
|
|
1434
1449
|
): OrchestrationResult {
|
|
1435
1450
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -1442,14 +1457,7 @@ function recoverActivatedOrchestrationDelegationUnderLock(
|
|
|
1442
1457
|
];
|
|
1443
1458
|
const receipt = matches.find((candidate) => candidate.id === event.receiptId);
|
|
1444
1459
|
if (!receipt) return failed('ORCHESTRATION_DELEGATION_MISSING', 'recovery receipt does not exist', resolved.taskId);
|
|
1445
|
-
if (
|
|
1446
|
-
receipt.taskId !== resolved.taskId
|
|
1447
|
-
|| receipt.stage !== event.stage
|
|
1448
|
-
|| receipt.round !== event.round
|
|
1449
|
-
|| receipt.artifact !== event.artifact
|
|
1450
|
-
|| receipt.childId !== event.childId
|
|
1451
|
-
|| normalizeAgentToken(event.startedAgent) !== normalizeAgentToken(receipt.client)
|
|
1452
|
-
) {
|
|
1460
|
+
if (!recoveryReceiptMatches(receipt, resolved.taskId, event)) {
|
|
1453
1461
|
return failed('ORCHESTRATION_PROVENANCE_MISMATCH', 'recovery selector does not match the delegation receipt', resolved.taskId);
|
|
1454
1462
|
}
|
|
1455
1463
|
if (receipt.status === 'aborted' && run.pendingDelegation === null) {
|
|
@@ -1461,6 +1469,9 @@ function recoverActivatedOrchestrationDelegationUnderLock(
|
|
|
1461
1469
|
if (run.receipts.some((candidate) => candidate.id === receipt.id)) {
|
|
1462
1470
|
return failed('ORCHESTRATION_RECEIPT_DUPLICATE', 'recovery receipt already exists in the completed receipt history', resolved.taskId);
|
|
1463
1471
|
}
|
|
1472
|
+
if (run.status !== 'running' || run.pause !== null) {
|
|
1473
|
+
return failed('ORCHESTRATION_RECOVERY_UNSAFE', 'activated recovery cannot replace an unrelated orchestration pause', resolved.taskId);
|
|
1474
|
+
}
|
|
1464
1475
|
const aborted = abortActivatedDelegation(receipt, {
|
|
1465
1476
|
childId: event.childId,
|
|
1466
1477
|
stopRevision: event.stopRevision,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,19 +12,33 @@
|
|
|
12
12
|
|
|
13
13
|
`orchestration.json` records stages, model policy, child identity, and actual outcomes. Records use the current structure; trusted local operators may correct them and rerun validation. Existing task write locks and atomic writes protect updates.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Current Host
|
|
16
16
|
|
|
17
|
-
- Use native spawn, wait, and App Server results from the current host
|
|
17
|
+
- Use native spawn, wait, and App Server results from the current host. A task-bound sandbox executes ordinary task orchestration locally in the current worktree.
|
|
18
|
+
- Codex controller `open`, `verify`, and `close` remain restricted broker operations. After `open`, complete typed `verify` with the same lease proof before accepting the task, control-generation, and controller-instance binding.
|
|
18
19
|
- Prepare validates the current task, model policy, and host preflight. Start and terminal records associate observed parent/child identity; failure must not be recorded as success.
|
|
19
|
-
-
|
|
20
|
+
- Client-specific preflight, event sources, and stop-evidence validation belong to the client adapter. The common orchestrator calls only the shared capability and contains no client-ID branches.
|
|
21
|
+
|
|
22
|
+
## Activated Delegation Recovery
|
|
23
|
+
|
|
24
|
+
- Before `begin-or-resume`, the orchestrator calls internal `task-lifecycle <task> recover-started --agent <client> --auto`. This entry is not user-facing and never infers child termination from missing liveness evidence.
|
|
25
|
+
- The client adapter declares recovery support and validates trusted stop evidence for the current pending delegation. Unsupported adapters and runs without an activated pending delegation return `no-op`; uncertain termination fails closed.
|
|
26
|
+
- Recovery consumes stop evidence, saves an aborted receipt while clearing pending, then appends the Activity Log terminal row. If the run save succeeds and the log write fails, the next invocation appends only the missing row.
|
|
27
|
+
- Routing continues only after `no-op` or `applied`. Lost control responses are not reconstructed; the next operator invocation relies on the idempotent persisted state.
|
|
28
|
+
|
|
29
|
+
## Current-only Cutover and Rollback
|
|
30
|
+
|
|
31
|
+
- Enter a quiet window before cutover: let in-flight orchestration intents return, stop the wrapper/controller, and preserve the worktree, task/run/orchestration state, receipts, registration, and audit. For an unknown result, inspect local `status`/`route` before using existing recover/advance semantics; do not replay an old broker request.
|
|
32
|
+
- Deploy or roll back the CLI, broker, controller adapter, and protocol as one versioned unit. After stopping the old container/broker, rebuild and run `ai sandbox start --recreate <task-ref>` to resume with a new control generation; never reuse an old token, proof, or request.
|
|
33
|
+
- On either the new or rolled-back version, first confirm the same task through `status`/`route`, then require a successful typed controller `open`→`verify`. Keep the quiet window and evidence intact if any check fails; do not mix component versions or rewrite domain facts.
|
|
20
34
|
|
|
21
35
|
## Model Policy
|
|
22
36
|
|
|
23
37
|
- A new run persists model and reasoning effort for both roles. Explicit policy is atomic across all four role fields; only a fully absent explicit policy may fall back to the current client's `agentClients[].orchestration`.
|
|
24
38
|
- Route resolves requested model/effort by role, and prepare matches both before snapshotting. Native spawn must not inherit session defaults.
|
|
25
|
-
- Native start records host-observed actual model/effort.
|
|
39
|
+
- Native start records host-observed actual model/effort. Fields declared unobservable by the adapter follow the recording rules; each observable mismatch needs its own fallback reason, and requested values must never be fabricated as actual evidence.
|
|
26
40
|
- Model selection is labeled as a complete catalog, partial catalog, or interactive-only guidance; a local override enum must not be presented as complete.
|
|
27
|
-
-
|
|
41
|
+
- An adapter must declare any policy field it cannot dispatch per role. Values observed in host events are actual evidence only; they do not promise dispatch or gate activation.
|
|
28
42
|
|
|
29
43
|
## Stable Pause Conditions
|
|
30
44
|
|
|
@@ -12,19 +12,33 @@
|
|
|
12
12
|
|
|
13
13
|
`orchestration.json` 记录阶段、模型策略、child 身份及实际执行结果。记录必须符合当前结构;本地操作者可直接修正后重新校验。已有任务写锁与原子写入继续保护写操作。
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 当前宿主
|
|
16
16
|
|
|
17
|
-
- 使用当前宿主的原生 spawn、wait 与 App Server
|
|
17
|
+
- 使用当前宿主的原生 spawn、wait 与 App Server 结果;task-bound sandbox 在当前 worktree 本地执行普通任务编排。
|
|
18
|
+
- Codex controller 的 `open`、`verify`、`close` 继续通过受限 broker 执行。`open` 后必须用同一 lease proof 完成 typed `verify`,再接受 task、control generation 和 controller instance 绑定。
|
|
18
19
|
- prepare 校验当前任务、模型策略与宿主 preflight。启动和完成记录关联实际 parent/child 身份,失败不得写成成功。
|
|
19
|
-
-
|
|
20
|
+
- 客户端特有的 preflight、事件来源与停止证据校验由客户端适配器实现;公共总控只调用统一能力。不得在公共流程中新增客户端 ID 分支。
|
|
21
|
+
|
|
22
|
+
## Activated delegation 自动恢复
|
|
23
|
+
|
|
24
|
+
- 总控在 `begin-or-resume` 前调用内部 `task-lifecycle <task> recover-started --agent <client> --auto`。该入口不公开给用户,也不从缺少存活证据推断 child 已终止。
|
|
25
|
+
- 客户端适配器决定是否支持恢复,并验证当前 pending delegation 的可信停止证据;不支持或没有 activated pending 时返回 `no-op`,无法确认停止时失败关闭。
|
|
26
|
+
- 恢复按固定顺序消费停止证据、保存 aborted receipt 并清除 pending,再写 Activity Log 结束记录。任务状态已保存而日志未写时,下次调用只补日志。
|
|
27
|
+
- 只有 `no-op` 或 `applied` 才能继续 route。控制响应丢失时不重建恢复结果;下一次人为调用依靠幂等状态继续。
|
|
28
|
+
|
|
29
|
+
## Current-only 切换与回滚
|
|
30
|
+
|
|
31
|
+
- 切换前进入静默窗口:等待在途编排 intent 返回,停止 wrapper/controller,并保留 worktree、task/run/orchestration 状态、receipts、registration 与 audit。结果未知时先用本地 `status`/`route` 判断 domain 是否已提交,再按现有 recover/advance 语义处理;旧 broker request 不重放。
|
|
32
|
+
- CLI、broker、controller adapter 和协议必须作为同一版本单元部署或回退。停止旧 container/broker 后执行 rebuild 与 `ai sandbox start --recreate <task-ref>`,以新 control generation 恢复执行;旧 token、proof 和 request 不得复用。
|
|
33
|
+
- 新版本或回退版本都必须先核对同一任务的 `status`/`route`,再要求 controller typed `open`→`verify` 成功。任一检查失败时继续保持静默并保留证据,不局部混用组件或改写 domain 事实。
|
|
20
34
|
|
|
21
35
|
## 模型策略
|
|
22
36
|
|
|
23
37
|
- 新 run 必须固化 executor/reviewer 各自的 model 与 reasoning effort;显式策略必须四字段原子完整,完全没有显式字段时才读取当前 client 的 `agentClients[].orchestration`。重入不得静默改写策略。
|
|
24
38
|
- route 按 role 返回 requested model/effort,prepare 必须在工作区快照前精确匹配两者;原生 spawn 不能继承会话默认值。
|
|
25
|
-
- 原生 start 必须记录宿主观察到的 actual model/effort
|
|
39
|
+
- 原生 start 必须记录宿主观察到的 actual model/effort。适配器声明无法观察的字段按记录规则处理;任一可观察字段与 requested 不同时必须记录独立 fallback reason,requested 值不得补造 actual 证据。
|
|
26
40
|
- 模型选择能力必须标记 complete catalog、partial catalog 或 interactive-only guidance;局部 override 枚举不得冒充完整目录。
|
|
27
|
-
-
|
|
41
|
+
- 客户端无法按角色下发的策略字段必须由适配器明确声明;宿主事件中观察到的值只作为实际证据,不构成下发承诺或放行门禁。
|
|
28
42
|
|
|
29
43
|
## 稳定暂停条件
|
|
30
44
|
|
|
@@ -21,7 +21,7 @@ Before generating the validation completion report, read `.agents/rules/evidence
|
|
|
21
21
|
- This skill closes the manual-validation status in an existing PR summary comment; it does not create a parallel ordinary validation comment.
|
|
22
22
|
- It must write `manual-validation.md` or `manual-validation-r{N}.md` so later PR summary refreshes can reuse the validation result.
|
|
23
23
|
- If the `sync-pr` summary comment is missing, fail instead of creating a partial fallback summary; before the receipt, completion log, and final summary are all committed, the remote summary may only be pending/non-pass.
|
|
24
|
-
- Before generating manual-validation artifact Markdown
|
|
24
|
+
- Before generating manual-validation artifact Markdown referenced by the task comment and PR summary, read `.agents/rules/sync-content-generation.md` and follow its generator-side constraints. Keep the artifact local instead of publishing it as an Issue artifact comment.
|
|
25
25
|
- After this skill runs, update `task.md` immediately.
|
|
26
26
|
|
|
27
27
|
Version stamp rule: when creating or updating `task.md` frontmatter, read `.agents/rules/version-stamp.md` first and write or refresh `agent_infra_version`.
|
|
@@ -106,7 +106,7 @@ The coordinator owns pending summary, receipt, completion log, final promotion,
|
|
|
106
106
|
|
|
107
107
|
After the transaction coordinator succeeds, the core has atomically recorded `manual-validation.completed` with the same transaction/receipt/head identity. Do not append the Activity Log manually.
|
|
108
108
|
|
|
109
|
-
If the task has a valid `platform_issue_identity`, run `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}
|
|
109
|
+
If the task has a valid `platform_issue_identity`, run only `agent-infra-internal platform-comment sync {task-id} --kind task --agent {standard-agent-token}`. Keep the manual-validation artifact as local evidence and PR-summary input; do not publish it as an Issue comment.
|
|
110
110
|
|
|
111
111
|
### 8. Verification Gate
|
|
112
112
|
|