@fitlab-ai/agent-infra 0.11.4 → 0.11.5

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 (111) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/platform/pr-summary.js +4 -23
  15. package/dist/lib/platform/verification-sync.js +12 -49
  16. package/dist/lib/sandbox/control/client.js +4 -1
  17. package/dist/lib/sandbox/control/executor.js +12 -4
  18. package/dist/lib/sandbox/control/protocol.js +2 -1
  19. package/dist/lib/sandbox/removal.js +15 -8
  20. package/dist/lib/task/activity-intent.js +1 -2
  21. package/dist/lib/task/artifact-operations.js +36 -2
  22. package/dist/lib/task/artifact-schema.js +7 -7
  23. package/dist/lib/task/control-authority.js +9 -166
  24. package/dist/lib/task/decision-intents.js +2 -4
  25. package/dist/lib/task/events.js +7 -28
  26. package/dist/lib/task/finalization-handoff.js +92 -0
  27. package/dist/lib/task/finalization.js +123 -12
  28. package/dist/lib/task/intermediate-cleanup.js +9 -148
  29. package/dist/lib/task/ledger-intents.js +3 -6
  30. package/dist/lib/task/lifecycle.js +50 -11
  31. package/dist/lib/task/local-artifact-finalization.js +13 -28
  32. package/dist/lib/task/review-command.js +1 -2
  33. package/dist/lib/task/review-finalization.js +1 -2
  34. package/dist/lib/task/snapshot.js +6 -6
  35. package/dist/lib/task/verification-engine.js +16 -11
  36. package/dist/lib/task/verification.js +1 -13
  37. package/dist/lib/task/workflow-warning-intents.js +2 -4
  38. package/dist/lib/task/write.js +1 -2
  39. package/dist/lifecycle-build-manifest.json +3 -3
  40. package/lib/decide.ts +5 -35
  41. package/lib/git/branch-worktree.ts +41 -0
  42. package/lib/internal/cli-route-inventory.ts +0 -2
  43. package/lib/internal/task-activity.ts +6 -34
  44. package/lib/internal/task-event.ts +4 -37
  45. package/lib/internal/task-finalization.ts +9 -2
  46. package/lib/internal/task-ledger.ts +5 -38
  47. package/lib/internal/task-lifecycle.ts +2 -2
  48. package/lib/internal/task-operation-registry.ts +1 -4
  49. package/lib/internal/task-review.ts +5 -31
  50. package/lib/internal/task-verify.ts +3 -39
  51. package/lib/internal/task-warning.ts +3 -32
  52. package/lib/platform/pr-summary.ts +4 -25
  53. package/lib/platform/verification-sync.ts +13 -53
  54. package/lib/sandbox/control/client.ts +5 -1
  55. package/lib/sandbox/control/executor.ts +13 -4
  56. package/lib/sandbox/control/protocol.ts +3 -2
  57. package/lib/sandbox/removal.ts +23 -7
  58. package/lib/task/activity-intent.ts +1 -2
  59. package/lib/task/artifact-operations.ts +58 -2
  60. package/lib/task/artifact-schema.ts +9 -7
  61. package/lib/task/control-authority.ts +14 -188
  62. package/lib/task/decision-intents.ts +2 -4
  63. package/lib/task/events.ts +8 -27
  64. package/lib/task/finalization-handoff.ts +104 -0
  65. package/lib/task/finalization.ts +120 -12
  66. package/lib/task/intermediate-cleanup.ts +9 -125
  67. package/lib/task/ledger-intents.ts +3 -6
  68. package/lib/task/lifecycle.ts +45 -13
  69. package/lib/task/local-artifact-finalization.ts +16 -35
  70. package/lib/task/review-command.ts +2 -5
  71. package/lib/task/review-finalization.ts +1 -4
  72. package/lib/task/snapshot.ts +6 -6
  73. package/lib/task/verification-engine.ts +20 -10
  74. package/lib/task/verification.ts +1 -15
  75. package/lib/task/workflow-warning-intents.ts +2 -4
  76. package/lib/task/write.ts +1 -4
  77. package/package.json +1 -1
  78. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  79. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  82. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  83. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  84. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  85. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  86. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  87. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  88. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  89. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  90. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  91. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  92. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  93. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  94. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  95. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  96. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  97. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  99. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  100. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  101. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  102. package/dist/lib/internal/task-override.js +0 -138
  103. package/dist/lib/task/artifact-recovery.js +0 -476
  104. package/dist/lib/task/artifact-repair-intent.js +0 -181
  105. package/dist/lib/task/guard-override.js +0 -31
  106. package/dist/lib/task/human-override.js +0 -1259
  107. package/lib/internal/task-override.ts +0 -144
  108. package/lib/task/artifact-recovery.ts +0 -572
  109. package/lib/task/artifact-repair-intent.ts +0 -227
  110. package/lib/task/guard-override.ts +0 -50
  111. package/lib/task/human-override.ts +0 -1522
@@ -13,6 +13,7 @@ type ArtifactSchema = Readonly<{
13
13
  family: ArtifactSchemaFamily;
14
14
  title: Readonly<{ zh: string; en: string }>;
15
15
  sections: readonly ArtifactSection[];
16
+ stateCheckPatterns: readonly string[];
16
17
  requiredPatterns: readonly string[];
17
18
  }>;
18
19
  type ArtifactSkeletonInput = Readonly<{
@@ -106,21 +107,22 @@ function buildSections(family: ArtifactSchemaFamily): readonly ArtifactSection[]
106
107
  }));
107
108
  }
108
109
 
110
+ const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
111
+
109
112
  const ARTIFACT_SCHEMAS: readonly ArtifactSchema[] = [
110
- { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: ['^\\$ '] },
111
- { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
112
- { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: ['^\\$ '] },
113
- { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
114
- { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: ['^\\$ '] },
113
+ { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
114
+ { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
115
+ { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
116
+ { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
117
+ { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
115
118
  { family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
116
119
  '^### (?:审查决定|Approval Decision)$',
117
- '^\\$ ',
118
120
  '^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
119
121
  '^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
120
122
  '^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
121
123
  '^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
122
124
  '^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
123
- ] }
125
+ ], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
124
126
  ];
125
127
 
126
128
  function getArtifactSchema(family: string): ArtifactSchema | null {
@@ -1,12 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import crypto from 'node:crypto';
3
- import fs from 'node:fs';
4
3
 
5
4
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
6
5
  import { isAgentClientId } from '../agent-clients/types.ts';
7
6
  import type { AgentClientId } from '../agent-clients/types.ts';
8
7
  import { getAgentClientAdapter } from '../agent-clients/registry.ts';
9
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from './human-override.ts';
10
8
  import {
11
9
  activateMatchingOrchestrationDelegation,
12
10
  activateOrchestrationDelegation,
@@ -40,19 +38,12 @@ import type {
40
38
  AgentClientLifecycleRecoveryRequest as LifecycleRecoveryRequest,
41
39
  AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult
42
40
  } from '../agent-clients/adapter.ts';
43
- import { locateActivityLog } from './activity-log.ts';
44
41
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.ts';
45
42
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
46
43
  import { verifyTaskEvent } from './verification.ts';
47
44
  import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
48
- import {
49
- findArtifactRecoveryIntentsByOperation,
50
- writeArtifactRecoveryIntent,
51
- type ArtifactRecoveryIntent
52
- } from './artifact-repair-intent.ts';
53
45
  import {
54
46
  computeLifecycleBuildIdentity,
55
- verifyLifecycleBuildIdentity,
56
47
  type LifecycleBuildIdentity
57
48
  } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
58
49
 
@@ -324,7 +315,6 @@ export function consumeLifecycleRecoveryAttestation(
324
315
  export function queryLifecycleRecoveryOperation(
325
316
  operationId: string,
326
317
  options: Readonly<{
327
- repoRoot?: string;
328
318
  capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
329
319
  }> = {}
330
320
  ): LifecycleRecoveryOperationQueryV1 {
@@ -334,7 +324,6 @@ export function queryLifecycleRecoveryOperation(
334
324
  const store = options.capabilityStore ?? createCodexCapabilityStore();
335
325
  const registry = authorityOperationMap.get(store);
336
326
  const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
337
- const intents = options.repoRoot ? findArtifactRecoveryIntentsByOperation(options.repoRoot, operationId) : [];
338
327
  const capabilities = store.findByRecoveryOperation(operationId);
339
328
  const phaseMap = new Map<string, LifecycleRecoveryOperationQueryV1['phases'][number]>();
340
329
  for (const capability of capabilities) {
@@ -358,21 +347,10 @@ export function queryLifecycleRecoveryOperation(
358
347
  ? 'consumed' : 'issued'
359
348
  });
360
349
  }
361
- for (const intent of intents) {
362
- if (!intent.phase) continue;
363
- const key = `${intent.phase}\0${intent.requestId}`;
364
- if (phaseMap.has(key)) continue;
365
- phaseMap.set(key, {
366
- phase: intent.phase,
367
- requestId: intent.requestId,
368
- attestationId: null,
369
- state: intent.state === 'consumed' ? 'consumed' : 'observed'
370
- });
371
- }
372
350
  const capabilityState = capabilities.some((record) => record.recoveryState === 'consumed')
373
351
  ? 'consumed'
374
352
  : capabilities.some((record) => record.recoveryState === 'reserved') ? 'reserved' : 'unknown';
375
- const committed = capabilityState === 'consumed' || intents.some((intent) => intent.state === 'consumed');
353
+ const committed = capabilityState === 'consumed';
376
354
  return {
377
355
  version: 1,
378
356
  operationId,
@@ -382,133 +360,6 @@ export function queryLifecycleRecoveryOperation(
382
360
  };
383
361
  }
384
362
 
385
- type LifecycleRecoveryCompensationOptions = Readonly<{
386
- repoRoot: string;
387
- controllerBinding: TaskControlControllerBinding;
388
- capabilityStore?: ReturnType<typeof createCodexCapabilityStore>;
389
- buildIdentity?: LifecycleBuildIdentity;
390
- now?: () => number;
391
- }>;
392
-
393
- function lifecycleRecoveryCompensationError(code: string, message: string): Error {
394
- const error = new Error(`${code}: ${message}`);
395
- error.name = code;
396
- return error;
397
- }
398
-
399
- function lifecycleRecoveryEventCommitted(
400
- selector: LifecycleAuthorityRequestV1,
401
- repoRoot: string
402
- ): boolean {
403
- const resolved = resolveTaskRef(selector.taskId, { repoRoot });
404
- if (!resolved.ok || resolved.taskId !== selector.taskId) return false;
405
- let content: string;
406
- try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
407
- catch { return false; }
408
- const section = locateActivityLog(content);
409
- if (!section) return false;
410
- const labels = { analysis: 'Analyze Task', plan: 'Plan Task', code: 'Code Task' } as const;
411
- const prefix = `${labels[selector.family]} (Round ${selector.round}`;
412
- return section.entries.some((entry) => {
413
- if (entry.step.endsWith(' [started]') || !entry.step.startsWith(prefix)) return false;
414
- const delimiter = entry.step[prefix.length];
415
- return (delimiter === ',' || delimiter === ')') && entry.note.endsWith(`→ ${selector.artifact}`);
416
- });
417
- }
418
-
419
- function lifecycleRecoveryIntentMatches(
420
- intent: ArtifactRecoveryIntent,
421
- selector: LifecycleAuthorityRequestV1
422
- ): boolean {
423
- return intent.taskId === selector.taskId
424
- && intent.family === selector.family
425
- && intent.artifact === selector.artifact
426
- && (intent.state === 'commit-started' || intent.state === 'consumption-started' || intent.state === 'consumed');
427
- }
428
-
429
- /**
430
- * Completes only the durable tail of a committed task event after a process
431
- * restart. The task event itself is the commit boundary and is never replayed.
432
- */
433
- export function recoverLifecycleRecoveryOperation(
434
- input: LifecycleAuthorityRequestV1,
435
- options: LifecycleRecoveryCompensationOptions
436
- ): LifecycleRecoveryOperationQueryV1 {
437
- const selector = validateLifecycleAuthorityRequest(input);
438
- if (selector.phase !== 'task-event.completed') {
439
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_UNSUPPORTED', 'only task-event.completed supports durable compensation');
440
- }
441
- const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(options.repoRoot);
442
- if (selector.expectedBuildIdentityDigest !== lifecycleAuthorityBuildDigest(buildIdentity)) {
443
- throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the recovery request');
444
- }
445
- if (selector.expectedControlGeneration !== options.controllerBinding.controlGeneration
446
- || selector.expectedControllerInstanceDigest !== options.controllerBinding.instanceDigest) {
447
- throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the recovery request');
448
- }
449
- const store = options.capabilityStore ?? createCodexCapabilityStore();
450
- const expected = {
451
- taskId: selector.taskId,
452
- hookDefinitionHash: selector.expectedHookDefinitionHash,
453
- buildIdentity,
454
- controller: options.controllerBinding
455
- };
456
- const capabilities = store.findByRecoveryOperation(selector.operationId);
457
- if (capabilities.length !== 1 || capabilities[0]!.capabilityRefDigest !== lifecycleAuthorityDigest(selector.authorityRef)) {
458
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not identify this operation');
459
- }
460
- const capability = capabilities[0]!;
461
- const build = verifyLifecycleBuildIdentity(capability.buildIdentity, buildIdentity);
462
- const expiredReserved = capability.status === 'expired' && capability.recoveryState === 'reserved';
463
- if (!build.ok
464
- || capability.taskId !== selector.taskId
465
- || capability.hookDefinitionHash !== selector.expectedHookDefinitionHash
466
- || capability.controller?.controlGeneration !== options.controllerBinding.controlGeneration
467
- || capability.controller?.instanceDigest !== options.controllerBinding.instanceDigest
468
- || capability.recoveryState === 'unreserved'
469
- || capability.recoveryOperationId !== selector.operationId
470
- || (!['attested', 'consumed'].includes(capability.status) && !expiredReserved)) {
471
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not match the recovery request');
472
- }
473
- const phase = capability.recoveryPhases.find((entry) => entry.phase === selector.phase);
474
- if (!phase) {
475
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
476
- }
477
- const intents = findArtifactRecoveryIntentsByOperation(options.repoRoot, selector.operationId)
478
- .filter((intent) => intent.taskId === selector.taskId
479
- && intent.family === selector.family
480
- && intent.artifact === selector.artifact);
481
- if (intents.length !== 1) {
482
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not identify this task event');
483
- }
484
- const intent = intents[0]!;
485
- if (!lifecycleRecoveryIntentMatches(intent, selector)
486
- || intent.phase !== selector.phase
487
- || intent.requestId !== selector.lifecycleRequestId
488
- || intent.authorityDigest === null) {
489
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact recovery intent does not match the recovery request');
490
- }
491
- if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
492
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
493
- }
494
- if (capability.recoveryState === 'reserved') {
495
- if (phase.state === 'issued') {
496
- store.consumeRecoveryPhase(selector.authorityRef, selector.operationId, selector.phase, phase.requestId, expected, { allowExpiredReserved: true });
497
- }
498
- store.consumeReference(selector.authorityRef, selector.operationId, expected, { allowExpiredReserved: true });
499
- } else if (phase.state !== 'consumed') {
500
- throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
501
- }
502
- if (intent.state !== 'consumed') {
503
- writeArtifactRecoveryIntent(options.repoRoot, {
504
- ...intent,
505
- state: 'consumed',
506
- updatedAt: (options.now ?? Date.now)()
507
- }, { expected: intent });
508
- }
509
- return queryLifecycleRecoveryOperation(selector.operationId, { repoRoot: options.repoRoot, capabilityStore: store });
510
- }
511
-
512
363
  function authorityRejected(request: LifecycleAuthorityRequestV1, code: string, message: string): LifecycleAuthorityResponseV1 {
513
364
  return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'rejected', attestation: null, error: { code, message } };
514
365
  }
@@ -537,6 +388,7 @@ export type TaskControlExecutionContext =
537
388
  generation: string;
538
389
  manifestPath: string;
539
390
  requestId: string;
391
+ handoffDirectory: string;
540
392
  controllerBinding?: TaskControlControllerBinding | null;
541
393
  lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
542
394
  }>;
@@ -575,11 +427,7 @@ export function taskControlOperationKey(operation: TaskControlOperation): string
575
427
  : operation.intent;
576
428
  }
577
429
 
578
- export type TaskLifecycleControlRequest = TaskLifecycleRequest & Readonly<{
579
- overrideTicket?: string;
580
- overrideTarget?: string;
581
- overrideScope?: string;
582
- }>;
430
+ export type TaskLifecycleControlRequest = TaskLifecycleRequest;
583
431
 
584
432
  function invalidContext(message: string): never {
585
433
  throw new Error(`TASK_CONTROL_CONTEXT_INVALID: ${message}`);
@@ -625,6 +473,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
625
473
  generation: string;
626
474
  manifestPath: string;
627
475
  requestId: string;
476
+ handoffDirectory?: string;
628
477
  controllerBinding?: TaskControlControllerBinding | null;
629
478
  lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
630
479
  }>): TaskControlExecutionContext {
@@ -645,6 +494,7 @@ export function createSandboxExecutorExecutionContext(params: Readonly<{
645
494
  generation: params.generation,
646
495
  manifestPath: absolute('manifestPath', params.manifestPath),
647
496
  requestId: params.requestId,
497
+ handoffDirectory: absolute('handoffDirectory', params.handoffDirectory ?? path.join(params.worktreeRoot, '.agent-infra-handoff')),
648
498
  ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
649
499
  ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
650
500
  lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
@@ -670,6 +520,7 @@ export function assertTaskControlExecutionContext(context: TaskControlExecutionC
670
520
  absolute('worktreeRoot', context.worktreeRoot);
671
521
  absolute('runtimeDir', context.runtimeDir);
672
522
  absolute('manifestPath', context.manifestPath);
523
+ absolute('handoffDirectory', context.handoffDirectory);
673
524
  if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
674
525
  invalidContext('runtimeDir is not bound to the manifest control root');
675
526
  }
@@ -706,12 +557,8 @@ function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string)
706
557
  async function applyLifecycleWithAuthority(
707
558
  context: TaskControlExecutionContext,
708
559
  request: TaskLifecycleControlRequest
709
- ): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> {
710
- const conflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
711
- if (conflict) {
712
- return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
713
- }
714
- const execute = async (): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }> => {
560
+ ): Promise<TaskLifecycleResult | LifecycleRecoveryResult> {
561
+ const execute = async (): Promise<TaskLifecycleResult | LifecycleRecoveryResult> => {
715
562
  if (request.intent === 'recover-started') {
716
563
  const adapter = isAgentClientId(request.agent)
717
564
  ? getAgentClientAdapter(request.agent).orchestrationAdapter
@@ -725,27 +572,7 @@ async function applyLifecycleWithAuthority(
725
572
  }
726
573
  return adapter.recoverStarted(request as LifecycleRecoveryRequest, { repoRoot: context.repoRoot });
727
574
  }
728
- const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
729
- if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
730
- if (!request.overrideTarget || !request.overrideScope) {
731
- return {
732
- ...lifecycleResult,
733
- error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
734
- };
735
- }
736
- const override = await consumeHumanOverride({
737
- taskRef: request.taskRef,
738
- ticketId: request.overrideTicket,
739
- failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
740
- target: request.overrideTarget,
741
- scope: request.overrideScope,
742
- intent: request.intent,
743
- ...('alertNumber' in request && request.alertNumber ? { alertNumber: request.alertNumber } : {}),
744
- ...('issueNumber' in request && request.issueNumber ? { issueNumber: request.issueNumber } : {}),
745
- ...('stagingDir' in request && request.stagingDir ? { stagingDir: request.stagingDir } : {})
746
- }, { repoRoot: context.repoRoot });
747
- if (override.status === 'failed') return { ...lifecycleResult, humanOverride: override };
748
- return { ...lifecycleResult, ...override, humanOverride: override, error: null };
575
+ return applyTaskLifecycle(request, { repoRoot: context.repoRoot });
749
576
  };
750
577
  const taskId = lifecycleTaskId(request, context.repoRoot);
751
578
  if (!taskId) return await execute();
@@ -840,7 +667,7 @@ function orchestrationFailure(error: unknown): OrchestrationResult {
840
667
  export function dispatchTaskControlOperation(
841
668
  context: TaskControlExecutionContext,
842
669
  operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
843
- ): Promise<(TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown }>;
670
+ ): Promise<TaskLifecycleResult | LifecycleRecoveryResult>;
844
671
  export function dispatchTaskControlOperation(
845
672
  context: TaskControlExecutionContext,
846
673
  operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
@@ -865,7 +692,9 @@ export function dispatchTaskControlOperation(
865
692
  return finalize(operation.request, {
866
693
  repoRoot: context.repoRoot,
867
694
  ...(context.source === 'sandbox-executor' ? {
868
- controlBinding: { generation: context.generation, requestId: context.requestId }
695
+ controlBinding: { generation: context.generation, requestId: context.requestId },
696
+ handoffDirectory: context.handoffDirectory,
697
+ manifestPath: context.manifestPath
869
698
  } : {}),
870
699
  preflight: (request, options) => verifyTaskEvent(
871
700
  { ...request, event: 'complete-task.hard-preflight' }, options
@@ -904,7 +733,7 @@ function operationInvalid(message: string): never {
904
733
 
905
734
  const LIFECYCLE_FLAGS = new Set([
906
735
  '--agent', '--reason', '--unblock-condition', '--note', '--alert-number', '--staging-dir', '--issue-number',
907
- '--auto', '--override-ticket', '--override-target', '--override-scope', '--dry-run'
736
+ '--auto', '--dry-run'
908
737
  ]);
909
738
 
910
739
  const FINALIZATION_FLAGS = new Set(['--agent']);
@@ -979,9 +808,6 @@ export function parseTaskControlOperation(
979
808
  ...(value(values, '--unblock-condition') ? { unblockCondition: value(values, '--unblock-condition') } : {}),
980
809
  ...(value(values, '--note') ? { note: value(values, '--note') } : {}),
981
810
  ...(value(values, '--staging-dir') ? { stagingDir: value(values, '--staging-dir') } : {}),
982
- ...(value(values, '--override-ticket') ? { overrideTicket: value(values, '--override-ticket') } : {}),
983
- ...(value(values, '--override-target') ? { overrideTarget: value(values, '--override-target') } : {}),
984
- ...(value(values, '--override-scope') ? { overrideScope: value(values, '--override-scope') } : {}),
985
811
  ...(value(values, '--alert-number') ? { alertNumber: Number(value(values, '--alert-number')) } : {}),
986
812
  ...(value(values, '--issue-number') ? { issueNumber: Number(value(values, '--issue-number')) } : {}),
987
813
  ...(values['--auto'] === true ? { auto: true } : {}),
@@ -16,7 +16,6 @@ import { resolveTaskContext } from './resolve-ref.ts';
16
16
  import { extractSection, extractSubSection, findSectionHeading } from './sections.ts';
17
17
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
18
18
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
19
- import { allowsManualOverride } from './guard-override.ts';
20
19
  import { consumeHumanDecisionReworkIntents, parseReworkIntentDocument, reworkIntentMutation } from './rework-intent.ts';
21
20
 
22
21
  type HumanDecisionRequest = {
@@ -87,8 +86,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
87
86
  if (!request.selector || !request.decision.trim()) return failed('DECISION_PAYLOAD_INVALID', 'selector and decision are required');
88
87
  const resolved = resolveTaskContext(request.taskRef, { repoRoot: options.repoRoot });
89
88
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
90
- const stateOverride = allowsManualOverride(options.manualOverride, 'decision-intent', 'TASK_STATE_MISMATCH');
91
- if (resolved.state !== 'active' && !stateOverride) return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
89
+ if (resolved.state !== 'active') return failed('TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
92
90
  let content: string;
93
91
  let rows: LedgerRow[];
94
92
  try {
@@ -186,7 +184,7 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
186
184
  } catch (error) {
187
185
  return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
188
186
  }
189
- const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
187
+ const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: 'active', mutations, dryRun: request.dryRun }, {
190
188
  ...options,
191
189
  taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
192
190
  metadataProvider: () => metadata
@@ -2,6 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
 
5
+ import { resolveBranchWorktree } from '../git/branch-worktree.ts';
6
+
5
7
  import { appendActivityEntry, locateActivityLog, pairEntries, startedBackedRows } from './activity-log.ts';
6
8
  import {
7
9
  buildArtifactLinkSection,
@@ -32,7 +34,6 @@ import type { OrchestrationStageCompletion } from './orchestration.ts';
32
34
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
33
35
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
34
36
  import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
35
- import { allowsManualOverride } from './guard-override.ts';
36
37
  import { validateLocalArtifact } from './local-artifact-finalization.ts';
37
38
  import type { LocalArtifactFamily } from './local-artifact-finalization.ts';
38
39
  import { buildLifecycleFacts, canStart, effectiveReworkTarget } from './capabilities.ts';
@@ -81,7 +82,6 @@ type TaskEventErrorCode =
81
82
  | ArtifactErrorCode | TaskWriteErrorCode;
82
83
  type TaskEventRequest = {
83
84
  taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
84
- overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
85
85
  initiator?: TriggerInitiator; requestId?: string; reasonCode?: TriggerReason;
86
86
  sourceFinding?: string; sourceArtifact?: string; sourceSha256?: string;
87
87
  round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
@@ -111,22 +111,7 @@ type TaskEventResult = {
111
111
 
112
112
  function branchWorktree(repoRoot: string, branch: string): string | null {
113
113
  if (!branch) return repoRoot;
114
- try {
115
- const records = execFileSync('git', ['worktree', 'list', '--porcelain'], {
116
- cwd: repoRoot,
117
- encoding: 'utf8',
118
- stdio: ['ignore', 'pipe', 'pipe']
119
- }).trim().split(/\r?\n\r?\n/);
120
- const expectedRef = `refs/heads/${branch}`;
121
- for (const record of records) {
122
- const worktree = /^worktree (.+)$/m.exec(record)?.[1];
123
- const branchRef = /^branch (.+)$/m.exec(record)?.[1];
124
- if (worktree && branchRef === expectedRef) return worktree;
125
- }
126
- } catch {
127
- // A missing worktree must not anchor a review to an unrelated repository.
128
- }
129
- return null;
114
+ return resolveBranchWorktree(repoRoot, branch);
130
115
  }
131
116
 
132
117
  function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | undefined, repoRoot: string, branch: string): string | null {
@@ -148,7 +133,7 @@ function approvedCleanReviewedCommit(reviewContent: string, verdict: Verdict | u
148
133
  }
149
134
  }
150
135
 
151
- const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
136
+ const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'initiator', 'requestId', 'reasonCode', 'sourceFinding', 'sourceArtifact', 'sourceSha256']);
152
137
  const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
153
138
  'analyze.started': { optional: ['round'] },
154
139
  'analyze.awaiting-input': { required: ['question'] },
@@ -758,8 +743,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
758
743
  if (invalid) return failed(request, invalid);
759
744
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
760
745
  if (!resolved.ok) return failed(request, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
761
- const stateOverride = allowsManualOverride(options.manualOverride, 'task-event', 'TASK_STATE_MISMATCH');
762
- if (resolved.state !== 'active' && !stateOverride) return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
746
+ if (resolved.state !== 'active') return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
763
747
  let content: string;
764
748
  try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
765
749
  catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
@@ -851,7 +835,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
851
835
  }
852
836
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, row.started, frontmatter, artifactContext);
853
837
  }
854
- if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0 && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires a started event or an earlier completed attempt' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
838
+ if (eventIdentity.phase === 'completed' && !row?.started && completedRows.length === 0) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires a started event or an earlier completed attempt' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
855
839
  if (eventIdentity.phase === 'completed') {
856
840
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
857
841
  if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
@@ -918,12 +902,9 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
918
902
  const facts = buildLifecycleFacts(resolved.taskDir, content, resolved.state);
919
903
  if (!facts.ok) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${facts.code}: ${facts.message}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
920
904
  const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
921
- const capabilityOverride = capability.reasonCode === 'TASK_NOT_ACTIVE'
922
- ? stateOverride
923
- : allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID');
924
905
  const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
925
906
  || capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
926
- if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure) && !capabilityOverride) {
907
+ if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)) {
927
908
  const code = capability.reasonCode === 'INVALIDATION_INCOMPLETE' ? 'TASK_INVALIDATION_BLOCKED' : 'EVENT_TRANSITION_INVALID';
928
909
  return failed(normalized, { code, message: `${capability.reasonCode}: ${capability.evidence.join(', ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
929
910
  }
@@ -1078,7 +1059,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
1078
1059
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
1079
1060
  const sourceCompletion = eventIdentity.phase === 'completed'
1080
1061
  && ['analyze', 'plan', 'code'].includes(eventIdentity.family);
1081
- const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
1062
+ const result = writeTask({ taskRef: normalized.taskRef, expectedState: 'active', dryRun: normalized.dryRun, mutations }, { ...options, invalidationContext: sourceCompletion ? 'source-completion' : 'standard', metadataProvider: () => metadata });
1082
1063
  if (result.status === 'failed') return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
1083
1064
  if (!normalized.dryRun && orchestrationCompletion) {
1084
1065
  try {
@@ -0,0 +1,104 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import type { TaskFinalizationReceipt } from './finalization.ts';
6
+
7
+ const HANDOFF_VERSION = 1 as const;
8
+
9
+ type TaskFinalizationHandoff = Readonly<{
10
+ version: typeof HANDOFF_VERSION;
11
+ taskId: string;
12
+ binding: Readonly<{ generation: string; requestId: string }>;
13
+ receipt: TaskFinalizationReceipt;
14
+ receiptSha256: string;
15
+ }>;
16
+ type ReadHandoff = Readonly<{ receipt: TaskFinalizationReceipt; cleanup: () => void }>;
17
+ const MAX_HANDOFF_BYTES = 1024 * 1024;
18
+
19
+ function digest(value: unknown): string {
20
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
21
+ }
22
+
23
+ export function finalizationHandoffPath(directory: string, taskId: string): string {
24
+ return path.join(directory, '.agent-infra', 'task-finalization', `${taskId}.json`);
25
+ }
26
+
27
+ export function publishTaskFinalizationHandoff(
28
+ directory: string,
29
+ receipt: TaskFinalizationReceipt,
30
+ binding: Readonly<{ generation: string; requestId: string }>
31
+ ): string {
32
+ if (receipt.taskId === '' || receipt.controlBinding?.generation !== binding.generation
33
+ || receipt.controlBinding.requestId !== binding.requestId) {
34
+ throw new Error('TASK_FINALIZATION_HANDOFF_BINDING_INVALID');
35
+ }
36
+ const envelope: TaskFinalizationHandoff = {
37
+ version: HANDOFF_VERSION, taskId: receipt.taskId, binding, receipt, receiptSha256: digest(receipt)
38
+ };
39
+ const target = finalizationHandoffPath(directory, receipt.taskId);
40
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
41
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
42
+ fs.writeFileSync(temporary, `${JSON.stringify(envelope)}\n`, { mode: 0o600, flag: 'wx' });
43
+ try { fs.renameSync(temporary, target); } catch (error) {
44
+ try { fs.unlinkSync(temporary); } catch { /* preserve the primary error */ }
45
+ throw error;
46
+ }
47
+ return digest(envelope);
48
+ }
49
+
50
+ export function readTaskFinalizationHandoff(
51
+ directory: string,
52
+ taskId: string,
53
+ binding: Readonly<{ generation: string; requestId: string }>,
54
+ handoffSha256: string
55
+ ): ReadHandoff {
56
+ const file = finalizationHandoffPath(directory, taskId);
57
+ const initial = fs.lstatSync(file);
58
+ if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_HANDOFF_BYTES) {
59
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
60
+ }
61
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
62
+ let content: string;
63
+ try {
64
+ const opened = fs.fstatSync(descriptor);
65
+ if (!opened.isFile() || opened.dev !== initial.dev || opened.ino !== initial.ino
66
+ || opened.size > MAX_HANDOFF_BYTES) throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
67
+ const bytes = Buffer.alloc(MAX_HANDOFF_BYTES + 1);
68
+ const length = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
69
+ if (length !== opened.size || length > MAX_HANDOFF_BYTES) throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
70
+ content = bytes.toString('utf8', 0, length);
71
+ const after = fs.fstatSync(descriptor);
72
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
73
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
74
+ }
75
+ } finally {
76
+ fs.closeSync(descriptor);
77
+ }
78
+ const envelope = JSON.parse(content) as Partial<TaskFinalizationHandoff>;
79
+ if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
80
+ || Object.keys(envelope).sort().join('\0') !== ['binding', 'receipt', 'receiptSha256', 'taskId', 'version'].join('\0')
81
+ || envelope.version !== HANDOFF_VERSION || envelope.taskId !== taskId
82
+ || !envelope.binding || typeof envelope.binding !== 'object' || Array.isArray(envelope.binding)
83
+ || Object.keys(envelope.binding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
84
+ || envelope.binding.generation !== binding.generation
85
+ || envelope.binding.requestId !== binding.requestId || !envelope.receipt
86
+ || envelope.receipt.controlBinding?.generation !== binding.generation
87
+ || envelope.receipt.controlBinding.requestId !== binding.requestId
88
+ || typeof envelope.receiptSha256 !== 'string' || envelope.receiptSha256 !== digest(envelope.receipt)
89
+ || digest(envelope) !== handoffSha256) {
90
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
91
+ }
92
+ return {
93
+ receipt: envelope.receipt,
94
+ cleanup: () => {
95
+ try {
96
+ const current = fs.lstatSync(file);
97
+ if (current.isFile() && !current.isSymbolicLink()
98
+ && current.dev === initial.dev && current.ino === initial.ino) fs.unlinkSync(file);
99
+ } catch (error) {
100
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
101
+ }
102
+ }
103
+ };
104
+ }