@fitlab-ai/agent-infra 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. package/lib/task/commit-intent.ts +0 -230
@@ -0,0 +1,719 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ import { syncPlatformComment } from '../platform/issue-comments.ts';
6
+ import type { PlatformResult } from '../platform/types.ts';
7
+ import {
8
+ applyTaskLifecycle,
9
+ type TaskLifecycleOptions,
10
+ type TaskLifecycleRequest,
11
+ type TaskLifecycleResult
12
+ } from './lifecycle.ts';
13
+ import { resolveTaskRef } from './resolve-ref.ts';
14
+ import { inspectShortIdRegistry } from './short-id.ts';
15
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
16
+ import { verifyTaskEvent } from './verification.ts';
17
+ import type { TaskVerificationResult } from './verification.ts';
18
+ import { projectFinalizationWarning } from './workflow-warning-intents.ts';
19
+ import {
20
+ mergeOperationWarnings,
21
+ type OperationWarning,
22
+ type OperationWarningSeverity
23
+ } from './operation-outcome.ts';
24
+
25
+ const RECEIPT_VERSION = 2 as const;
26
+ const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification'] as const;
27
+ type FinalizationStep = typeof FINALIZATION_STEPS[number];
28
+ type FinalizationStepState = 'pending' | 'done' | 'skipped';
29
+ type FinalizationError = { code: string; message: string; retryable: boolean };
30
+ type WarningProjectionState = 'pending' | 'done';
31
+ type FinalizationWarning = OperationWarning & Readonly<{
32
+ status: 'open' | 'resolved';
33
+ resolvedAt: string | null;
34
+ }>;
35
+ type FinalizationCapability = Readonly<{
36
+ receiptId: string;
37
+ baseRevision: number;
38
+ scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection';
39
+ nonce: string;
40
+ issuedAt: string;
41
+ }>;
42
+ type FinalizationMutation =
43
+ | Readonly<{ scope: 'task-comment'; operation: 'succeeded'; state: 'done' | 'skipped' }>
44
+ | Readonly<{ scope: 'task-comment'; operation: 'failed'; error: FinalizationError }>
45
+ | Readonly<{ scope: 'verification'; operation: 'succeeded' }>
46
+ | Readonly<{ scope: 'verification'; operation: 'failed'; error: FinalizationError }>
47
+ | Readonly<{ scope: 'warning-projection'; operation: 'succeeded' }>
48
+ | Readonly<{ scope: 'warning-projection'; operation: 'failed'; error: FinalizationError }>;
49
+
50
+ type TaskFinalizationRequest = Readonly<{
51
+ taskRef: string;
52
+ intent: 'complete';
53
+ agent: string;
54
+ }>;
55
+
56
+ type TaskFinalizationOptions = Readonly<{
57
+ repoRoot: string;
58
+ metadataProvider?: TaskLifecycleOptions['metadataProvider'];
59
+ lifecycle?: typeof applyTaskLifecycle;
60
+ commentSync?: typeof syncPlatformComment;
61
+ verify?: typeof verifyTaskEvent;
62
+ preflight?: typeof verifyTaskEvent;
63
+ }>;
64
+
65
+ type TaskFinalizationReceipt = Readonly<{
66
+ version: typeof RECEIPT_VERSION;
67
+ taskId: string;
68
+ intent: 'complete';
69
+ receiptId: string;
70
+ revision: number;
71
+ lifecycle: FinalizationStepState;
72
+ taskComment: FinalizationStepState;
73
+ verification: FinalizationStepState;
74
+ warningProjection: WarningProjectionState;
75
+ warnings: readonly FinalizationWarning[];
76
+ updatedAt: string;
77
+ lastError: FinalizationError | null;
78
+ }>;
79
+
80
+ type TaskFinalizationStep = Readonly<{
81
+ status: string;
82
+ changed: boolean;
83
+ error: FinalizationError | null;
84
+ completedSteps?: readonly string[];
85
+ pendingSteps?: readonly string[];
86
+ }>;
87
+
88
+ type TaskFinalizationResult = Readonly<{
89
+ status: 'completed' | 'failed' | 'blocked';
90
+ changed: boolean;
91
+ taskId: string | null;
92
+ lifecycle: TaskFinalizationStep | null;
93
+ taskComment: TaskFinalizationStep | null;
94
+ verification: TaskFinalizationStep | null;
95
+ completedSteps: readonly FinalizationStep[];
96
+ pendingSteps: readonly FinalizationStep[];
97
+ result: 'completed' | 'completed_with_warnings' | 'failed' | 'blocked';
98
+ warnings: readonly OperationWarning[];
99
+ error: FinalizationError | null;
100
+ }>;
101
+
102
+ function finalizationRoot(repoRoot: string): string {
103
+ return path.join(repoRoot, '.agents', 'workspace', '.task-finalization');
104
+ }
105
+
106
+ function receiptPath(repoRoot: string, taskId: string): string {
107
+ return path.join(finalizationRoot(repoRoot), `${taskId}.json`);
108
+ }
109
+
110
+ function now(): string {
111
+ return new Date().toISOString();
112
+ }
113
+
114
+ function errorOf(error: unknown, fallbackCode: string, retryable = false): FinalizationError {
115
+ const value = error as { code?: unknown; message?: unknown; retryable?: unknown } | null;
116
+ const code = typeof value?.code === 'string' && value.code ? value.code : fallbackCode;
117
+ const message = typeof value?.message === 'string' && value.message ? value.message : String(error);
118
+ return { code, message, retryable: value?.retryable === true || retryable };
119
+ }
120
+
121
+ function failed(
122
+ taskId: string | null,
123
+ error: FinalizationError,
124
+ overrides: Partial<TaskFinalizationResult> = {}
125
+ ): TaskFinalizationResult {
126
+ return {
127
+ status: error.retryable ? 'blocked' : 'failed',
128
+ changed: false,
129
+ taskId,
130
+ lifecycle: null,
131
+ taskComment: null,
132
+ verification: null,
133
+ completedSteps: [],
134
+ pendingSteps: [...FINALIZATION_STEPS],
135
+ result: error.retryable ? 'blocked' : 'failed',
136
+ warnings: [],
137
+ error,
138
+ ...overrides
139
+ };
140
+ }
141
+
142
+ function emptyReceipt(taskId: string): TaskFinalizationReceipt {
143
+ return {
144
+ version: RECEIPT_VERSION,
145
+ taskId,
146
+ intent: 'complete',
147
+ receiptId: randomUUID(),
148
+ revision: 0,
149
+ lifecycle: 'pending',
150
+ taskComment: 'pending',
151
+ verification: 'pending',
152
+ warningProjection: 'done',
153
+ warnings: [],
154
+ updatedAt: now(),
155
+ lastError: null
156
+ };
157
+ }
158
+
159
+ function validWarning(value: unknown): value is FinalizationWarning {
160
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
161
+ const warning = value as Record<string, unknown>;
162
+ if (Object.keys(warning).sort().join('\0') !== [
163
+ 'code', 'message', 'retryable', 'step', 'target', 'severity', 'status', 'resolvedAt'
164
+ ].sort().join('\0')) return false;
165
+ return typeof warning.code === 'string' && warning.code.length > 0
166
+ && typeof warning.message === 'string' && warning.message.length > 0
167
+ && typeof warning.retryable === 'boolean'
168
+ && typeof warning.step === 'string' && warning.step.length > 0
169
+ && typeof warning.target === 'string' && warning.target.length > 0
170
+ && (warning.severity === 'IMPORTANT' || warning.severity === 'ACTION_REQUIRED')
171
+ && (warning.status === 'open' || warning.status === 'resolved')
172
+ && (warning.resolvedAt === null || typeof warning.resolvedAt === 'string');
173
+ }
174
+
175
+ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceipt {
176
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('receipt must be an object');
177
+ const receipt = value as Record<string, unknown>;
178
+ const states = ['pending', 'done', 'skipped'];
179
+ if (
180
+ receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
181
+ || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
182
+ || !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
183
+ || !states.includes(String(receipt.lifecycle))
184
+ || !states.includes(String(receipt.taskComment))
185
+ || !states.includes(String(receipt.verification))
186
+ || !['pending', 'done'].includes(String(receipt.warningProjection))
187
+ || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
188
+ || typeof receipt.updatedAt !== 'string'
189
+ || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
190
+ ) throw new Error('receipt schema is invalid');
191
+ return receipt as TaskFinalizationReceipt;
192
+ }
193
+
194
+ function readReceipt(repoRoot: string, taskId: string): TaskFinalizationReceipt | null {
195
+ const file = receiptPath(repoRoot, taskId);
196
+ if (!fs.existsSync(file)) return null;
197
+ const value = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;
198
+ return validateReceipt(value, taskId);
199
+ }
200
+
201
+ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void {
202
+ const directory = finalizationRoot(repoRoot);
203
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
204
+ const target = receiptPath(repoRoot, receipt.taskId);
205
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
206
+ try {
207
+ fs.writeFileSync(temporary, `${JSON.stringify(receipt)}\n`, { mode: 0o600, flag: 'wx' });
208
+ fs.renameSync(temporary, target);
209
+ } catch (error) {
210
+ try { fs.unlinkSync(temporary); } catch { /* preserve the primary error */ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ function updateReceipt(
216
+ repoRoot: string,
217
+ receipt: TaskFinalizationReceipt,
218
+ patch: Partial<Pick<TaskFinalizationReceipt, 'lifecycle' | 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>>
219
+ ): TaskFinalizationReceipt {
220
+ const current = readReceipt(repoRoot, receipt.taskId);
221
+ if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
222
+ const error = new Error('finalization receipt revision is stale');
223
+ Object.assign(error, { code: 'FINALIZATION_CAPABILITY_STALE' });
224
+ throw error;
225
+ }
226
+ const next = { ...current, ...patch, revision: current.revision + 1, updatedAt: now() };
227
+ writeReceipt(repoRoot, next);
228
+ return next;
229
+ }
230
+
231
+ function completedSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
232
+ return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] !== 'pending');
233
+ }
234
+
235
+ function pendingSteps(receipt: TaskFinalizationReceipt): FinalizationStep[] {
236
+ return FINALIZATION_STEPS.filter((step) => receipt[step === 'task-comment' ? 'taskComment' : step] === 'pending');
237
+ }
238
+
239
+ function lifecycleStep(result: TaskLifecycleResult): TaskFinalizationStep {
240
+ return {
241
+ status: result.status,
242
+ changed: result.changed,
243
+ error: result.error ? errorOf(result.error, 'LIFECYCLE_FAILED') : null,
244
+ completedSteps: result.completedSteps ?? [],
245
+ pendingSteps: result.pendingSteps ?? []
246
+ };
247
+ }
248
+
249
+ function commentStep(result: PlatformResult): TaskFinalizationStep {
250
+ return {
251
+ status: result.status,
252
+ changed: result.changed,
253
+ error: result.error ? errorOf(result.error, 'COMMENT_SYNC_FAILED') : null
254
+ };
255
+ }
256
+
257
+ function verificationStep(result: TaskVerificationResult): TaskFinalizationStep {
258
+ return {
259
+ status: result.status,
260
+ changed: false,
261
+ error: result.status === 'pass' ? null : verificationFailure(result)
262
+ };
263
+ }
264
+
265
+ function verificationFailure(result: TaskVerificationResult): FinalizationError {
266
+ if (result.error) return errorOf(result.error, 'VERIFY_FAILED', result.status === 'blocked');
267
+ const invocation = [...result.invocations].reverse().find((item) => item.status !== 'pass');
268
+ const payload = invocation?.payload;
269
+ const details = [payload?.message, payload?.summary, payload?.action]
270
+ .filter((value): value is string => typeof value === 'string' && value.length > 0);
271
+ const code = result.status === 'blocked' || invocation?.status === 'blocked'
272
+ ? 'CHECK_BLOCKED'
273
+ : result.status === 'fail' ? 'CHECK_FAILED' : 'VERIFY_FAILED';
274
+ return {
275
+ code,
276
+ message: details.length > 0 ? details.join(' - ') : `verification ${result.status}`,
277
+ retryable: code === 'CHECK_BLOCKED'
278
+ };
279
+ }
280
+
281
+ function warningFromError(
282
+ step: FinalizationStep,
283
+ error: FinalizationError,
284
+ severity: OperationWarningSeverity = 'ACTION_REQUIRED'
285
+ ): OperationWarning {
286
+ return {
287
+ code: error.code,
288
+ message: error.message,
289
+ retryable: error.retryable,
290
+ step,
291
+ target: step,
292
+ severity
293
+ };
294
+ }
295
+
296
+ function openWarnings(receipt: TaskFinalizationReceipt): readonly OperationWarning[] {
297
+ return mergeOperationWarnings(
298
+ receipt.warnings.filter((warning) => warning.status === 'open').map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning)
299
+ );
300
+ }
301
+
302
+ function warningRecord(warning: OperationWarning, status: FinalizationWarning['status']): FinalizationWarning {
303
+ return { ...warning, status, resolvedAt: status === 'resolved' ? now() : null };
304
+ }
305
+
306
+ function replaceWarning(
307
+ receipt: TaskFinalizationReceipt,
308
+ warning: OperationWarning,
309
+ status: FinalizationWarning['status']
310
+ ): readonly FinalizationWarning[] {
311
+ const next = warningRecord(warning, status);
312
+ const key = `${warning.step}\0${warning.code}\0${warning.target}`;
313
+ return [
314
+ ...receipt.warnings.filter((item) => `${item.step}\0${item.code}\0${item.target}` !== key),
315
+ next
316
+ ];
317
+ }
318
+
319
+ function resolveStepWarnings(receipt: TaskFinalizationReceipt, step: FinalizationStep): readonly FinalizationWarning[] {
320
+ return receipt.warnings.map((warning) => warning.step === step && warning.status === 'open'
321
+ ? { ...warning, status: 'resolved', resolvedAt: now() }
322
+ : warning);
323
+ }
324
+
325
+ function issueCapability(receipt: TaskFinalizationReceipt, scope: Exclude<FinalizationStep, 'lifecycle'> | 'warning-projection'): FinalizationCapability {
326
+ return {
327
+ receiptId: receipt.receiptId,
328
+ baseRevision: receipt.revision,
329
+ scope,
330
+ nonce: randomUUID(),
331
+ issuedAt: now()
332
+ };
333
+ }
334
+
335
+ function capabilityError(code: 'FINALIZATION_CAPABILITY_STALE' | 'FINALIZATION_SCOPE_INVALID', message: string): Error {
336
+ const error = new Error(message);
337
+ error.name = 'OrchestrationStateError';
338
+ Object.assign(error, { code });
339
+ return error;
340
+ }
341
+
342
+ function mutationKeys(value: FinalizationMutation): string[] {
343
+ return Object.keys(value).sort();
344
+ }
345
+
346
+ function validateCapabilityMutation(
347
+ current: TaskFinalizationReceipt,
348
+ capability: FinalizationCapability,
349
+ mutation: FinalizationMutation,
350
+ consumed: Set<string>
351
+ ): void {
352
+ if (capability.receiptId !== current.receiptId || capability.baseRevision !== current.revision || consumed.has(capability.nonce)) {
353
+ throw capabilityError('FINALIZATION_CAPABILITY_STALE', 'finalization capability is stale or outside its scope');
354
+ }
355
+ if (capability.scope !== mutation.scope) {
356
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation scope does not match its capability');
357
+ }
358
+ if (mutation.operation !== 'succeeded' && mutation.operation !== 'failed') {
359
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability operation is invalid');
360
+ }
361
+ if (mutation.scope === 'task-comment') {
362
+ if (current.taskComment !== 'pending') throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability scope task-comment is not pending');
363
+ const expected = mutation.operation === 'succeeded' ? ['operation', 'scope', 'state'] : ['error', 'operation', 'scope'];
364
+ if (mutationKeys(mutation).join('\0') !== expected.join('\0')) throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation shape is invalid');
365
+ if (mutation.operation === 'succeeded' && mutation.state !== 'done' && mutation.state !== 'skipped') {
366
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability transition is invalid');
367
+ }
368
+ } else if (mutation.scope === 'verification') {
369
+ if (current.verification !== 'pending') throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability scope verification is not pending');
370
+ const expected = mutation.operation === 'succeeded' ? ['operation', 'scope'] : ['error', 'operation', 'scope'];
371
+ if (mutationKeys(mutation).join('\0') !== expected.join('\0')) throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation shape is invalid');
372
+ } else {
373
+ if (current.warningProjection !== 'pending') throw capabilityError('FINALIZATION_SCOPE_INVALID', 'warning projection capability is not pending');
374
+ const expected = mutation.operation === 'succeeded' ? ['operation', 'scope'] : ['error', 'operation', 'scope'];
375
+ if (mutationKeys(mutation).join('\0') !== expected.join('\0')) throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability mutation shape is invalid');
376
+ }
377
+ if (mutation.operation === 'failed' && (!mutation.error.code || !mutation.error.message || typeof mutation.error.retryable !== 'boolean')) {
378
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability failure detail is invalid');
379
+ }
380
+ }
381
+
382
+ function mutationPatch(current: TaskFinalizationReceipt, mutation: FinalizationMutation): Partial<Pick<TaskFinalizationReceipt, 'taskComment' | 'verification' | 'warningProjection' | 'warnings' | 'lastError'>> {
383
+ if (mutation.scope === 'task-comment') {
384
+ if (mutation.operation === 'succeeded') {
385
+ const warnings = resolveStepWarnings(current, 'task-comment');
386
+ return { taskComment: mutation.state, warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null };
387
+ }
388
+ const warnings = replaceWarning(current, warningFromError('task-comment', mutation.error), 'open');
389
+ return { taskComment: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
390
+ }
391
+ if (mutation.scope === 'verification') {
392
+ if (mutation.operation === 'succeeded') {
393
+ const warnings = resolveStepWarnings(current, 'verification');
394
+ return { verification: 'done', warningProjection: warnings.length > 0 ? 'pending' : 'done', warnings, lastError: null };
395
+ }
396
+ const warnings = replaceWarning(current, warningFromError('verification', mutation.error), 'open');
397
+ return { verification: 'pending', warningProjection: 'pending', warnings, lastError: mutation.error };
398
+ }
399
+ return mutation.operation === 'succeeded'
400
+ ? { warningProjection: 'done', lastError: null }
401
+ : { warningProjection: 'pending', lastError: mutation.error };
402
+ }
403
+
404
+ function applyFinalizationReceiptMutationUnderLock(
405
+ repoRoot: string,
406
+ receipt: TaskFinalizationReceipt,
407
+ capability: FinalizationCapability,
408
+ mutation: FinalizationMutation,
409
+ consumed: Set<string>
410
+ ): TaskFinalizationReceipt {
411
+ const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
412
+ if (!resolved.ok || resolved.state !== 'completed') {
413
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
414
+ }
415
+ const current = readReceipt(repoRoot, receipt.taskId);
416
+ if (!current || current.receiptId !== receipt.receiptId || current.revision !== receipt.revision) {
417
+ throw capabilityError('FINALIZATION_CAPABILITY_STALE', 'finalization receipt revision is stale');
418
+ }
419
+ validateCapabilityMutation(current, capability, mutation, consumed);
420
+ const next = updateReceipt(repoRoot, current, mutationPatch(current, mutation));
421
+ consumed.add(capability.nonce);
422
+ return next;
423
+ }
424
+
425
+ function applyFinalizationReceiptMutation(
426
+ repoRoot: string,
427
+ receipt: TaskFinalizationReceipt,
428
+ capability: FinalizationCapability,
429
+ mutation: FinalizationMutation
430
+ ): TaskFinalizationReceipt {
431
+ const resolved = resolveTaskRef(receipt.taskId, { repoRoot });
432
+ if (!resolved.ok || resolved.state !== 'completed') {
433
+ throw capabilityError('FINALIZATION_SCOPE_INVALID', 'finalization capability requires a completed task');
434
+ }
435
+ return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.receipt-mutation', () =>
436
+ applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, mutation, new Set<string>())
437
+ );
438
+ }
439
+
440
+ function warningKey(warning: Pick<FinalizationWarning, 'step' | 'code' | 'target'>): string {
441
+ return `${warning.step}\0${warning.code}\0${warning.target}`;
442
+ }
443
+
444
+ function reconcileWarningProjection(repoRoot: string, taskId: string, receipt: TaskFinalizationReceipt, consumed: Set<string>): TaskFinalizationReceipt {
445
+ if (receipt.warningProjection === 'done') return receipt;
446
+ const warnings = [...new Map(receipt.warnings.map((warning) => [warningKey(warning), warning])).values()];
447
+ for (const warning of warnings) {
448
+ try {
449
+ const projected = projectFinalizationWarning(taskId, warning, { repoRoot });
450
+ if (projected.status === 'failed') {
451
+ const detail: FinalizationError = {
452
+ code: projected.error?.code || 'FINALIZATION_WARNING_PROJECTION_FAILED',
453
+ message: projected.error?.message || 'workflow warning projection failed',
454
+ retryable: true
455
+ };
456
+ const capability = issueCapability(receipt, 'warning-projection');
457
+ return applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, { scope: 'warning-projection', operation: 'failed', error: detail }, consumed);
458
+ }
459
+ } catch (error) {
460
+ const detail = errorOf(error, 'FINALIZATION_WARNING_PROJECTION_FAILED', true);
461
+ try {
462
+ const capability = issueCapability(receipt, 'warning-projection');
463
+ return applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, { scope: 'warning-projection', operation: 'failed', error: detail }, consumed);
464
+ }
465
+ catch { return receipt; }
466
+ }
467
+ }
468
+ try {
469
+ const capability = issueCapability(receipt, 'warning-projection');
470
+ return applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, { scope: 'warning-projection', operation: 'succeeded' }, consumed);
471
+ }
472
+ catch { return receipt; }
473
+ }
474
+
475
+ function terminalResult(
476
+ taskId: string,
477
+ receipt: TaskFinalizationReceipt,
478
+ steps: Pick<TaskFinalizationResult, 'lifecycle' | 'taskComment' | 'verification'>,
479
+ changed: boolean,
480
+ error: FinalizationError | null = null
481
+ ): TaskFinalizationResult {
482
+ const pending = pendingSteps(receipt);
483
+ const blocked = [steps.lifecycle, steps.taskComment, steps.verification].some((step) => step?.status === 'blocked');
484
+ const warnings = openWarnings(receipt);
485
+ const postLifecyclePending = receipt.lifecycle === 'done' && (pending.some((step) => step !== 'lifecycle') || receipt.warningProjection === 'pending');
486
+ const hardError = error?.code.startsWith('FINALIZATION_') || error?.code === 'TASK_FINALIZATION_RECEIPT_INVALID';
487
+ return {
488
+ status: hardError ? (error?.retryable ? 'blocked' : 'failed') : pending.length === 0 ? 'completed' : postLifecyclePending ? 'completed' : blocked ? 'blocked' : 'failed',
489
+ changed,
490
+ taskId,
491
+ ...steps,
492
+ completedSteps: completedSteps(receipt),
493
+ pendingSteps: pending,
494
+ result: postLifecyclePending && (warnings.length > 0 || receipt.warningProjection === 'pending')
495
+ ? 'completed_with_warnings'
496
+ : hardError
497
+ ? (error?.retryable ? 'blocked' : 'failed')
498
+ : pending.length === 0 || postLifecyclePending
499
+ ? 'completed'
500
+ : blocked
501
+ ? 'blocked'
502
+ : 'failed',
503
+ warnings,
504
+ error: hardError ? error : postLifecyclePending ? null : error
505
+ };
506
+ }
507
+
508
+ function applyUnderLock(
509
+ request: TaskFinalizationRequest,
510
+ taskId: string,
511
+ options: TaskFinalizationOptions
512
+ ): TaskFinalizationResult {
513
+ const repoRoot = path.resolve(options.repoRoot);
514
+ const lifecycle = options.lifecycle ?? applyTaskLifecycle;
515
+ const commentSync = options.commentSync ?? syncPlatformComment;
516
+ const verify = options.verify ?? verifyTaskEvent;
517
+ const consumedCapabilities = new Set<string>();
518
+ let receipt: TaskFinalizationReceipt;
519
+ try {
520
+ const file = receiptPath(repoRoot, taskId);
521
+ const existed = fs.existsSync(file);
522
+ receipt = readReceipt(repoRoot, taskId) ?? emptyReceipt(taskId);
523
+ if (!existed) writeReceipt(repoRoot, receipt);
524
+ } catch (error) {
525
+ return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
526
+ }
527
+
528
+ const preflightState = resolveTaskRef(taskId, { repoRoot });
529
+ if (options.preflight && preflightState.ok && preflightState.state === 'active') {
530
+ try {
531
+ const preflight = options.preflight(
532
+ { taskRef: taskId, event: 'complete-task.preflight' },
533
+ { repoRoot }
534
+ );
535
+ if (preflight.status !== 'pass') {
536
+ const detail = verificationFailure(preflight);
537
+ return failed(taskId, detail, {
538
+ lifecycle: null,
539
+ taskComment: null,
540
+ verification: null,
541
+ completedSteps: completedSteps(receipt),
542
+ pendingSteps: pendingSteps(receipt)
543
+ });
544
+ }
545
+ } catch (error) {
546
+ return failed(taskId, errorOf(error, 'TASK_FINALIZATION_PREFLIGHT_FAILED', true));
547
+ }
548
+ }
549
+
550
+ let lifecycleResult: TaskFinalizationStep | null = null;
551
+ let changed = false;
552
+ try {
553
+ const resolved = resolveTaskRef(taskId, { repoRoot });
554
+ const registry = resolved.ok && resolved.state === 'completed' ? inspectShortIdRegistry(repoRoot) : null;
555
+ if (registry && registry.status !== 'valid') {
556
+ const detail: FinalizationError = {
557
+ code: 'TASK_FINALIZATION_SHORT_ID_REGISTRY_UNAVAILABLE',
558
+ message: `cannot verify canonical short-id registry: ${registry.error.code}: ${registry.error.message}`,
559
+ retryable: false
560
+ };
561
+ receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
562
+ return terminalResult(taskId, receipt, {
563
+ lifecycle: { status: 'failed', changed: false, error: detail },
564
+ taskComment: null,
565
+ verification: null
566
+ }, changed, detail);
567
+ }
568
+ const shortIds = registry?.status === 'valid' ? registry.shortIds : null;
569
+ if (shortIds?.has(taskId)) {
570
+ const detail: FinalizationError = {
571
+ code: 'TASK_FINALIZATION_CANONICAL_STATE_INVALID',
572
+ message: 'completed task still has an active short-id registry entry',
573
+ retryable: false
574
+ };
575
+ receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail });
576
+ return terminalResult(taskId, receipt, {
577
+ lifecycle: { status: 'failed', changed: false, error: detail },
578
+ taskComment: null,
579
+ verification: null
580
+ }, changed, detail);
581
+ }
582
+ const lifecycleDone = receipt.lifecycle === 'done' && resolved.ok && resolved.state === 'completed' && shortIds !== null;
583
+ if (lifecycleDone) {
584
+ lifecycleResult = { status: 'no-op', changed: false, error: null };
585
+ receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
586
+ } else {
587
+ const result = lifecycle(
588
+ { taskRef: taskId, intent: 'complete', agent: request.agent },
589
+ { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
590
+ );
591
+ lifecycleResult = lifecycleStep(result);
592
+ if (result.status !== 'applied' && result.status !== 'no-op') {
593
+ receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: lifecycleResult.error });
594
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: null, verification: null }, result.changed, lifecycleResult.error);
595
+ }
596
+ changed = result.changed;
597
+ receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'done', lastError: null });
598
+ }
599
+ } catch (error) {
600
+ const detail = errorOf(error, 'TASK_FINALIZATION_LIFECYCLE_FAILED');
601
+ try { receipt = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: detail }); } catch { /* preserve the primary error */ }
602
+ return failed(taskId, detail, { lifecycle: lifecycleResult });
603
+ }
604
+
605
+ let taskComment: TaskFinalizationStep | null = null;
606
+ try {
607
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
608
+ if (receipt.taskComment !== 'pending') {
609
+ taskComment = {
610
+ status: receipt.taskComment === 'skipped' ? 'skipped' : 'no-op',
611
+ changed: false,
612
+ error: null
613
+ };
614
+ } else {
615
+ const result = commentSync(taskId, { kind: 'task', agent: request.agent, cwd: repoRoot });
616
+ taskComment = commentStep(result);
617
+ if (result.status === 'applied' || result.status === 'no-op') {
618
+ const skipped = result.error?.code === 'ISSUE_NOT_LINKED';
619
+ const capability = issueCapability(receipt, 'task-comment');
620
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
621
+ scope: 'task-comment', operation: 'succeeded', state: skipped ? 'skipped' : 'done'
622
+ }, consumedCapabilities);
623
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
624
+ taskComment = skipped ? { ...taskComment, status: 'skipped' } : taskComment;
625
+ changed = changed || result.changed;
626
+ } else {
627
+ const detail = taskComment.error ?? { code: 'COMMENT_SYNC_FAILED', message: 'task comment synchronization failed', retryable: true };
628
+ const capability = issueCapability(receipt, 'task-comment');
629
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
630
+ scope: 'task-comment', operation: 'failed', error: detail
631
+ }, consumedCapabilities);
632
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
633
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: null }, changed, taskComment.error);
634
+ }
635
+ }
636
+ } catch (error) {
637
+ const detail = errorOf(error, 'COMMENT_SYNC_FAILED', true);
638
+ try {
639
+ const capability = issueCapability(receipt, 'task-comment');
640
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
641
+ scope: 'task-comment', operation: 'failed', error: detail
642
+ }, consumedCapabilities);
643
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
644
+ } catch { /* preserve the primary error */ }
645
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment: { status: 'blocked', changed: false, error: detail }, verification: null }, changed, detail);
646
+ }
647
+
648
+ let verification: TaskFinalizationStep | null = null;
649
+ try {
650
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
651
+ if (receipt.verification !== 'pending') {
652
+ verification = { status: 'no-op', changed: false, error: null };
653
+ } else {
654
+ const result = verify(
655
+ { taskRef: taskId, event: 'complete-task.completed' },
656
+ { repoRoot }
657
+ );
658
+ verification = verificationStep(result);
659
+ if (result.status === 'pass') {
660
+ const capability = issueCapability(receipt, 'verification');
661
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
662
+ scope: 'verification', operation: 'succeeded'
663
+ }, consumedCapabilities);
664
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
665
+ } else {
666
+ const detail = verification.error ?? { code: 'VERIFY_FAILED', message: 'verification failed', retryable: true };
667
+ const capability = issueCapability(receipt, 'verification');
668
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
669
+ scope: 'verification', operation: 'failed', error: detail
670
+ }, consumedCapabilities);
671
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
672
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed, verification.error);
673
+ }
674
+ }
675
+ } catch (error) {
676
+ const detail = errorOf(error, 'VERIFY_FAILED', true);
677
+ try {
678
+ const capability = issueCapability(receipt, 'verification');
679
+ receipt = applyFinalizationReceiptMutationUnderLock(repoRoot, receipt, capability, {
680
+ scope: 'verification', operation: 'failed', error: detail
681
+ }, consumedCapabilities);
682
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
683
+ } catch { /* preserve the primary error */ }
684
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification: { status: 'blocked', changed: false, error: detail } }, changed, detail);
685
+ }
686
+ receipt = reconcileWarningProjection(repoRoot, taskId, receipt, consumedCapabilities);
687
+ return terminalResult(taskId, receipt, { lifecycle: lifecycleResult, taskComment, verification }, changed);
688
+ }
689
+
690
+ function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): TaskFinalizationResult {
691
+ if (request.intent !== 'complete' || !request.taskRef || !request.agent) {
692
+ return failed(null, { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'complete finalization requires taskRef and agent', retryable: false });
693
+ }
694
+ const repoRoot = path.resolve(options.repoRoot);
695
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot });
696
+ if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
697
+ try {
698
+ return withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.complete', () => applyUnderLock(request, resolved.taskId, options));
699
+ } catch (error) {
700
+ const detail = error instanceof TaskExecutionLockError
701
+ ? { code: error.code, message: error.message, retryable: error.code === 'ORCHESTRATION_LOCK_BUSY' }
702
+ : errorOf(error, 'TASK_FINALIZATION_FAILED');
703
+ return failed(resolved.taskId, detail);
704
+ }
705
+ }
706
+
707
+ export { applyFinalizationReceiptMutation, applyTaskFinalization, issueCapability as createFinalizationCapability };
708
+ export type {
709
+ FinalizationError,
710
+ FinalizationCapability,
711
+ FinalizationMutation,
712
+ FinalizationWarning,
713
+ FinalizationStep,
714
+ TaskFinalizationOptions,
715
+ TaskFinalizationReceipt,
716
+ TaskFinalizationRequest,
717
+ TaskFinalizationResult,
718
+ TaskFinalizationStep
719
+ };