@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
@@ -0,0 +1,92 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ const HANDOFF_VERSION = 1;
5
+ const MAX_HANDOFF_BYTES = 1024 * 1024;
6
+ function digest(value) {
7
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
8
+ }
9
+ export function finalizationHandoffPath(directory, taskId) {
10
+ return path.join(directory, '.agent-infra', 'task-finalization', `${taskId}.json`);
11
+ }
12
+ export function publishTaskFinalizationHandoff(directory, receipt, binding) {
13
+ if (receipt.taskId === '' || receipt.controlBinding?.generation !== binding.generation
14
+ || receipt.controlBinding.requestId !== binding.requestId) {
15
+ throw new Error('TASK_FINALIZATION_HANDOFF_BINDING_INVALID');
16
+ }
17
+ const envelope = {
18
+ version: HANDOFF_VERSION, taskId: receipt.taskId, binding, receipt, receiptSha256: digest(receipt)
19
+ };
20
+ const target = finalizationHandoffPath(directory, receipt.taskId);
21
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
22
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
23
+ fs.writeFileSync(temporary, `${JSON.stringify(envelope)}\n`, { mode: 0o600, flag: 'wx' });
24
+ try {
25
+ fs.renameSync(temporary, target);
26
+ }
27
+ catch (error) {
28
+ try {
29
+ fs.unlinkSync(temporary);
30
+ }
31
+ catch { /* preserve the primary error */ }
32
+ throw error;
33
+ }
34
+ return digest(envelope);
35
+ }
36
+ export function readTaskFinalizationHandoff(directory, taskId, binding, handoffSha256) {
37
+ const file = finalizationHandoffPath(directory, taskId);
38
+ const initial = fs.lstatSync(file);
39
+ if (!initial.isFile() || initial.isSymbolicLink() || initial.size > MAX_HANDOFF_BYTES) {
40
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
41
+ }
42
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
43
+ let content;
44
+ try {
45
+ const opened = fs.fstatSync(descriptor);
46
+ if (!opened.isFile() || opened.dev !== initial.dev || opened.ino !== initial.ino
47
+ || opened.size > MAX_HANDOFF_BYTES)
48
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
49
+ const bytes = Buffer.alloc(MAX_HANDOFF_BYTES + 1);
50
+ const length = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
51
+ if (length !== opened.size || length > MAX_HANDOFF_BYTES)
52
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
53
+ content = bytes.toString('utf8', 0, length);
54
+ const after = fs.fstatSync(descriptor);
55
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
56
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
57
+ }
58
+ }
59
+ finally {
60
+ fs.closeSync(descriptor);
61
+ }
62
+ const envelope = JSON.parse(content);
63
+ if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
64
+ || Object.keys(envelope).sort().join('\0') !== ['binding', 'receipt', 'receiptSha256', 'taskId', 'version'].join('\0')
65
+ || envelope.version !== HANDOFF_VERSION || envelope.taskId !== taskId
66
+ || !envelope.binding || typeof envelope.binding !== 'object' || Array.isArray(envelope.binding)
67
+ || Object.keys(envelope.binding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
68
+ || envelope.binding.generation !== binding.generation
69
+ || envelope.binding.requestId !== binding.requestId || !envelope.receipt
70
+ || envelope.receipt.controlBinding?.generation !== binding.generation
71
+ || envelope.receipt.controlBinding.requestId !== binding.requestId
72
+ || typeof envelope.receiptSha256 !== 'string' || envelope.receiptSha256 !== digest(envelope.receipt)
73
+ || digest(envelope) !== handoffSha256) {
74
+ throw new Error('TASK_FINALIZATION_HANDOFF_INVALID');
75
+ }
76
+ return {
77
+ receipt: envelope.receipt,
78
+ cleanup: () => {
79
+ try {
80
+ const current = fs.lstatSync(file);
81
+ if (current.isFile() && !current.isSymbolicLink()
82
+ && current.dev === initial.dev && current.ino === initial.ino)
83
+ fs.unlinkSync(file);
84
+ }
85
+ catch (error) {
86
+ if (error.code !== 'ENOENT')
87
+ throw error;
88
+ }
89
+ }
90
+ };
91
+ }
92
+ //# sourceMappingURL=finalization-handoff.js.map
@@ -6,13 +6,16 @@ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
6
6
  import { backfillCompletionComments, inspectCompletionBackfillEligibility } from '../platform/completion-backfill.js';
7
7
  import { taskIssueIdentity } from '../platform/task-identities.js';
8
8
  import { parseTaskFrontmatter } from './frontmatter.js';
9
- import { applyTaskLifecycle } from './lifecycle.js';
9
+ import { applyTaskLifecycle, inspectTaskLifecycleProgress } from './lifecycle.js';
10
10
  import { resolveTaskRef } from './resolve-ref.js';
11
11
  import { inspectShortIdRegistry } from './short-id.js';
12
12
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
13
13
  import { verifyTaskEvent } from './verification.js';
14
14
  import { projectFinalizationWarning } from './workflow-warning-intents.js';
15
15
  import { mergeOperationWarnings } from './operation-outcome.js';
16
+ import { readTaskFinalizationHandoff } from './finalization-handoff.js';
17
+ import { readSandboxControlManifest } from '../sandbox/control/lifecycle.js';
18
+ import { validateSandboxControlIdentity } from '../sandbox/control/identity-sentinel.js';
16
19
  const RECEIPT_VERSION = 4;
17
20
  const FINALIZATION_STEPS = ['lifecycle', 'task-comment', 'verification', 'summary'];
18
21
  function finalizationRoot(repoRoot) {
@@ -89,19 +92,32 @@ function validateReceipt(value, taskId) {
89
92
  const receipt = value;
90
93
  const controlBinding = receipt.controlBinding;
91
94
  const states = ['pending', 'done', 'skipped'];
92
- if (receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
95
+ const requiredKeys = [
96
+ 'version', 'taskId', 'intent', 'receiptId', 'revision', 'lifecycle', 'taskComment',
97
+ 'verification', 'summary', 'warningProjection', 'warnings', 'updatedAt', 'lastError'
98
+ ];
99
+ if (receipt.controlBinding !== undefined)
100
+ requiredKeys.push('controlBinding');
101
+ if (Object.keys(receipt).sort().join('\0') !== requiredKeys.sort().join('\0')
102
+ ||
103
+ receipt.version !== RECEIPT_VERSION || receipt.taskId !== taskId || receipt.intent !== 'complete'
93
104
  || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
94
105
  || !Number.isSafeInteger(receipt.revision) || Number(receipt.revision) < 0
95
- || !states.includes(String(receipt.lifecycle))
96
- || !states.includes(String(receipt.taskComment))
97
- || !states.includes(String(receipt.verification))
98
- || !states.includes(String(receipt.summary))
99
- || !['pending', 'done'].includes(String(receipt.warningProjection))
106
+ || !states.includes(receipt.lifecycle)
107
+ || !states.includes(receipt.taskComment)
108
+ || !states.includes(receipt.verification)
109
+ || !states.includes(receipt.summary)
110
+ || !['pending', 'done'].includes(receipt.warningProjection)
100
111
  || !Array.isArray(receipt.warnings) || receipt.warnings.some((warning) => !validWarning(warning))
101
- || typeof receipt.updatedAt !== 'string'
102
- || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)))
112
+ || typeof receipt.updatedAt !== 'string' || receipt.updatedAt.length === 0
113
+ || (receipt.lastError !== null && (typeof receipt.lastError !== 'object' || Array.isArray(receipt.lastError)
114
+ || Object.keys(receipt.lastError).sort().join('\0') !== ['code', 'message', 'retryable'].join('\0')
115
+ || typeof receipt.lastError.code !== 'string'
116
+ || typeof receipt.lastError.message !== 'string'
117
+ || typeof receipt.lastError.retryable !== 'boolean'))
103
118
  || (receipt.controlBinding !== undefined && (!controlBinding || typeof controlBinding !== 'object'
104
119
  || Array.isArray(controlBinding)
120
+ || Object.keys(controlBinding).sort().join('\0') !== ['generation', 'requestId'].join('\0')
105
121
  || typeof controlBinding.generation !== 'string' || controlBinding.generation.length === 0
106
122
  || typeof controlBinding.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId))))
107
123
  throw new Error('receipt schema is invalid');
@@ -134,6 +150,12 @@ function writeReceipt(repoRoot, receipt) {
134
150
  throw error;
135
151
  }
136
152
  }
153
+ export function bindTaskFinalizationReceipt(repoRoot, taskId, controlBinding) {
154
+ const receipt = readReceipt(path.resolve(repoRoot), taskId);
155
+ if (!receipt)
156
+ throw new Error('TASK_FINALIZATION_PREPARATION_REQUIRED');
157
+ return updateReceipt(path.resolve(repoRoot), receipt, { controlBinding, lastError: null });
158
+ }
137
159
  function updateReceipt(repoRoot, receipt, patch) {
138
160
  const current = readReceipt(repoRoot, receipt.taskId);
139
161
  if (!current || current.receiptId !== receipt.receiptId)
@@ -629,8 +651,15 @@ async function prepareUnderLock(request, taskId, options) {
629
651
  return failed(taskId, errorOf(error, 'TASK_FINALIZATION_RECEIPT_INVALID'));
630
652
  }
631
653
  if (options.controlBinding && (receipt.controlBinding?.generation !== options.controlBinding.generation
632
- || receipt.controlBinding.requestId !== options.controlBinding.requestId))
633
- receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
654
+ || receipt.controlBinding.requestId !== options.controlBinding.requestId)) {
655
+ const state = resolveTaskRef(taskId, { repoRoot });
656
+ if (!(receipt.lifecycle === 'done' && state.ok && state.state === 'completed')) {
657
+ return failed(taskId, {
658
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
659
+ message: 'canonical receipt is not bound to the current sandbox request', retryable: false
660
+ });
661
+ }
662
+ }
634
663
  const preflightState = resolveTaskRef(taskId, { repoRoot });
635
664
  if (options.preflight && preflightState.ok && preflightState.state === 'active') {
636
665
  try {
@@ -878,7 +907,53 @@ async function commitPreparedTaskFinalization(request, options) {
878
907
  return failed(resolved.taskId, { code: resolved.code, message: resolved.message, retryable: false });
879
908
  try {
880
909
  return await withTaskExecutionLock(repoRoot, resolved.taskId, 'task-finalization.commit', () => {
881
- const receipt = readReceipt(repoRoot, resolved.taskId);
910
+ if (options.controlBinding && options.manifestPath) {
911
+ try {
912
+ const manifest = readSandboxControlManifest(options.manifestPath);
913
+ const identity = validateSandboxControlIdentity({
914
+ publicStatusDir: manifest.publicStatusDir,
915
+ root: path.dirname(path.resolve(options.manifestPath)),
916
+ mode: manifest.mode,
917
+ taskId: manifest.taskId,
918
+ generation: manifest.generation,
919
+ controlRootId: manifest.controlRootId
920
+ });
921
+ if (identity.state !== 'valid' || manifest.taskId !== resolved.taskId
922
+ || manifest.generation !== options.controlBinding.generation
923
+ || fs.realpathSync.native(manifest.repoRoot) !== fs.realpathSync.native(repoRoot)) {
924
+ throw new Error('current sandbox manifest does not match finalization request');
925
+ }
926
+ }
927
+ catch (error) {
928
+ return failed(resolved.taskId, {
929
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
930
+ message: error instanceof Error ? error.message : String(error), retryable: false
931
+ });
932
+ }
933
+ }
934
+ let receipt = readReceipt(repoRoot, resolved.taskId);
935
+ if (!receipt && options.controlBinding && options.handoffDirectory && request.handoffSha256) {
936
+ try {
937
+ const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
938
+ receipt = validateReceipt(handoff.receipt, resolved.taskId);
939
+ if (receipt.lifecycle !== 'pending')
940
+ throw new Error('handoff lifecycle must be pending');
941
+ if (inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent) === 'unknown') {
942
+ throw new Error('TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE');
943
+ }
944
+ writeReceipt(repoRoot, receipt);
945
+ try {
946
+ handoff.cleanup();
947
+ }
948
+ catch { /* keep the persisted canonical receipt usable */ }
949
+ }
950
+ catch (error) {
951
+ return failed(resolved.taskId, {
952
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
953
+ message: error instanceof Error ? error.message : String(error), retryable: false
954
+ });
955
+ }
956
+ }
882
957
  if (!receipt)
883
958
  return failed(resolved.taskId, {
884
959
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation receipt is unavailable', retryable: true
@@ -886,6 +961,42 @@ async function commitPreparedTaskFinalization(request, options) {
886
961
  if (receipt.lifecycle === 'done' && resolved.state === 'completed') {
887
962
  return terminalResult(resolved.taskId, receipt, { lifecycle: { status: 'no-op', changed: false, error: null } }, false);
888
963
  }
964
+ if (options.controlBinding) {
965
+ const proof = inspectTaskLifecycleProgress(repoRoot, resolved.taskId, request.agent);
966
+ const oldBinding = receipt.controlBinding;
967
+ if (!oldBinding || oldBinding.generation !== options.controlBinding.generation || proof === 'unknown') {
968
+ return failed(resolved.taskId, {
969
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
970
+ message: 'canonical receipt has no valid host recovery proof', retryable: false
971
+ });
972
+ }
973
+ if (oldBinding.requestId !== options.controlBinding.requestId && proof === 'not-started') {
974
+ if (receipt.lifecycle !== 'pending' || !options.handoffDirectory || !request.handoffSha256) {
975
+ return failed(resolved.taskId, {
976
+ code: 'TASK_FINALIZATION_RECOVERY_PROOF_UNAVAILABLE',
977
+ message: 'pending receipt cannot be rebound without a current handoff', retryable: false
978
+ });
979
+ }
980
+ try {
981
+ const handoff = readTaskFinalizationHandoff(options.handoffDirectory, resolved.taskId, options.controlBinding, request.handoffSha256);
982
+ const nextReceipt = validateReceipt(handoff.receipt, resolved.taskId);
983
+ if (nextReceipt.receiptId !== receipt.receiptId || nextReceipt.intent !== receipt.intent
984
+ || nextReceipt.lifecycle !== 'pending')
985
+ throw new Error('handoff is not continuous with canonical receipt');
986
+ receipt = updateReceipt(repoRoot, receipt, { controlBinding: options.controlBinding });
987
+ try {
988
+ handoff.cleanup();
989
+ }
990
+ catch { /* keep the persisted canonical receipt usable */ }
991
+ }
992
+ catch (error) {
993
+ return failed(resolved.taskId, {
994
+ code: 'TASK_FINALIZATION_HANDOFF_INVALID',
995
+ message: error instanceof Error ? error.message : String(error), retryable: false
996
+ });
997
+ }
998
+ }
999
+ }
889
1000
  if (receipt.taskComment === 'pending' || receipt.verification === 'pending' || receipt.summary === 'pending' || receipt.warningProjection === 'pending') {
890
1001
  return failed(resolved.taskId, {
891
1002
  code: 'TASK_FINALIZATION_PREPARATION_REQUIRED', message: 'sandbox preparation has pending external steps', retryable: true
@@ -7,17 +7,10 @@ import { checkpointCommitMatches } from './commit-identity.js';
7
7
  import { readCheckpointIntent } from './commit-intent.js';
8
8
  import { parseTaskFrontmatter } from './frontmatter.js';
9
9
  import { readTaskFinalizationReceipt } from './finalization.js';
10
- import { semanticDigest, sha256Content } from './local-artifact-finalization.js';
11
- import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.js';
12
- import { parseArtifactName } from './artifact-name.js';
13
10
  import { enumerateAllTaskDirs } from './resolve-ref.js';
14
11
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
15
- const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
16
12
  const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
17
- const AUXILIARY_ROOTS = [
18
- '.local-artifact-finalization-intents',
19
- '.task-commit-intents'
20
- ];
13
+ const AUXILIARY_ROOTS = ['.task-commit-intents'];
21
14
  function auxiliaryRoot(repoRoot, name) {
22
15
  return path.join(repoRoot, '.agents', 'workspace', name);
23
16
  }
@@ -98,46 +91,6 @@ function receiptGate(repoRoot, task, options) {
98
91
  }
99
92
  return null;
100
93
  }
101
- function canonicalLocalIntentPath(taskId, family, artifact) {
102
- return `${taskId}-${family}-${artifact}.json`;
103
- }
104
- function localIntentCandidate(repoRoot, task, filePath, taskId, family, artifact, identity, options) {
105
- const protectedReason = taskGate(task);
106
- if (protectedReason)
107
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
108
- if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
109
- || !parseArtifactName(artifact)?.family || parseArtifactName(artifact).family !== family) {
110
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
111
- }
112
- let intent;
113
- try {
114
- intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact);
115
- }
116
- catch {
117
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity);
118
- }
119
- if (!intent || intent.state !== 'consumed') {
120
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
121
- }
122
- const artifactPath = path.join(task.taskDir, artifact);
123
- if (safeLstat(task.taskDir, artifactPath, 'file') === null) {
124
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
125
- }
126
- let content;
127
- try {
128
- content = fs.readFileSync(artifactPath, 'utf8');
129
- }
130
- catch {
131
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity);
132
- }
133
- if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
134
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
135
- }
136
- const receiptReason = receiptGate(repoRoot, task, options);
137
- if (receiptReason)
138
- return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
139
- return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
140
- }
141
94
  function commitIntentCandidate(repoRoot, task, filePath, taskId, identity, options) {
142
95
  const protectedReason = taskGate(task);
143
96
  if (protectedReason)
@@ -209,24 +162,13 @@ function readAuxiliaryCandidates(repoRoot, tasks, options) {
209
162
  candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
210
163
  continue;
211
164
  }
212
- if (rootName === '.local-artifact-finalization-intents') {
213
- const match = LOCAL_INTENT_RE.exec(entry.name);
214
- if (!match || (selected && !selected.has(match[1]))) {
215
- if (!selected || !match)
216
- candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
217
- continue;
218
- }
219
- candidates.push(localIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], match[2], match[3], identity, options));
220
- }
221
- else {
222
- const match = COMMIT_INTENT_RE.exec(entry.name);
223
- if (!match || (selected && !selected.has(match[1]))) {
224
- if (!selected || !match)
225
- candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
226
- continue;
227
- }
228
- candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
165
+ const match = COMMIT_INTENT_RE.exec(entry.name);
166
+ if (!match || (selected && !selected.has(match[1]))) {
167
+ if (!selected || !match)
168
+ candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
169
+ continue;
229
170
  }
171
+ candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
230
172
  }
231
173
  if (entries.length === 0) {
232
174
  candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
@@ -255,85 +197,7 @@ function scanIntermediateCleanup(repoRoot, options = {}) {
255
197
  const candidates = scanInternal(repoRoot, options);
256
198
  return buildIntermediateCleanupReport(candidates, true);
257
199
  }
258
- function cleanupConsumedRecoveryFiles(repoRoot, task, candidate) {
259
- if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId)
260
- return true;
261
- if (!task)
262
- return false;
263
- const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
264
- if (!match)
265
- return false;
266
- let intent;
267
- try {
268
- intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2], match[3]);
269
- }
270
- catch {
271
- return false;
272
- }
273
- if (!intent || intent.state !== 'consumed')
274
- return false;
275
- const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
276
- let stat = null;
277
- try {
278
- stat = fs.lstatSync(recovery);
279
- }
280
- catch (error) {
281
- if (errorCode(error) === 'ENOENT')
282
- return true;
283
- return false;
284
- }
285
- if (!stat.isDirectory() || stat.isSymbolicLink())
286
- return false;
287
- let entries;
288
- try {
289
- entries = fs.readdirSync(recovery, { withFileTypes: true });
290
- }
291
- catch {
292
- return false;
293
- }
294
- for (const entry of entries) {
295
- if (entry.isSymbolicLink())
296
- return false;
297
- if (entry.isFile()) {
298
- if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name))
299
- return false;
300
- continue;
301
- }
302
- if (!entry.isDirectory() || entry.name !== 'generations')
303
- return false;
304
- let generations;
305
- try {
306
- generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true });
307
- }
308
- catch {
309
- return false;
310
- }
311
- if (generations.some((generation) => generation.isSymbolicLink()
312
- || !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name)))
313
- return false;
314
- }
315
- try {
316
- for (const entry of entries) {
317
- const target = path.join(recovery, entry.name);
318
- if (entry.isFile())
319
- fs.unlinkSync(target);
320
- else {
321
- for (const generation of fs.readdirSync(target, { withFileTypes: true }))
322
- fs.unlinkSync(path.join(target, generation.name));
323
- fs.rmdirSync(target);
324
- }
325
- }
326
- fs.rmdirSync(recovery);
327
- const recoveryRoot = path.dirname(recovery);
328
- if (fs.readdirSync(recoveryRoot).length === 0)
329
- fs.rmdirSync(recoveryRoot);
330
- return true;
331
- }
332
- catch (error) {
333
- return errorCode(error) === 'ENOENT';
334
- }
335
- }
336
- function removeCandidate(repoRoot, candidate, task) {
200
+ function removeCandidate(repoRoot, candidate) {
337
201
  if (candidate.disposition !== 'planned')
338
202
  return candidate;
339
203
  const root = candidate.kind === 'EMPTY-AUX-PARENT'
@@ -358,9 +222,6 @@ function removeCandidate(repoRoot, candidate, task) {
358
222
  return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
359
223
  if (!sameFileIdentity(expected, actual))
360
224
  return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
361
- if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
362
- return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
363
- }
364
225
  try {
365
226
  if (candidate.kind === 'EMPTY-AUX-PARENT') {
366
227
  if (fs.readdirSync(candidate.path).length !== 0)
@@ -413,7 +274,7 @@ function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
413
274
  continue;
414
275
  const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
415
276
  items[index] = current
416
- ? removeCandidate(repoRoot, current, tasks.get(taskId))
277
+ ? removeCandidate(repoRoot, current)
417
278
  : { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
418
279
  }
419
280
  };
@@ -6,7 +6,6 @@ import { parseArtifactName } from './artifact-name.js';
6
6
  import { LEDGER_COLUMNS, LEDGER_HEADINGS, LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, nextHdId, parseLedgerDocument, validateLedgerRows } from './ledger.js';
7
7
  import { resolveTaskRef } from './resolve-ref.js';
8
8
  import { writeTask } from './write.js';
9
- import { allowsManualOverride } from './guard-override.js';
10
9
  import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.js';
11
10
  import { extractSection, extractSubSection } from './sections.js';
12
11
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
@@ -105,8 +104,7 @@ function applyLedgerIntent(intent, options = {}) {
105
104
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
106
105
  if (!resolved.ok)
107
106
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
108
- const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
109
- if (resolved.state !== 'active' && !stateOverride)
107
+ if (resolved.state !== 'active')
110
108
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
111
109
  let content;
112
110
  let rows;
@@ -341,8 +339,7 @@ function applyLedgerIntent(intent, options = {}) {
341
339
  ? new Set(['confirmed', 'open', 'needs-human-decision'])
342
340
  : before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set();
343
341
  const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
344
- if ((!sameRoundMinorClose && !allowed.has(intent.status))
345
- && !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
342
+ if (!sameRoundMinorClose && !allowed.has(intent.status)) {
346
343
  return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
347
344
  }
348
345
  after = { ...before, status: intent.status, evidence };
@@ -375,7 +372,7 @@ function applyLedgerIntent(intent, options = {}) {
375
372
  }
376
373
  }
377
374
  const result = writeTask({
378
- taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
375
+ taskRef: intent.taskRef, expectedState: 'active',
379
376
  mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
380
377
  dryRun: 'dryRun' in intent ? intent.dryRun : false
381
378
  }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
@@ -43,9 +43,6 @@ const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
43
43
  guardId: 'G-03',
44
44
  code
45
45
  }));
46
- function allowsManualOverride(options, code) {
47
- return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
48
- }
49
46
  const STEPS = ['task-written', 'directory-moved', 'registry-committed'];
50
47
  const DEFAULT_IO = {
51
48
  renameSync: (source, target) => fs.renameSync(source, target),
@@ -320,6 +317,49 @@ export function readLifecycleJournalEvidence(repoRoot, taskId) {
320
317
  return { exists: true, completedSteps: [], failure: 'SANDBOX_CONTROL_LIFECYCLE_JOURNAL_INVALID' };
321
318
  }
322
319
  }
320
+ export function inspectTaskLifecycleProgress(repoRoot, taskId, agent) {
321
+ try {
322
+ const hot = locateHotTaskDirs(repoRoot, taskId);
323
+ if (hot.length !== 1 || (hot[0].state !== 'active' && hot[0].state !== 'completed'))
324
+ return 'unknown';
325
+ const { state, taskDir } = hot[0];
326
+ const content = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
327
+ const frontmatter = parseTypedTaskFrontmatter(content);
328
+ if (frontmatter.id !== taskId)
329
+ return 'unknown';
330
+ const journalPath = path.join(taskDir, '.task-lifecycle.json');
331
+ if (!fs.existsSync(journalPath)) {
332
+ if (state === 'active')
333
+ return frontmatter.status === 'active' ? 'not-started' : 'unknown';
334
+ return matchingCompletion(content, { taskRef: taskId, intent: 'complete', agent })
335
+ ? 'started-recoverable' : 'unknown';
336
+ }
337
+ const journal = parseJournal(fs.readFileSync(journalPath, 'utf8'));
338
+ const request = { taskRef: taskId, intent: 'complete', agent };
339
+ if (journal.taskId !== taskId || journal.intent !== 'complete'
340
+ || journal.intentDigest !== intentIdentity(request, taskId)
341
+ || journal.sourceState !== 'active' || journal.targetState !== 'completed')
342
+ return 'unknown';
343
+ if (journal.completedSteps.some((step, index) => STEPS[index] !== step))
344
+ return 'unknown';
345
+ const directoryMoved = journal.completedSteps.includes('directory-moved');
346
+ const registryCommitted = journal.completedSteps.includes('registry-committed');
347
+ const hasShortId = loadShortIdByTaskId(repoRoot).has(taskId);
348
+ if (state === 'active' && (directoryMoved || !hasShortId))
349
+ return 'unknown';
350
+ if (state === 'completed' && (!journal.completedSteps.includes('task-written')
351
+ || !matchingCompletion(content, request) || (registryCommitted && hasShortId)))
352
+ return 'unknown';
353
+ if (frontmatter.status !== state
354
+ && !(state === 'active' && frontmatter.status === 'completed' && matchingCompletion(content, request))) {
355
+ return 'unknown';
356
+ }
357
+ return 'started-recoverable';
358
+ }
359
+ catch {
360
+ return 'unknown';
361
+ }
362
+ }
323
363
  function directoryManifest(root) {
324
364
  const entries = [];
325
365
  const visit = (current, relative = '') => {
@@ -440,7 +480,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
440
480
  }
441
481
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
442
482
  }
443
- if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
483
+ if (!spec.sources.includes(sourceState) && !existingJournal) {
444
484
  return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
445
485
  }
446
486
  if (sourcePath !== targetPath && fs.existsSync(targetPath) && !existingJournal) {
@@ -456,7 +496,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
456
496
  }
457
497
  if (frontmatter.id !== taskId)
458
498
  return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
459
- if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'))
499
+ if (!locateActivityLog(content))
460
500
  return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
461
501
  if (spec.target === 'active') {
462
502
  const contract = validateCurrentTaskContract(content);
@@ -472,10 +512,10 @@ function applyTaskLifecycle(requestInput, options = {}) {
472
512
  }
473
513
  const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}');
474
514
  const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
475
- if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
515
+ if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
476
516
  return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
477
517
  }
478
- if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
518
+ if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
479
519
  return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
480
520
  }
481
521
  const digest = intentIdentity(request, taskId);
@@ -499,8 +539,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
499
539
  return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
500
540
  return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
501
541
  }
502
- const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
503
- if (journal.intentDigest !== digest && !overrideResumesRecordedFailure)
542
+ if (journal.intentDigest !== digest)
504
543
  return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
505
544
  }
506
545
  else {
@@ -516,7 +555,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
516
555
  }
517
556
  journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
518
557
  if (request.dryRun) {
519
- const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
558
+ const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, false);
520
559
  if ('code' in plannedMutations)
521
560
  return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
522
561
  const taskPlan = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true }, {
@@ -558,7 +597,7 @@ function applyTaskLifecycle(requestInput, options = {}) {
558
597
  completed.add('task-written');
559
598
  if (!completed.has('task-written')) {
560
599
  const current = io.readFileSync(path.join(sourcePath, 'task.md'));
561
- const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
600
+ const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, false);
562
601
  if ('code' in mutations)
563
602
  return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
564
603
  const writeResult = writeTask({ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations }, {