@fitlab-ai/agent-infra 0.9.15 → 0.11.0
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 +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +4 -4
- package/dist/lib/internal/task-operation-registry.js +22 -13
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +12 -7
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +89 -31
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +45 -7
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +87 -35
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +11 -30
- package/dist/lib/task/artifact-operations.js +17 -231
- package/dist/lib/task/artifact-recovery.js +476 -0
- package/dist/lib/task/artifact-repair-intent.js +90 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +39 -8
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +165 -23
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +91 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +212 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +3 -2
- package/dist/lib/task/review-finalization.js +294 -74
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +2 -1
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +5 -4
- package/lib/internal/task-operation-registry.ts +22 -13
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +11 -7
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +105 -30
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +48 -11
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +6 -1
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +106 -35
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +12 -29
- package/lib/task/artifact-operations.ts +22 -252
- package/lib/task/artifact-recovery.ts +572 -0
- package/lib/task/artifact-repair-intent.ts +126 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +48 -15
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +188 -24
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +66 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +249 -131
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +2 -1
- package/lib/task/review-finalization.ts +298 -84
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +2 -1
- package/lib/task/workflow-dispatch.ts +171 -0
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +12 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
- package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- 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/restore-task/SKILL.en.md +11 -6
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -3,36 +3,63 @@ import path from 'node:path';
|
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
|
|
5
5
|
import type { ArtifactSchemaFamily } from './artifact-schema.ts';
|
|
6
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
6
7
|
|
|
7
|
-
export type
|
|
8
|
+
export type ArtifactRecoveryPhase =
|
|
8
9
|
| 'orchestration.prepare'
|
|
9
10
|
| 'artifact.finalize-local'
|
|
10
11
|
| 'task-event.completed';
|
|
11
|
-
export type
|
|
12
|
+
export type ArtifactRecoveryState =
|
|
13
|
+
| 'awaiting-preflight-recovery'
|
|
14
|
+
| 'preflight-ready'
|
|
15
|
+
| 'preflight-commit-started'
|
|
16
|
+
| 'preflight-passed'
|
|
17
|
+
| 'full-finalizer-ready'
|
|
18
|
+
| 'commit-started'
|
|
19
|
+
| 'passed'
|
|
20
|
+
| 'consumption-started'
|
|
21
|
+
| 'consumed'
|
|
22
|
+
| 'aborted';
|
|
12
23
|
|
|
13
|
-
export type
|
|
14
|
-
version:
|
|
24
|
+
export type ArtifactRecoveryIntent = Readonly<{
|
|
25
|
+
version: 4;
|
|
15
26
|
taskId: string;
|
|
16
27
|
family: ArtifactSchemaFamily;
|
|
17
28
|
artifact: string;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
round: number;
|
|
30
|
+
state: ArtifactRecoveryState;
|
|
31
|
+
baselineSha256: string;
|
|
32
|
+
baselineSemanticDigest: string;
|
|
33
|
+
stagingId: string;
|
|
34
|
+
candidateSha256: string | null;
|
|
35
|
+
preflightArtifactSha256: string | null;
|
|
36
|
+
preflightSemanticDigest: string | null;
|
|
37
|
+
activeGenerationSha256: string | null;
|
|
38
|
+
activeGenerationSemanticDigest: string | null;
|
|
39
|
+
pendingGenerationSha256: string | null;
|
|
40
|
+
pendingGenerationSemanticDigest: string | null;
|
|
41
|
+
finalArtifactSha256: string | null;
|
|
42
|
+
finalSemanticDigest: string | null;
|
|
43
|
+
recoveryOperationId: string;
|
|
44
|
+
phase: ArtifactRecoveryPhase | null;
|
|
24
45
|
authorityDigest: string | null;
|
|
25
46
|
requestId: string;
|
|
47
|
+
errorCode: string | null;
|
|
48
|
+
errorMessage: string | null;
|
|
26
49
|
createdAt: number;
|
|
27
50
|
updatedAt: number;
|
|
28
51
|
}>;
|
|
29
52
|
|
|
30
|
-
function
|
|
53
|
+
export function artifactRecoveryIntentRoot(repoRoot: string): string {
|
|
31
54
|
return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
|
|
32
55
|
}
|
|
33
56
|
|
|
57
|
+
export function artifactRecoveryRoot(taskDir: string, stagingId: string): string {
|
|
58
|
+
return path.join(taskDir, '.local-artifact-recovery', stagingId);
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
function intentPath(repoRoot: string, taskId: string, family: ArtifactSchemaFamily, artifact: string): string {
|
|
35
|
-
return path.join(
|
|
62
|
+
return path.join(artifactRecoveryIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
function validDigest(value: unknown): value is string {
|
|
@@ -43,82 +70,132 @@ function validTaskId(value: unknown): value is string {
|
|
|
43
70
|
return typeof value === 'string' && /^TASK-\d{8}-\d{6}$/u.test(value);
|
|
44
71
|
}
|
|
45
72
|
|
|
73
|
+
function validToken(value: unknown): value is string {
|
|
74
|
+
return typeof value === 'string' && value.length > 0 && !/[\r\n]/u.test(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validRecoveryId(value: unknown): value is string {
|
|
78
|
+
return typeof value === 'string' && /^[a-f0-9-]{16,64}$/u.test(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validArtifact(value: unknown, family: unknown, round: unknown): value is string {
|
|
82
|
+
if (typeof value !== 'string' || path.basename(value) !== value || !value.endsWith('.md')) return false;
|
|
83
|
+
const parsed = parseArtifactName(value);
|
|
84
|
+
return Boolean(parsed && parsed.family === family && parsed.round === round);
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
47
88
|
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
export function
|
|
91
|
+
export function isArtifactRecoveryIntent(value: unknown): value is ArtifactRecoveryIntent {
|
|
51
92
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
52
93
|
const intent = value as Record<string, unknown>;
|
|
53
94
|
return exactKeys(intent, [
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
95
|
+
'activeGenerationSemanticDigest', 'activeGenerationSha256', 'artifact', 'authorityDigest', 'baselineSemanticDigest', 'baselineSha256', 'candidateSha256',
|
|
96
|
+
'createdAt', 'errorCode', 'errorMessage', 'family', 'finalArtifactSha256', 'finalSemanticDigest',
|
|
97
|
+
'pendingGenerationSemanticDigest', 'pendingGenerationSha256', 'phase', 'preflightArtifactSha256', 'preflightSemanticDigest', 'recoveryOperationId', 'requestId', 'round', 'stagingId', 'state', 'taskId', 'updatedAt',
|
|
98
|
+
'version'
|
|
57
99
|
])
|
|
58
|
-
&& intent.version ===
|
|
100
|
+
&& intent.version === 4
|
|
59
101
|
&& validTaskId(intent.taskId)
|
|
60
102
|
&& typeof intent.family === 'string'
|
|
61
103
|
&& ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(intent.family)
|
|
62
|
-
&&
|
|
63
|
-
&&
|
|
64
|
-
&&
|
|
65
|
-
&& validDigest(intent.
|
|
66
|
-
&& validDigest(intent.
|
|
67
|
-
&& (intent.
|
|
104
|
+
&& Number.isSafeInteger(intent.round) && (intent.round as number) > 0
|
|
105
|
+
&& validArtifact(intent.artifact, intent.family, intent.round)
|
|
106
|
+
&& ['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started', 'passed', 'consumption-started', 'consumed', 'aborted'].includes(String(intent.state))
|
|
107
|
+
&& validDigest(intent.baselineSha256)
|
|
108
|
+
&& validDigest(intent.baselineSemanticDigest)
|
|
109
|
+
&& validRecoveryId(intent.stagingId)
|
|
110
|
+
&& (intent.candidateSha256 === null || validDigest(intent.candidateSha256))
|
|
111
|
+
&& (intent.preflightArtifactSha256 === null || validDigest(intent.preflightArtifactSha256))
|
|
112
|
+
&& (intent.preflightSemanticDigest === null || validDigest(intent.preflightSemanticDigest))
|
|
113
|
+
&& (intent.activeGenerationSha256 === null || validDigest(intent.activeGenerationSha256))
|
|
114
|
+
&& (intent.activeGenerationSemanticDigest === null || validDigest(intent.activeGenerationSemanticDigest))
|
|
115
|
+
&& (intent.pendingGenerationSha256 === null || validDigest(intent.pendingGenerationSha256))
|
|
116
|
+
&& (intent.pendingGenerationSemanticDigest === null || validDigest(intent.pendingGenerationSemanticDigest))
|
|
117
|
+
&& (intent.finalArtifactSha256 === null || validDigest(intent.finalArtifactSha256))
|
|
118
|
+
&& (intent.finalSemanticDigest === null || validDigest(intent.finalSemanticDigest))
|
|
119
|
+
&& validRecoveryId(intent.recoveryOperationId)
|
|
120
|
+
&& intent.stagingId === intent.recoveryOperationId
|
|
68
121
|
&& (intent.phase === null || ['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(String(intent.phase)))
|
|
69
122
|
&& (intent.authorityDigest === null || validDigest(intent.authorityDigest))
|
|
70
|
-
&&
|
|
71
|
-
&&
|
|
72
|
-
&& (intent.
|
|
123
|
+
&& validToken(intent.requestId)
|
|
124
|
+
&& (intent.errorCode === null || validToken(intent.errorCode))
|
|
125
|
+
&& (intent.errorMessage === null || validToken(intent.errorMessage))
|
|
126
|
+
&& Number.isSafeInteger(intent.createdAt)
|
|
127
|
+
&& Number.isSafeInteger(intent.updatedAt)
|
|
128
|
+
&& (intent.state === 'awaiting-preflight-recovery'
|
|
129
|
+
? intent.preflightArtifactSha256 === null && intent.preflightSemanticDigest === null
|
|
130
|
+
&& intent.activeGenerationSha256 === null && intent.activeGenerationSemanticDigest === null
|
|
131
|
+
&& intent.pendingGenerationSha256 === null && intent.pendingGenerationSemanticDigest === null
|
|
132
|
+
&& intent.finalArtifactSha256 === null && intent.finalSemanticDigest === null
|
|
133
|
+
: true)
|
|
134
|
+
&& (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started' || intent.state === 'preflight-passed' || intent.state === 'full-finalizer-ready' || intent.state === 'commit-started' || intent.state === 'passed' || intent.state === 'consumed'
|
|
135
|
+
? validDigest(intent.candidateSha256) && validDigest(intent.preflightArtifactSha256) && validDigest(intent.preflightSemanticDigest)
|
|
136
|
+
&& validDigest(intent.activeGenerationSha256) && validDigest(intent.activeGenerationSemanticDigest)
|
|
137
|
+
: true)
|
|
138
|
+
&& (intent.state === 'full-finalizer-ready' || intent.state === 'commit-started'
|
|
139
|
+
? validDigest(intent.pendingGenerationSha256) && validDigest(intent.pendingGenerationSemanticDigest)
|
|
140
|
+
&& validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
|
|
141
|
+
: true)
|
|
142
|
+
&& (intent.state === 'passed' || intent.state === 'consumption-started' || intent.state === 'consumed'
|
|
143
|
+
? validDigest(intent.finalArtifactSha256) && validDigest(intent.finalSemanticDigest)
|
|
144
|
+
: true)
|
|
145
|
+
&& (intent.state === 'aborted' ? intent.errorCode !== null && intent.errorMessage !== null : true);
|
|
73
146
|
}
|
|
74
147
|
|
|
75
|
-
function
|
|
148
|
+
export function readArtifactRecoveryIntent(
|
|
76
149
|
repoRoot: string,
|
|
77
150
|
taskId: string,
|
|
78
151
|
family: ArtifactSchemaFamily,
|
|
79
152
|
artifact: string
|
|
80
|
-
):
|
|
153
|
+
): ArtifactRecoveryIntent | null {
|
|
154
|
+
if (!validTaskId(taskId) || !validArtifact(artifact, family, parseArtifactName(artifact)?.round)) {
|
|
155
|
+
throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal identity is invalid');
|
|
156
|
+
}
|
|
81
157
|
const target = intentPath(repoRoot, taskId, family, artifact);
|
|
82
158
|
if (!fs.existsSync(target)) return null;
|
|
83
159
|
let value: unknown;
|
|
84
160
|
try { value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown; }
|
|
85
|
-
catch (error) { throw new Error(`
|
|
86
|
-
if (!
|
|
87
|
-
throw new Error('
|
|
161
|
+
catch (error) { throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`); }
|
|
162
|
+
if (!isArtifactRecoveryIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
|
|
163
|
+
throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
|
|
88
164
|
}
|
|
89
165
|
return value;
|
|
90
166
|
}
|
|
91
167
|
|
|
92
|
-
export function
|
|
168
|
+
export function writeArtifactRecoveryIntent(
|
|
93
169
|
repoRoot: string,
|
|
94
|
-
value:
|
|
95
|
-
options: Readonly<{ expected?:
|
|
170
|
+
value: unknown,
|
|
171
|
+
options: Readonly<{ expected?: ArtifactRecoveryIntent | null }> = {}
|
|
96
172
|
): void {
|
|
97
|
-
if (!
|
|
98
|
-
const
|
|
173
|
+
if (!isArtifactRecoveryIntent(value)) throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
|
|
174
|
+
const intent = value;
|
|
175
|
+
const directory = artifactRecoveryIntentRoot(repoRoot);
|
|
99
176
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
100
|
-
const target = intentPath(repoRoot,
|
|
177
|
+
const target = intentPath(repoRoot, intent.taskId, intent.family, intent.artifact);
|
|
101
178
|
const lock = `${target}.lock`;
|
|
102
179
|
let descriptor: number;
|
|
103
180
|
try { descriptor = fs.openSync(lock, 'wx', 0o600); }
|
|
104
181
|
catch (error) {
|
|
105
182
|
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
106
|
-
throw new Error('
|
|
183
|
+
throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: another journal writer owns the intent');
|
|
107
184
|
}
|
|
108
185
|
throw error;
|
|
109
186
|
}
|
|
110
187
|
try {
|
|
111
188
|
const current = fs.existsSync(target)
|
|
112
|
-
?
|
|
189
|
+
? readArtifactRecoveryIntent(repoRoot, intent.taskId, intent.family, intent.artifact)
|
|
113
190
|
: null;
|
|
114
191
|
if (!isDeepStrictEqual(current, options.expected ?? null)) {
|
|
115
|
-
throw new Error('
|
|
192
|
+
throw new Error('ARTIFACT_RECOVERY_INTENT_CONFLICT: recovery journal changed during finalization');
|
|
116
193
|
}
|
|
117
194
|
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
118
|
-
fs.writeFileSync(temporary, `${JSON.stringify(
|
|
195
|
+
fs.writeFileSync(temporary, `${JSON.stringify(intent)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
119
196
|
try { fs.renameSync(temporary, target); }
|
|
120
197
|
catch (error) {
|
|
121
|
-
try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
|
|
198
|
+
try { fs.unlinkSync(temporary); } catch { /* preserve the primary error */ }
|
|
122
199
|
throw error;
|
|
123
200
|
}
|
|
124
201
|
} finally {
|
|
@@ -127,14 +204,12 @@ export function writeArtifactRepairIntent(
|
|
|
127
204
|
}
|
|
128
205
|
}
|
|
129
206
|
|
|
130
|
-
export function
|
|
207
|
+
export function findArtifactRecoveryIntentsByOperation(
|
|
131
208
|
repoRoot: string,
|
|
132
209
|
operationId: string
|
|
133
|
-
): readonly
|
|
134
|
-
if (!operationId
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
const directory = intentRoot(repoRoot);
|
|
210
|
+
): readonly ArtifactRecoveryIntent[] {
|
|
211
|
+
if (!validToken(operationId)) throw new Error('ARTIFACT_RECOVERY_OPERATION_INVALID: recovery operation id is invalid');
|
|
212
|
+
const directory = artifactRecoveryIntentRoot(repoRoot);
|
|
138
213
|
if (!fs.existsSync(directory)) return Object.freeze([]);
|
|
139
214
|
return Object.freeze(fs.readdirSync(directory)
|
|
140
215
|
.filter((name) => name.endsWith('.json'))
|
|
@@ -142,11 +217,11 @@ export function findArtifactRepairIntentsByOperation(
|
|
|
142
217
|
const target = path.join(directory, name);
|
|
143
218
|
let value: unknown;
|
|
144
219
|
try { value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown; }
|
|
145
|
-
catch (error) { throw new Error(`
|
|
146
|
-
if (!
|
|
220
|
+
catch (error) { throw new Error(`ARTIFACT_RECOVERY_INTENT_INVALID: ${String(error)}`); }
|
|
221
|
+
if (!isArtifactRecoveryIntent(value)) throw new Error('ARTIFACT_RECOVERY_INTENT_INVALID: recovery journal schema is invalid');
|
|
147
222
|
return value;
|
|
148
223
|
})
|
|
149
224
|
.filter((value) => value.recoveryOperationId === operationId));
|
|
150
225
|
}
|
|
151
226
|
|
|
152
|
-
export {
|
|
227
|
+
export { intentPath as artifactRecoveryIntentPath };
|
|
@@ -32,6 +32,8 @@ type CommitOperationInput = Readonly<{
|
|
|
32
32
|
agent?: string;
|
|
33
33
|
mode?: CommitExecutionMode;
|
|
34
34
|
round?: number;
|
|
35
|
+
/** Permit one retry to rebase a prepared intent's expected tree while HEAD and intent identity remain unchanged. */
|
|
36
|
+
recoverPreparedIntent?: boolean;
|
|
35
37
|
delivery?: Readonly<{
|
|
36
38
|
mode: 'local' | 'push';
|
|
37
39
|
remote: string;
|
|
@@ -280,6 +282,20 @@ function checkpointTimestamp(): string {
|
|
|
280
282
|
return new Date().toISOString();
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
function samePreparedCheckpointIdentity(
|
|
286
|
+
pending: NonNullable<ReturnType<typeof readCheckpointIntent>>,
|
|
287
|
+
identity: Parameters<typeof checkpointIntentDigest>[0]
|
|
288
|
+
): boolean {
|
|
289
|
+
return pending.state === 'prepared'
|
|
290
|
+
&& pending.taskId === identity.taskId
|
|
291
|
+
&& pending.branch === identity.branch
|
|
292
|
+
&& pending.mode === identity.mode
|
|
293
|
+
&& pending.expectedHead === identity.expectedHead
|
|
294
|
+
&& pending.message === identity.message
|
|
295
|
+
&& pending.round === identity.round
|
|
296
|
+
&& JSON.stringify(pending.paths) === JSON.stringify(identity.paths);
|
|
297
|
+
}
|
|
298
|
+
|
|
283
299
|
function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
|
|
284
300
|
const inspected = inspectGitWorkflow(input.cwd);
|
|
285
301
|
if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
|
|
@@ -313,10 +329,25 @@ function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mo
|
|
|
313
329
|
error: { code: 'COMMIT_INTENT_INVALID', message: error instanceof Error ? error.message : String(error) }
|
|
314
330
|
};
|
|
315
331
|
}
|
|
316
|
-
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity))
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
332
|
+
if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
|
|
333
|
+
if (!input.recoverPreparedIntent || !samePreparedCheckpointIdentity(pendingIntent, identity)) return {
|
|
334
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
335
|
+
error: { code: 'COMMIT_INTENT_CONFLICT', message: 'A different checkpoint intent is still pending for this task' }
|
|
336
|
+
};
|
|
337
|
+
pendingIntent = {
|
|
338
|
+
...pendingIntent,
|
|
339
|
+
expectedTree: identity.expectedTree,
|
|
340
|
+
digest: checkpointIntentDigest(identity),
|
|
341
|
+
updatedAt: checkpointTimestamp()
|
|
342
|
+
};
|
|
343
|
+
try { writeCheckpointIntent(task!.repoRoot, pendingIntent); }
|
|
344
|
+
catch (error) {
|
|
345
|
+
return {
|
|
346
|
+
status: 'failed' as const, changed: false, snapshot: inspected.snapshot, operations: [],
|
|
347
|
+
error: { code: 'COMMIT_INTENT_WRITE_FAILED', message: error instanceof Error ? error.message : String(error) }
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
320
351
|
if (!pendingIntent) {
|
|
321
352
|
const timestamp = checkpointTimestamp();
|
|
322
353
|
pendingIntent = {
|
|
@@ -39,15 +39,21 @@ import {
|
|
|
39
39
|
type TaskLifecycleRequest,
|
|
40
40
|
type TaskLifecycleResult
|
|
41
41
|
} from './lifecycle.ts';
|
|
42
|
+
import {
|
|
43
|
+
recoverStartedLifecycleUnderLock,
|
|
44
|
+
recoveryFailure,
|
|
45
|
+
type LifecycleRecoveryResult
|
|
46
|
+
} from './lifecycle-recovery.ts';
|
|
47
|
+
import type { LifecycleRecoveryRequest } from './lifecycle-recovery.ts';
|
|
42
48
|
import { locateActivityLog } from './activity-log.ts';
|
|
43
49
|
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
|
|
44
50
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
45
51
|
import { verifyTaskEvent } from './verification.ts';
|
|
46
52
|
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
47
53
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
type
|
|
54
|
+
findArtifactRecoveryIntentsByOperation,
|
|
55
|
+
writeArtifactRecoveryIntent,
|
|
56
|
+
type ArtifactRecoveryIntent
|
|
51
57
|
} from './artifact-repair-intent.ts';
|
|
52
58
|
import {
|
|
53
59
|
computeLifecycleBuildIdentity,
|
|
@@ -333,7 +339,7 @@ export function queryLifecycleRecoveryOperation(
|
|
|
333
339
|
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
334
340
|
const registry = authorityOperationMap.get(store);
|
|
335
341
|
const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
|
|
336
|
-
const intents = options.repoRoot ?
|
|
342
|
+
const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
|
|
337
343
|
const capabilities = store.findByRecoveryOperation(operationId);
|
|
338
344
|
const phaseMap = new Map<string, LifecycleRecoveryOperationQueryV1['phases'][number]>();
|
|
339
345
|
for (const capability of capabilities) {
|
|
@@ -416,13 +422,13 @@ function lifecycleRecoveryEventCommitted(
|
|
|
416
422
|
}
|
|
417
423
|
|
|
418
424
|
function lifecycleRecoveryIntentMatches(
|
|
419
|
-
intent:
|
|
425
|
+
intent: ArtifactRecoveryIntent,
|
|
420
426
|
selector: LifecycleAuthorityRequestV1
|
|
421
427
|
): boolean {
|
|
422
428
|
return intent.taskId === selector.taskId
|
|
423
429
|
&& intent.family === selector.family
|
|
424
430
|
&& intent.artifact === selector.artifact
|
|
425
|
-
&& (intent.state === 'commit-started' || intent.state === 'consumed');
|
|
431
|
+
&& (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
|
|
426
432
|
}
|
|
427
433
|
|
|
428
434
|
/**
|
|
@@ -473,19 +479,19 @@ export function recoverLifecycleRecoveryOperation(
|
|
|
473
479
|
if (!phase) {
|
|
474
480
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
|
|
475
481
|
}
|
|
476
|
-
const intents =
|
|
482
|
+
const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
|
|
477
483
|
.filter((intent) => intent.taskId === selector.taskId
|
|
478
484
|
&& intent.family === selector.family
|
|
479
485
|
&& intent.artifact === selector.artifact);
|
|
480
486
|
if (intents.length !== 1) {
|
|
481
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact
|
|
487
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
|
|
482
488
|
}
|
|
483
489
|
const intent = intents[0]!;
|
|
484
490
|
if (!lifecycleRecoveryIntentMatches(intent, selector)
|
|
485
491
|
|| intent.phase !== selector.phase
|
|
486
492
|
|| intent.requestId !== selector.lifecycleRequestId
|
|
487
493
|
|| intent.authorityDigest === null) {
|
|
488
|
-
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact
|
|
494
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
|
|
489
495
|
}
|
|
490
496
|
if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
|
|
491
497
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
|
|
@@ -499,7 +505,7 @@ export function recoverLifecycleRecoveryOperation(
|
|
|
499
505
|
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
|
|
500
506
|
}
|
|
501
507
|
if (intent.state !== 'consumed') {
|
|
502
|
-
|
|
508
|
+
writeArtifactRecoveryIntent(options.repoRoot, {
|
|
503
509
|
...intent,
|
|
504
510
|
state: 'consumed',
|
|
505
511
|
updatedAt: (options.now ?? Date.now)()
|
|
@@ -565,7 +571,7 @@ export type TaskControlOperation =
|
|
|
565
571
|
options?: Omit<OrchestrationOptions, 'repoRoot'>;
|
|
566
572
|
}>;
|
|
567
573
|
|
|
568
|
-
export type TaskControlDispatchResult = TaskLifecycleResult | TaskFinalizationResult | OrchestrationResult;
|
|
574
|
+
export type TaskControlDispatchResult = TaskLifecycleResult | LifecycleRecoveryResult | TaskFinalizationResult | OrchestrationResult;
|
|
569
575
|
|
|
570
576
|
export function taskControlOperationKey(operation: TaskControlOperation): string {
|
|
571
577
|
if (operation.family === 'task-finalization') return 'complete';
|
|
@@ -712,12 +718,15 @@ function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string)
|
|
|
712
718
|
async function applyLifecycleWithAuthority(
|
|
713
719
|
context: TaskControlExecutionContext,
|
|
714
720
|
request: TaskLifecycleControlRequest
|
|
715
|
-
): Promise<TaskLifecycleResult & { humanOverride?: unknown }> {
|
|
721
|
+
): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> {
|
|
716
722
|
const conflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
|
|
717
723
|
if (conflict) {
|
|
718
724
|
return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
|
|
719
725
|
}
|
|
720
|
-
const execute = async (): Promise<TaskLifecycleResult & { humanOverride?: unknown }> => {
|
|
726
|
+
const execute = async (): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> => {
|
|
727
|
+
if (request.intent === 'recover-started') {
|
|
728
|
+
return recoverStartedLifecycleUnderLock(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
|
|
729
|
+
}
|
|
721
730
|
const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
|
|
722
731
|
if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
|
|
723
732
|
if (!request.overrideTarget || !request.overrideScope) {
|
|
@@ -751,6 +760,9 @@ async function applyLifecycleWithAuthority(
|
|
|
751
760
|
);
|
|
752
761
|
} catch (error) {
|
|
753
762
|
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
763
|
+
if (request.intent === 'recover-started') {
|
|
764
|
+
return recoveryFailure(request as LifecycleRecoveryRequest, 'owner-unknown', error.code, error.message);
|
|
765
|
+
}
|
|
754
766
|
return taskLifecycleFailure(request, { code: error.code, message: error.message }, taskId);
|
|
755
767
|
}
|
|
756
768
|
}
|
|
@@ -829,7 +841,7 @@ function orchestrationFailure(error: unknown): OrchestrationResult {
|
|
|
829
841
|
export function dispatchTaskControlOperation(
|
|
830
842
|
context: TaskControlExecutionContext,
|
|
831
843
|
operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
|
|
832
|
-
): Promise<TaskLifecycleResult & { humanOverride?: unknown }>;
|
|
844
|
+
): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }>;
|
|
833
845
|
export function dispatchTaskControlOperation(
|
|
834
846
|
context: TaskControlExecutionContext,
|
|
835
847
|
operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
|
|
@@ -892,7 +904,7 @@ function operationInvalid(message: string): never {
|
|
|
892
904
|
|
|
893
905
|
const LIFECYCLE_FLAGS = new Set([
|
|
894
906
|
'--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
|
|
895
|
-
'--override-ticket', '--override-target', '--override-scope', '--dry-run'
|
|
907
|
+
'--stage', '--round', '--artifact', '--override-ticket', '--override-target', '--override-scope', '--dry-run'
|
|
896
908
|
]);
|
|
897
909
|
|
|
898
910
|
const FINALIZATION_FLAGS = new Set(['--agent']);
|
|
@@ -972,8 +984,29 @@ export function parseTaskControlOperation(
|
|
|
972
984
|
...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
|
|
973
985
|
...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
|
|
974
986
|
...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
|
|
987
|
+
...(value(values, '--stage') ? { stage: value(values, '--stage') } : {}),
|
|
988
|
+
...(value(values, '--round') ? { round: Number(value(values, '--round')) } : {}),
|
|
989
|
+
...(value(values, '--artifact') ? { artifact: value(values, '--artifact') } : {}),
|
|
975
990
|
...(values['--dry-run'] === true ? { dryRun: true } : {})
|
|
976
991
|
};
|
|
992
|
+
if (intent === 'recover-started') {
|
|
993
|
+
const recoveryFlags = new Set(['--agent', '--stage', '--round', '--artifact', '--reason']);
|
|
994
|
+
for (const flag of Object.keys(values)) {
|
|
995
|
+
if (!recoveryFlags.has(flag)) operationInvalid(`option '${flag}' is not supported for recover-started`);
|
|
996
|
+
}
|
|
997
|
+
required(values, ['--stage', '--round', '--artifact', '--reason']);
|
|
998
|
+
const stage = value(values, '--stage');
|
|
999
|
+
if (!['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(stage ?? '')) {
|
|
1000
|
+
operationInvalid('--stage must be a supported lifecycle stage');
|
|
1001
|
+
}
|
|
1002
|
+
const round = Number(value(values, '--round'));
|
|
1003
|
+
if (!Number.isSafeInteger(round) || round < 1) operationInvalid('--round must be a positive integer');
|
|
1004
|
+
if (value(values, '--reason')!.includes('\n') || value(values, '--reason')!.includes('\r')) {
|
|
1005
|
+
operationInvalid('--reason must be a single line');
|
|
1006
|
+
}
|
|
1007
|
+
} else if (values['--stage'] !== undefined || values['--round'] !== undefined || values['--artifact'] !== undefined) {
|
|
1008
|
+
operationInvalid('--stage, --round, and --artifact are only supported for recover-started');
|
|
1009
|
+
}
|
|
977
1010
|
return { family, request: input as unknown as TaskLifecycleControlRequest };
|
|
978
1011
|
}
|
|
979
1012
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { isCompletionEvidence } from './orchestration.ts';
|
|
3
3
|
import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.ts';
|
|
4
|
+
import { sameRecoveryWarning } from './recovery-warning.ts';
|
|
4
5
|
|
|
5
6
|
export type ControlRecoveryOutcome = 'not-executed' | 'in-progress' | 'success' | 'failure' | 'unknown' | 'rejected';
|
|
6
7
|
|
|
@@ -53,7 +54,7 @@ export type ControlRecoveryDecision = Readonly<{
|
|
|
53
54
|
reasonCode: string;
|
|
54
55
|
}>;
|
|
55
56
|
|
|
56
|
-
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'] as const;
|
|
57
|
+
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'] as const;
|
|
57
58
|
const ORCHESTRATION_INTENTS = [
|
|
58
59
|
'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
|
|
59
60
|
'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
|
|
@@ -107,6 +108,15 @@ function domainEvidenceMatches(
|
|
|
107
108
|
&& snapshot.worktreeTree === completion.worktreeTree
|
|
108
109
|
&& lastReviewedCommit === completion.lastReviewedCommit;
|
|
109
110
|
}
|
|
111
|
+
if (operation.family === 'task-lifecycle' && operation.intent === 'recover-started') {
|
|
112
|
+
const retryRequired = domain.recoveryState === 'retry-required'
|
|
113
|
+
&& sameRecoveryWarning(domain.warning, result.warning);
|
|
114
|
+
return result.targetState === 'active'
|
|
115
|
+
&& ['applied', 'no-op'].includes(String(result.status))
|
|
116
|
+
&& domain.recovery === true
|
|
117
|
+
&& domain.targetState === 'active'
|
|
118
|
+
&& (domain.recoveryState === 'released' || retryRequired);
|
|
119
|
+
}
|
|
110
120
|
if (operation.class === 'read-only') return result.changed === false && domain.snapshotValid === true;
|
|
111
121
|
return true;
|
|
112
122
|
}
|
|
@@ -159,6 +169,8 @@ export function classifySandboxControlRecovery(input: ControlRecoveryInput): Con
|
|
|
159
169
|
responseReconstructable: true,
|
|
160
170
|
reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
|
|
161
171
|
? 'RECOVERY_ROUTE_COMPLETION_NOOP'
|
|
172
|
+
: input.operation.intent === 'recover-started' && input.domain?.recoveryState === 'retry-required'
|
|
173
|
+
? 'RECOVERY_RELEASE_RETRY_REQUIRED'
|
|
162
174
|
: 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
|
|
163
175
|
};
|
|
164
176
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { syncPlatformComment } from '../platform/issue-comments.ts';
|
|
5
2
|
import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
|
|
6
3
|
import type { PlatformResult } from '../platform/types.ts';
|
|
@@ -190,12 +187,6 @@ function platformOperations(prefix: string, operations: readonly { name: string;
|
|
|
190
187
|
}));
|
|
191
188
|
}
|
|
192
189
|
|
|
193
|
-
function issueFromTask(repoRoot: string, taskId: string): { number: number; url: string } | null {
|
|
194
|
-
const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
|
|
195
|
-
const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
|
|
196
|
-
return match ? { number: Number(match[1]), url: '' } : null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
190
|
async function createTask(value: unknown, options: CreateTaskOptions): Promise<TaskCreateResult> {
|
|
200
191
|
const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
|
|
201
192
|
let candidate: TaskCreateCandidateV1;
|
|
@@ -215,7 +206,7 @@ async function createTask(value: unknown, options: CreateTaskOptions): Promise<T
|
|
|
215
206
|
const operations: TaskCreateOperation[] = [{
|
|
216
207
|
name: 'task:local', status: local.status, reasonCode: null
|
|
217
208
|
}];
|
|
218
|
-
let issue =
|
|
209
|
+
let issue: TaskCreateResult['issue'] = null;
|
|
219
210
|
const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
220
211
|
operations.push(...platformOperations('platform-create', created.operations));
|
|
221
212
|
if (created.issue) issue = { number: created.issue.number, url: created.issue.url };
|