@fitlab-ai/agent-infra 0.11.4 → 0.11.6

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 (149) 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/merge.js +62 -10
  15. package/dist/lib/platform/github-provider.js +9 -14
  16. package/dist/lib/platform/github-release-notes.js +80 -11
  17. package/dist/lib/platform/none-provider.js +1 -1
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/provider-contract.d.ts +39 -16
  20. package/dist/lib/platform/provider-contract.js +1 -1
  21. package/dist/lib/platform/provider-validation.js +83 -13
  22. package/dist/lib/platform/release-notes.js +7 -8
  23. package/dist/lib/platform/verification-sync.js +12 -49
  24. package/dist/lib/sandbox/capture.js +1 -1
  25. package/dist/lib/sandbox/commands/create.js +46 -1
  26. package/dist/lib/sandbox/commands/enter.js +1 -1
  27. package/dist/lib/sandbox/commands/start.js +1 -1
  28. package/dist/lib/sandbox/config.js +10 -0
  29. package/dist/lib/sandbox/control/client.js +4 -1
  30. package/dist/lib/sandbox/control/executor.js +12 -4
  31. package/dist/lib/sandbox/control/lifecycle.js +18 -13
  32. package/dist/lib/sandbox/control/protocol.js +2 -1
  33. package/dist/lib/sandbox/removal.js +15 -8
  34. package/dist/lib/sandbox/task-resolver.js +62 -2
  35. package/dist/lib/task/activity-intent.js +1 -2
  36. package/dist/lib/task/artifact-operations.js +36 -2
  37. package/dist/lib/task/artifact-schema.js +7 -7
  38. package/dist/lib/task/control-authority.js +9 -166
  39. package/dist/lib/task/decision-intents.js +2 -4
  40. package/dist/lib/task/events.js +7 -28
  41. package/dist/lib/task/finalization-handoff.js +92 -0
  42. package/dist/lib/task/finalization.js +123 -12
  43. package/dist/lib/task/intermediate-cleanup.js +9 -148
  44. package/dist/lib/task/ledger-intents.js +3 -6
  45. package/dist/lib/task/lifecycle.js +50 -11
  46. package/dist/lib/task/local-artifact-finalization.js +13 -28
  47. package/dist/lib/task/resolve-ref.js +3 -3
  48. package/dist/lib/task/review-command.js +1 -2
  49. package/dist/lib/task/review-finalization.js +1 -2
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +16 -11
  52. package/dist/lib/task/verification.js +1 -13
  53. package/dist/lib/task/workflow-warning-intents.js +2 -4
  54. package/dist/lib/task/write.js +122 -3
  55. package/dist/lifecycle-build-manifest.json +3 -3
  56. package/lib/decide.ts +5 -35
  57. package/lib/git/branch-worktree.ts +41 -0
  58. package/lib/internal/cli-route-inventory.ts +0 -2
  59. package/lib/internal/task-activity.ts +6 -34
  60. package/lib/internal/task-event.ts +4 -37
  61. package/lib/internal/task-finalization.ts +9 -2
  62. package/lib/internal/task-ledger.ts +5 -38
  63. package/lib/internal/task-lifecycle.ts +2 -2
  64. package/lib/internal/task-operation-registry.ts +1 -4
  65. package/lib/internal/task-review.ts +5 -31
  66. package/lib/internal/task-verify.ts +3 -39
  67. package/lib/internal/task-warning.ts +3 -32
  68. package/lib/merge.ts +59 -10
  69. package/lib/platform/github-provider.ts +11 -18
  70. package/lib/platform/github-release-notes.ts +89 -14
  71. package/lib/platform/none-provider.ts +1 -1
  72. package/lib/platform/pr-summary.ts +4 -25
  73. package/lib/platform/provider-contract.ts +38 -12
  74. package/lib/platform/provider-validation.ts +83 -12
  75. package/lib/platform/release-notes.ts +11 -12
  76. package/lib/platform/verification-sync.ts +13 -53
  77. package/lib/sandbox/capture.ts +1 -1
  78. package/lib/sandbox/commands/create.ts +52 -1
  79. package/lib/sandbox/commands/enter.ts +1 -1
  80. package/lib/sandbox/commands/start.ts +1 -1
  81. package/lib/sandbox/config.ts +12 -0
  82. package/lib/sandbox/control/client.ts +5 -1
  83. package/lib/sandbox/control/executor.ts +13 -4
  84. package/lib/sandbox/control/lifecycle.ts +22 -13
  85. package/lib/sandbox/control/protocol.ts +3 -2
  86. package/lib/sandbox/removal.ts +23 -7
  87. package/lib/sandbox/task-resolver.ts +51 -2
  88. package/lib/task/activity-intent.ts +1 -2
  89. package/lib/task/artifact-operations.ts +58 -2
  90. package/lib/task/artifact-schema.ts +9 -7
  91. package/lib/task/control-authority.ts +14 -188
  92. package/lib/task/decision-intents.ts +2 -4
  93. package/lib/task/events.ts +8 -27
  94. package/lib/task/finalization-handoff.ts +104 -0
  95. package/lib/task/finalization.ts +120 -12
  96. package/lib/task/intermediate-cleanup.ts +9 -125
  97. package/lib/task/ledger-intents.ts +3 -6
  98. package/lib/task/lifecycle.ts +45 -13
  99. package/lib/task/local-artifact-finalization.ts +16 -35
  100. package/lib/task/resolve-ref.ts +3 -3
  101. package/lib/task/review-command.ts +2 -5
  102. package/lib/task/review-finalization.ts +1 -4
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +20 -10
  105. package/lib/task/verification.ts +1 -15
  106. package/lib/task/workflow-warning-intents.ts +2 -4
  107. package/lib/task/write.ts +116 -5
  108. package/package.json +1 -1
  109. package/templates/.agents/rules/evidence-reporting.en.md +1 -2
  110. package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  113. package/templates/.agents/skills/archive-tasks/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
  116. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  117. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  119. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  120. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  121. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  122. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -11
  125. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
  126. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  127. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  128. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  129. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  130. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  131. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  132. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  133. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  134. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  137. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  138. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  139. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  140. package/dist/lib/internal/task-override.js +0 -138
  141. package/dist/lib/task/artifact-recovery.js +0 -476
  142. package/dist/lib/task/artifact-repair-intent.js +0 -181
  143. package/dist/lib/task/guard-override.js +0 -31
  144. package/dist/lib/task/human-override.js +0 -1259
  145. package/lib/internal/task-override.ts +0 -144
  146. package/lib/task/artifact-recovery.ts +0 -572
  147. package/lib/task/artifact-repair-intent.ts +0 -227
  148. package/lib/task/guard-override.ts +0 -50
  149. package/lib/task/human-override.ts +0 -1522
@@ -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
+ }
@@ -10,6 +10,7 @@ import { taskIssueIdentity } from '../platform/task-identities.ts';
10
10
  import { parseTaskFrontmatter } from './frontmatter.ts';
11
11
  import {
12
12
  applyTaskLifecycle,
13
+ inspectTaskLifecycleProgress,
13
14
  type TaskLifecycleOptions,
14
15
  type TaskLifecycleRequest,
15
16
  type TaskLifecycleResult
@@ -25,6 +26,9 @@ import {
25
26
  type OperationWarning,
26
27
  type OperationWarningSeverity
27
28
  } from './operation-outcome.ts';
29
+ import { readTaskFinalizationHandoff } from './finalization-handoff.ts';
30
+ import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
31
+ import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.ts';
28
32
 
29
33
  const RECEIPT_VERSION = 4 as const;
30
34
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'] as const;
@@ -55,11 +59,14 @@ type TaskFinalizationRequest = Readonly<{
55
59
  taskRef: string;
56
60
  intent: 'complete';
57
61
  agent: string;
62
+ handoffSha256?: string;
58
63
  }>;
59
64
 
60
65
  type TaskFinalizationOptions = Readonly<{
61
66
  repoRoot: string;
62
67
  controlBinding?: Readonly<{ generation: string; requestId: string }>;
68
+ handoffDirectory?: string;
69
+ manifestPath?: string;
63
70
  metadataProvider?: TaskLifecycleOptions['metadataProvider'];
64
71
  lifecycle?: typeof applyTaskLifecycle;
65
72
  backfill?: typeof backfillCompletionComments;
@@ -198,20 +205,32 @@ function validateReceipt(value: unknown, taskId: string): TaskFinalizationReceip
198
205
  const receipt = value as Record<string, unknown>;
199
206
  const controlBinding = receipt.controlBinding as Record<string, unknown> | null | undefined;
200
207
  const states = ['pending', 'done', 'skipped'];
208
+ const requiredKeys = [
209
+ 'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
210
+ 'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
211
+ ];
212
+ if (receipt.controlBinding !== undefined) requiredKeys.push('controlBinding');
201
213
  if (
214
+ Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
215
+ ||
202
216
  receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
203
217
  || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
204
218
  || !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
205
- || !states.includes(String(receipt.lifecycle))
206
- || !states.includes(String(receipt.taskComment))
207
- || !states.includes(String(receipt.verification))
208
- || !states.includes(String(receipt.summary))
209
- || !['pending', 'done'].includes(String(receipt.warningProjection))
219
+ || !states.includes(receipt.lifecycle as string)
220
+ || !states.includes(receipt.taskComment as string)
221
+ || !states.includes(receipt.verification as string)
222
+ || !states.includes(receipt.summary as string)
223
+ || !['pending', 'done'].includes(receipt.warningProjection as string)
210
224
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
211
- || typeof receipt.updatedAt !== 'string'
212
- || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
225
+ || typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
226
+ || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
227
+ || Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
228
+ || typeof (receipt.lastError as Record<string, unknown>).code !== 'string'
229
+ || typeof (receipt.lastError as Record<string, unknown>).message !== 'string'
230
+ || typeof (receipt.lastError as Record<string, unknown>).retryable !== 'boolean'))
213
231
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
214
232
  || Array.isArray(controlBinding)
233
+ || Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
215
234
  || typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
216
235
  || typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)))
217
236
  ) throw new Error('receipt schema is invalid');
@@ -243,6 +262,14 @@ function writeReceipt(repoRoot: string, receipt: TaskFinalizationReceipt): void
243
262
  }
244
263
  }
245
264
 
265
+ export function bindTaskFinalizationReceipt(
266
+ repoRoot: string, taskId: string, controlBinding: Readonly<{ generation: string; requestId: string }>
267
+ ): TaskFinalizationReceipt {
268
+ const receipt = readReceipt(path.resolve(repoRoot), taskId);
269
+ if (!receipt) throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
270
+ return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
271
+ }
272
+
246
273
  function updateReceipt(
247
274
  repoRoot: string,
248
275
  receipt: TaskFinalizationReceipt,
@@ -798,10 +825,16 @@ async function prepareUnderLock(
798
825
  } catch (error) {
799
826
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
800
827
  }
801
- if (options.controlBinding && (
802
- receipt.controlBinding?.generation !== options.controlBinding.generation
803
- || receipt.controlBinding.requestId !== options.controlBinding.requestId
804
- )) receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
828
+ if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
829
+ || receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
830
+ const state = resolveTaskRef(taskId, { repoRoot });
831
+ if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
832
+ return failed(taskId, {
833
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
834
+ message: 'canonical receipt is not bound to the current sandbox request', retryable: false
835
+ });
836
+ }
837
+ }
805
838
 
806
839
  const preflightState = resolveTaskRef(taskId, { repoRoot });
807
840
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
@@ -1043,13 +1076,88 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
1043
1076
  if (!resolved.ok) return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
1044
1077
  try {
1045
1078
  return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
1046
- const receipt = readReceipt(repoRoot, resolved.taskId);
1079
+ if (options.controlBinding && options.manifestPath) {
1080
+ try {
1081
+ const manifest = readSandboxControlManifest(options.manifestPath);
1082
+ const identity = validateSandboxControlIdentity({
1083
+ publicStatusDir: manifest.publicStatusDir,
1084
+ root: path.dirname(path.resolve(options.manifestPath)),
1085
+ mode: manifest.mode,
1086
+ taskId: manifest.taskId,
1087
+ generation: manifest.generation,
1088
+ controlRootId: manifest.controlRootId
1089
+ });
1090
+ if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
1091
+ || manifest.generation !== options.controlBinding.generation
1092
+ || fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
1093
+ throw new Error('current sandbox manifest does not match finalization request');
1094
+ }
1095
+ } catch (error) {
1096
+ return failed(resolved.taskId, {
1097
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
1098
+ message: error instanceof Error ? error.message : String(error), retryable: false
1099
+ });
1100
+ }
1101
+ }
1102
+ let receipt = readReceipt(repoRoot, resolved.taskId);
1103
+ if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
1104
+ try {
1105
+ const handoff = readTaskFinalizationHandoff(
1106
+ options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
1107
+ );
1108
+ receipt = validateReceipt(handoff.receipt, resolved.taskId);
1109
+ if (receipt.lifecycle !== 'pending') throw new Error('handoff lifecycle must be pending');
1110
+ if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
1111
+ throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
1112
+ }
1113
+ writeReceipt(repoRoot, receipt);
1114
+ try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
1115
+ } catch (error) {
1116
+ return failed(resolved.taskId, {
1117
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
1118
+ message: error instanceof Error ? error.message : String(error), retryable: false
1119
+ });
1120
+ }
1121
+ }
1047
1122
  if (!receipt) return failed(resolved.taskId, {
1048
1123
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
1049
1124
  });
1050
1125
  if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
1051
1126
  return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
1052
1127
  }
1128
+ if (options.controlBinding) {
1129
+ const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
1130
+ const oldBinding = receipt.controlBinding;
1131
+ if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
1132
+ return failed(resolved.taskId, {
1133
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
1134
+ message: 'canonical receipt has no valid host recovery proof', retryable: false
1135
+ });
1136
+ }
1137
+ if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
1138
+ if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
1139
+ return failed(resolved.taskId, {
1140
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
1141
+ message: 'pending receipt cannot be rebound without a current handoff', retryable: false
1142
+ });
1143
+ }
1144
+ try {
1145
+ const handoff = readTaskFinalizationHandoff(
1146
+ options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256
1147
+ );
1148
+ const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
1149
+ if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
1150
+ || nextReceipt.lifecycle !== 'pending') throw new Error('handoff is not continuous with canonical receipt');
1151
+ receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
1152
+ try { handoff.cleanup(); } catch { /* keep the persisted canonical receipt usable */ }
1153
+ } catch (error) {
1154
+ return failed(resolved.taskId, {
1155
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
1156
+ message: error instanceof Error ? error.message : String(error), retryable: false
1157
+ });
1158
+ }
1159
+ }
1160
+ }
1053
1161
  if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
1054
1162
  return failed(resolved.taskId, {
1055
1163
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
@@ -14,26 +14,14 @@ import {
14
14
  readTaskFinalizationReceipt,
15
15
  type TaskFinalizationReceipt
16
16
  } from './finalization.ts';
17
- import {
18
- semanticDigest,
19
- sha256Content
20
- } from './local-artifact-finalization.ts';
21
- import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
22
- import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
23
- import { parseArtifactName } from './artifact-name.ts';
24
- import type { ArtifactSchemaFamily } from './artifact-schema.ts';
25
17
  import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
26
18
  import type { TaskControlBindingEvidence } from './finalization-state.ts';
27
19
 
28
20
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
29
- const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
30
21
  const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
31
- const AUXILIARY_ROOTS = [
32
- '.local-artifact-finalization-intents',
33
- '.task-commit-intents'
34
- ] as const;
22
+ const AUXILIARY_ROOTS = ['.task-commit-intents'] as const;
35
23
 
36
- type CleanupKind = 'LFAI-CONSUMED' | 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
24
+ type CleanupKind = 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
37
25
  type CleanupDisposition = 'planned' | 'protected' | 'skipped' | 'deleted' | 'failed';
38
26
  type FileIdentity = Readonly<{ dev: string; ino: string; size: number; mtimeMs: number }>;
39
27
 
@@ -153,47 +141,6 @@ function receiptGate(
153
141
  return null;
154
142
  }
155
143
 
156
- function canonicalLocalIntentPath(taskId: string, family: string, artifact: string): string {
157
- return `${taskId}-${family}-${artifact}.json`;
158
- }
159
-
160
- function localIntentCandidate(
161
- repoRoot: string,
162
- task: TaskRecord | undefined,
163
- filePath: string,
164
- taskId: string,
165
- family: ArtifactSchemaFamily,
166
- artifact: string,
167
- identity: FileIdentity,
168
- options: ScanOptions
169
- ): IntermediateCleanupItem {
170
- const protectedReason = taskGate(task);
171
- if (protectedReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
172
- if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
173
- || !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
174
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
175
- }
176
- let intent: ArtifactRecoveryIntent | null;
177
- try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
178
- catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
179
- if (!intent || intent.state !== 'consumed') {
180
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
181
- }
182
- const artifactPath = path.join(task!.taskDir, artifact);
183
- if (safeLstat(task!.taskDir, artifactPath, 'file') === null) {
184
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
185
- }
186
- let content: string;
187
- try { content = fs.readFileSync(artifactPath, 'utf8'); }
188
- catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
189
- if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
190
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
191
- }
192
- const receiptReason = receiptGate(repoRoot, task!, options);
193
- if (receiptReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
194
- return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
195
- }
196
-
197
144
  function commitIntentCandidate(
198
145
  repoRoot: string,
199
146
  task: TaskRecord | undefined,
@@ -258,23 +205,12 @@ function readAuxiliaryCandidates(
258
205
  candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
259
206
  continue;
260
207
  }
261
- if (rootName === '.local-artifact-finalization-intents') {
262
- const match = LOCAL_INTENT_RE.exec(entry.name);
263
- if (!match || (selected && !selected.has(match[1]!))) {
264
- if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
265
- continue;
266
- }
267
- candidates.push(localIntentCandidate(
268
- repoRoot, tasks.get(match[1]!), filePath, match[1]!, match[2] as 'analysis' | 'plan' | 'code', match[3]!, identity, options
269
- ));
270
- } else {
271
- const match = COMMIT_INTENT_RE.exec(entry.name);
272
- if (!match || (selected && !selected.has(match[1]!))) {
273
- if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
274
- continue;
275
- }
276
- candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
208
+ const match = COMMIT_INTENT_RE.exec(entry.name);
209
+ if (!match || (selected && !selected.has(match[1]!))) {
210
+ if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
211
+ continue;
277
212
  }
213
+ candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
278
214
  }
279
215
  if (entries.length === 0) {
280
216
  candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
@@ -307,56 +243,7 @@ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): I
307
243
  return buildIntermediateCleanupReport(candidates, true);
308
244
  }
309
245
 
310
- function cleanupConsumedRecoveryFiles(repoRoot: string, task: TaskRecord | undefined, candidate: IntermediateCleanupItem): boolean {
311
- if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
312
- if (!task) return false;
313
- const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
314
- if (!match) return false;
315
- let intent: ReturnType<typeof readArtifactRecoveryIntent>;
316
- try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
317
- catch { return false; }
318
- if (!intent || intent.state !== 'consumed') return false;
319
-
320
- const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
321
- let stat: fs.Stats | null = null;
322
- try { stat = fs.lstatSync(recovery); } catch (error) {
323
- if (errorCode(error) === 'ENOENT') return true;
324
- return false;
325
- }
326
- if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
327
- let entries: fs.Dirent[];
328
- try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
329
- for (const entry of entries) {
330
- if (entry.isSymbolicLink()) return false;
331
- if (entry.isFile()) {
332
- if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
333
- continue;
334
- }
335
- if (!entry.isDirectory() || entry.name !== 'generations') return false;
336
- let generations: fs.Dirent[];
337
- try { generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true }); } catch { return false; }
338
- if (generations.some((generation) => generation.isSymbolicLink()
339
- || !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name))) return false;
340
- }
341
- try {
342
- for (const entry of entries) {
343
- const target = path.join(recovery, entry.name);
344
- if (entry.isFile()) fs.unlinkSync(target);
345
- else {
346
- for (const generation of fs.readdirSync(target, { withFileTypes: true })) fs.unlinkSync(path.join(target, generation.name));
347
- fs.rmdirSync(target);
348
- }
349
- }
350
- fs.rmdirSync(recovery);
351
- const recoveryRoot = path.dirname(recovery);
352
- if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
353
- return true;
354
- } catch (error) {
355
- return errorCode(error) === 'ENOENT';
356
- }
357
- }
358
-
359
- function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
246
+ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem): IntermediateCleanupItem {
360
247
  if (candidate.disposition !== 'planned') return candidate;
361
248
  const root = candidate.kind === 'EMPTY-AUX-PARENT'
362
249
  ? path.dirname(path.dirname(candidate.path))
@@ -373,9 +260,6 @@ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, t
373
260
  const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
374
261
  if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
375
262
  if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
376
- if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
377
- return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
378
- }
379
263
  try {
380
264
  if (candidate.kind === 'EMPTY-AUX-PARENT') {
381
265
  if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
@@ -434,7 +318,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
434
318
  if (initialCandidate.disposition !== 'planned') continue;
435
319
  const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
436
320
  items[index] = current
437
- ? removeCandidate(repoRoot, current, tasks.get(taskId))
321
+ ? removeCandidate(repoRoot, current)
438
322
  : { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
439
323
  }
440
324
  };
@@ -14,7 +14,6 @@ import type { LedgerRow, ReviewStage } from './ledger.ts';
14
14
  import { resolveTaskRef } from './resolve-ref.ts';
15
15
  import { writeTask } from './write.ts';
16
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
- import { allowsManualOverride } from './guard-override.ts';
18
17
  import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
19
18
  import type { ReworkClassification, ReworkIntent, ReworkTarget } from './rework-intent.ts';
20
19
  import { extractSection, extractSubSection } from './sections.ts';
@@ -149,8 +148,7 @@ function mapWrite(intent: LedgerIntent, entityId: string, before: LedgerRow | nu
149
148
  function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {}): LedgerIntentResult {
150
149
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
151
150
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
152
- const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
153
- if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
151
+ if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
154
152
  let content: string;
155
153
  let rows: LedgerRow[];
156
154
  try {
@@ -353,8 +351,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
353
351
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
354
352
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
355
353
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
356
- if ((!sameRoundMinorClose && !allowed.has(intent.status))
357
- && !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
354
+ if (!sameRoundMinorClose && !allowed.has(intent.status)) {
358
355
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
359
356
  }
360
357
  after = { ...before, status: intent.status, evidence };
@@ -388,7 +385,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
388
385
  }
389
386
 
390
387
  const result = writeTask({
391
- taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
388
+ taskRef: intent.taskRef, expectedState: 'active',
392
389
  mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
393
390
  dryRun: 'dryRun' in intent ? intent.dryRun : false
394
391
  }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });