@fitlab-ai/agent-infra 0.11.9 → 0.11.11

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 (219) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/agent-clients/adapter.js +20 -0
  3. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  4. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  5. package/dist/lib/git/workflow.js +1 -1
  6. package/dist/lib/internal/cli-route-inventory.js +0 -3
  7. package/dist/lib/internal/manual-validation.js +2 -2
  8. package/dist/lib/internal/platform-pr.js +4 -5
  9. package/dist/lib/internal/sandbox-control.js +30 -5
  10. package/dist/lib/internal/task-artifact.js +1 -1
  11. package/dist/lib/internal/task-create.js +15 -1
  12. package/dist/lib/internal/task-finalization.js +14 -25
  13. package/dist/lib/internal/task-operation-registry.js +6 -141
  14. package/dist/lib/internal/task-platform-recovery.js +9 -4
  15. package/dist/lib/platform/completion-backfill.js +2 -1
  16. package/dist/lib/platform/context.js +4 -2
  17. package/dist/lib/platform/github-client.js +20 -17
  18. package/dist/lib/platform/github-provider.js +42 -22
  19. package/dist/lib/platform/github-release-notes.js +4 -2
  20. package/dist/lib/platform/in-label-sync.js +2 -1
  21. package/dist/lib/platform/issue-comments.js +13 -9
  22. package/dist/lib/platform/operation-coordinator.js +1 -1
  23. package/dist/lib/platform/pr-review.js +2 -1
  24. package/dist/lib/platform/pr-summary.js +45 -8
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +83 -16
  30. package/dist/lib/platform/verification-sync.js +17 -14
  31. package/dist/lib/sandbox/commands/create.js +18 -2
  32. package/dist/lib/sandbox/commands/enter.js +47 -33
  33. package/dist/lib/sandbox/control/client.js +99 -17
  34. package/dist/lib/sandbox/control/executor.js +12 -14
  35. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  36. package/dist/lib/sandbox/control/protocol.js +2 -3
  37. package/dist/lib/sandbox/control/server.js +42 -2
  38. package/dist/lib/sandbox/environment.js +150 -0
  39. package/dist/lib/sandbox/recovery.js +8 -0
  40. package/dist/lib/sandbox/removal.js +0 -1
  41. package/dist/lib/sandbox/workspace-identity.js +0 -14
  42. package/dist/lib/sandbox/workspace-view.js +33 -6
  43. package/dist/lib/task/artifact-command.js +18 -3
  44. package/dist/lib/task/artifact-lifecycle.js +121 -63
  45. package/dist/lib/task/artifact-selection.js +1 -1
  46. package/dist/lib/task/capabilities.js +59 -22
  47. package/dist/lib/task/commands/status.js +0 -27
  48. package/dist/lib/task/commit-identity.js +22 -7
  49. package/dist/lib/task/commit-intent.js +67 -26
  50. package/dist/lib/task/commit-operation.js +19 -8
  51. package/dist/lib/task/completion-facts.js +32 -10
  52. package/dist/lib/task/control-authority.js +2 -6
  53. package/dist/lib/task/current-contract.js +1 -5
  54. package/dist/lib/task/events.js +9 -275
  55. package/dist/lib/task/execution-inputs.js +14 -0
  56. package/dist/lib/task/finalization-envelope.js +4 -0
  57. package/dist/lib/task/finalization.js +43 -86
  58. package/dist/lib/task/frontmatter.js +25 -2
  59. package/dist/lib/task/ledger-intents.js +6 -8
  60. package/dist/lib/task/orchestration.js +11 -10
  61. package/dist/lib/task/platform-operation-journal.js +29 -10
  62. package/dist/lib/task/platform-operation-recovery.js +109 -35
  63. package/dist/lib/task/resolve-ref.js +8 -3
  64. package/dist/lib/task/review-fingerprint.js +4 -26
  65. package/dist/lib/task/verification-engine.js +95 -6
  66. package/dist/lib/task/write.js +0 -11
  67. package/dist/lifecycle-build-manifest.json +3 -3
  68. package/lib/agent-clients/adapter.ts +22 -0
  69. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  70. package/lib/agent-clients/adapters/codex.ts +4 -0
  71. package/lib/git/workflow.ts +1 -1
  72. package/lib/internal/cli-route-inventory.ts +0 -2
  73. package/lib/internal/manual-validation.ts +2 -2
  74. package/lib/internal/platform-pr.ts +3 -4
  75. package/lib/internal/sandbox-control.ts +39 -6
  76. package/lib/internal/task-artifact.ts +1 -1
  77. package/lib/internal/task-create.ts +15 -2
  78. package/lib/internal/task-finalization.ts +16 -26
  79. package/lib/internal/task-operation-registry.ts +6 -153
  80. package/lib/internal/task-platform-recovery.ts +9 -4
  81. package/lib/platform/completion-backfill.ts +2 -1
  82. package/lib/platform/context.ts +8 -4
  83. package/lib/platform/github-client.ts +24 -20
  84. package/lib/platform/github-provider.ts +47 -26
  85. package/lib/platform/github-release-notes.ts +5 -3
  86. package/lib/platform/in-label-sync.ts +2 -1
  87. package/lib/platform/issue-comments.ts +14 -9
  88. package/lib/platform/operation-coordinator.ts +1 -1
  89. package/lib/platform/pr-review.ts +2 -1
  90. package/lib/platform/pr-summary.ts +43 -7
  91. package/lib/platform/provider-bridge.ts +10 -9
  92. package/lib/platform/provider-contract.ts +6 -21
  93. package/lib/platform/provider-loader.ts +7 -6
  94. package/lib/platform/provider-validation.ts +30 -8
  95. package/lib/platform/pull-requests.ts +78 -15
  96. package/lib/platform/repository-metadata.ts +3 -2
  97. package/lib/platform/security-alerts.ts +1 -1
  98. package/lib/platform/types.ts +2 -0
  99. package/lib/platform/verification-sync.ts +18 -14
  100. package/lib/sandbox/commands/create.ts +24 -2
  101. package/lib/sandbox/commands/enter.ts +42 -39
  102. package/lib/sandbox/control/client.ts +107 -19
  103. package/lib/sandbox/control/executor.ts +13 -15
  104. package/lib/sandbox/control/host-recovery.ts +195 -0
  105. package/lib/sandbox/control/protocol.ts +4 -5
  106. package/lib/sandbox/control/server.ts +42 -1
  107. package/lib/sandbox/environment.ts +162 -0
  108. package/lib/sandbox/recovery.ts +10 -0
  109. package/lib/sandbox/removal.ts +2 -10
  110. package/lib/sandbox/tool-types.ts +1 -0
  111. package/lib/sandbox/workspace-identity.ts +0 -26
  112. package/lib/sandbox/workspace-view.ts +33 -8
  113. package/lib/task/artifact-command.ts +19 -3
  114. package/lib/task/artifact-lifecycle.ts +102 -53
  115. package/lib/task/artifact-selection.ts +1 -1
  116. package/lib/task/capabilities.ts +52 -26
  117. package/lib/task/commands/status.ts +0 -31
  118. package/lib/task/commit-identity.ts +24 -7
  119. package/lib/task/commit-intent.ts +58 -24
  120. package/lib/task/commit-operation.ts +17 -8
  121. package/lib/task/completion-facts.ts +31 -9
  122. package/lib/task/control-authority.ts +1 -8
  123. package/lib/task/current-contract.ts +1 -6
  124. package/lib/task/events.ts +9 -248
  125. package/lib/task/execution-inputs.ts +25 -0
  126. package/lib/task/finalization-envelope.ts +21 -0
  127. package/lib/task/finalization.ts +40 -82
  128. package/lib/task/frontmatter.ts +26 -2
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/orchestration.ts +11 -17
  131. package/lib/task/platform-operation-journal.ts +47 -13
  132. package/lib/task/platform-operation-recovery.ts +102 -37
  133. package/lib/task/resolve-ref.ts +8 -3
  134. package/lib/task/review-fingerprint.ts +4 -26
  135. package/lib/task/verification-engine.ts +103 -9
  136. package/lib/task/write.ts +0 -19
  137. package/package.json +1 -1
  138. package/templates/.agents/rules/README.en.md +1 -1
  139. package/templates/.agents/rules/README.zh-CN.md +1 -1
  140. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  141. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  142. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  143. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  144. package/templates/.agents/rules/issue-sync.en.md +1 -1
  145. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  146. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  147. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  148. package/templates/.agents/rules/review-handshake.en.md +3 -0
  149. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  150. package/templates/.agents/rules/task-management.en.md +1 -1
  151. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  152. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  158. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  159. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  160. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  163. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
  164. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  166. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
  168. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  169. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  170. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  172. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  173. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  176. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  177. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  178. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  179. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  180. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  181. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  182. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  183. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  184. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  197. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  198. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  199. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  200. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
  201. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
  202. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  205. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  206. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  207. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  208. package/templates/.agents/templates/task.zh-CN.md +0 -12
  209. package/templates/.github/workflows/pr-label.yml +1 -2
  210. package/dist/lib/internal/task-invalidation.js +0 -63
  211. package/dist/lib/task/finalization-handoff.js +0 -93
  212. package/dist/lib/task/intermediate-cleanup.js +0 -327
  213. package/dist/lib/task/invalidation-command.js +0 -88
  214. package/dist/lib/task/invalidation.js +0 -242
  215. package/lib/internal/task-invalidation.ts +0 -41
  216. package/lib/task/finalization-handoff.ts +0 -105
  217. package/lib/task/intermediate-cleanup.ts +0 -393
  218. package/lib/task/invalidation-command.ts +0 -88
  219. package/lib/task/invalidation.ts +0 -313
@@ -8,7 +8,7 @@ function selectArtifactDisposition(request) {
8
8
  if (!request.latest) {
9
9
  return {
10
10
  disposition: 'create',
11
- reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
11
+ reasonCode: 'no-completed-history',
12
12
  artifact: request.next, writeRequired: true
13
13
  };
14
14
  }
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parseArtifactName } from './artifact-name.js';
4
- import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
5
4
  import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.js';
6
5
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
6
  import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
@@ -14,9 +13,11 @@ import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
14
13
  import { parseImplementationInputs } from './implementation-inputs.js';
15
14
  import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
16
15
  import { inspectReviewIdentity } from './review-identity.js';
16
+ import { inspectArtifactDirectory } from './artifact-lifecycle.js';
17
+ import { executionInputsMatchLatest as executionReceiptsMatchLatest } from './execution-inputs.js';
17
18
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
18
19
  const REASONS = new Set([
19
- 'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
20
+ 'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'
20
21
  ]);
21
22
  function deny(reasonCode, ...evidence) {
22
23
  return { allowed: false, reasonCode, evidence };
@@ -39,6 +40,9 @@ function canStart(action, facts, trigger) {
39
40
  return deny('TRIGGER_REQUEST_ID_REQUIRED');
40
41
  if (!REASONS.has(trigger.reasonCode))
41
42
  return deny('TRIGGER_REASON_INVALID');
43
+ if (trigger.reasonCode === 'manual-review-supplement' && (action !== 'review-code' || trigger.initiator !== 'human')) {
44
+ return deny('MANUAL_REVIEW_SUPPLEMENT_REQUIRES_HUMAN_REVIEW_CODE');
45
+ }
42
46
  if (trigger.sourceArtifact && !trigger.sourceSha256)
43
47
  return deny('SOURCE_ARTIFACT_HASH_REQUIRED');
44
48
  if (trigger.sourceSha256 && !trigger.sourceArtifact)
@@ -52,8 +56,6 @@ function canStart(action, facts, trigger) {
52
56
  }
53
57
  if (facts.taskState !== 'active')
54
58
  return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
55
- if (invalidationBlocks(facts.invalidation))
56
- return deny('INVALIDATION_INCOMPLETE');
57
59
  if (facts.executionBusy)
58
60
  return deny('EXECUTION_BUSY');
59
61
  const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
@@ -102,7 +104,9 @@ function canStart(action, facts, trigger) {
102
104
  return allow('analysis-review-approved');
103
105
  }
104
106
  if (action === 'review-plan') {
105
- return hasArtifact(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_ARTIFACT_REQUIRED');
107
+ if (!hasArtifact(facts, 'plan'))
108
+ return deny('PLAN_ARTIFACT_REQUIRED');
109
+ return executionInputsMatchLatest(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_INPUT_STALE');
106
110
  }
107
111
  if (action === 'code') {
108
112
  if (trigger.implementationInput) {
@@ -141,11 +145,15 @@ function canStart(action, facts, trigger) {
141
145
  return allow('plan-review-approved');
142
146
  }
143
147
  if (action === 'review-code') {
148
+ if (hasArtifact(facts, 'code') && !executionInputsMatchLatest(facts, 'code'))
149
+ return deny('CODE_INPUT_STALE');
144
150
  return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
145
151
  }
146
152
  if (action === 'manual-validation' || action === 'validation-run') {
147
153
  if (!hasArtifact(facts, 'review-code'))
148
154
  return deny('CODE_REVIEW_REQUIRED');
155
+ if (!executionInputsMatchLatest(facts, 'code'))
156
+ return deny('CODE_INPUT_STALE');
149
157
  if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
150
158
  return deny('CODE_REVIEW_NOT_LATEST');
151
159
  if (!facts.reviewCodeIdentityValid)
@@ -159,13 +167,22 @@ function canStart(action, facts, trigger) {
159
167
  return deny('CAPABILITY_ACTION_UNKNOWN');
160
168
  }
161
169
  function latestArtifact(names) {
162
- return names.map(parseArtifactName).filter((identity) => identity !== null)
163
- .sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
170
+ return names.at(-1) ?? null;
164
171
  }
165
172
  function reviewMatchesLatest(facts, source, review) {
166
173
  const latestSource = latestArtifact(facts.artifacts[source] ?? []);
167
174
  return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
168
175
  }
176
+ function executionInputsMatchLatest(facts, stage) {
177
+ if (!facts.pathState || facts.pathState.status !== 'valid' || facts.executionInputs === undefined)
178
+ return true;
179
+ return executionReceiptsMatchLatest(stage, facts.pathState, {
180
+ analysis: latestArtifact(facts.artifacts.analysis ?? []) ?? undefined,
181
+ 'review-analysis': latestArtifact(facts.artifacts['review-analysis'] ?? []) ?? undefined,
182
+ plan: latestArtifact(facts.artifacts.plan ?? []) ?? undefined,
183
+ 'review-plan': latestArtifact(facts.artifacts['review-plan'] ?? []) ?? undefined
184
+ }, facts.executionInputs[stage] ?? []);
185
+ }
169
186
  function qualificationCandidateSnapshotMatches(audit, qualification) {
170
187
  const current = new Map(qualification.candidates.map((candidate) => [candidate.candidateId, candidate]));
171
188
  return audit.candidateQualifications.length === current.size && audit.candidateQualifications.every((row) => {
@@ -201,6 +218,8 @@ function recommendNext(facts) {
201
218
  }
202
219
  if (stage === 'plan' && !hasArtifact(facts, stage))
203
220
  return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
221
+ if (stage === 'plan' && !executionInputsMatchLatest(facts, stage))
222
+ return { action: stage, reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
204
223
  if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
205
224
  if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
206
225
  return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
@@ -211,6 +230,8 @@ function recommendNext(facts) {
211
230
  }
212
231
  if (stage === 'code' && !hasArtifact(facts, stage))
213
232
  return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
233
+ if (stage === 'code' && !executionInputsMatchLatest(facts, stage))
234
+ return { action: stage, reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
214
235
  if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
215
236
  if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
216
237
  return facts.resolvedHumanDecisions.code === 'implementation'
@@ -234,6 +255,8 @@ function recommendNext(facts) {
234
255
  }
235
256
  if (!hasArtifact(facts, 'plan'))
236
257
  return { action: 'plan', reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
258
+ if (!executionInputsMatchLatest(facts, 'plan'))
259
+ return { action: 'plan', reasonCode: 'PLAN_INPUT_STALE', evidence: ['latest plan does not record the current selected analysis inputs'] };
237
260
  if (!hasArtifact(facts, 'review-plan') || !reviewMatchesLatest(facts, 'plan', 'review-plan')) {
238
261
  return { action: 'review-plan', reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
239
262
  }
@@ -242,6 +265,8 @@ function recommendNext(facts) {
242
265
  }
243
266
  if (!hasArtifact(facts, 'code'))
244
267
  return { action: 'code', reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
268
+ if (!executionInputsMatchLatest(facts, 'code'))
269
+ return { action: 'code', reasonCode: 'CODE_INPUT_STALE', evidence: ['latest code does not record the current selected execution inputs'] };
245
270
  if (!hasArtifact(facts, 'review-code') || !reviewMatchesLatest(facts, 'code', 'review-code')) {
246
271
  return { action: 'review-code', reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
247
272
  }
@@ -253,9 +278,6 @@ function recommendNext(facts) {
253
278
  function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBusy = false) {
254
279
  try {
255
280
  const metadata = parseTypedTaskFrontmatter(content);
256
- const invalidation = parseInvalidationDocument(content);
257
- if (!invalidation.ok)
258
- return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalidation.message };
259
281
  const rework = parseReworkIntentDocument(content);
260
282
  if (!rework.ok)
261
283
  return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: rework.message };
@@ -276,22 +298,38 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
276
298
  const family = parseArtifactName(name)?.family;
277
299
  return artifactFamilies.find((candidate) => candidate === family) ?? null;
278
300
  };
279
- const activeFiles = files.filter((name) => {
280
- const family = familyFor(name);
281
- return !family || !isArtifactInvalidated(invalidation.document, family, name);
282
- });
283
301
  const allArtifactHashes = {};
284
302
  for (const name of files)
285
303
  allArtifactHashes[name] = sha256File(path.join(taskDir, name));
286
304
  const artifactHashes = {};
287
- for (const name of activeFiles)
305
+ for (const name of files)
288
306
  artifactHashes[name] = allArtifactHashes[name];
307
+ const completedByFamily = new Map();
308
+ for (const family of artifactFamilies) {
309
+ const inventory = inspectArtifactDirectory(taskDir, family);
310
+ if (inventory.status !== 'ready')
311
+ continue;
312
+ completedByFamily.set(family, inventory.completed.map((artifact) => artifact.name));
313
+ }
314
+ const completedFiles = new Set([...completedByFamily.values()].flat());
315
+ const activeFiles = [
316
+ ...files.filter((name) => !familyFor(name)),
317
+ ...artifactFamilies.flatMap((family) => completedByFamily.get(family) ?? [])
318
+ ];
289
319
  const artifacts = Object.fromEntries(artifactFamilies.map((family) => [
290
320
  family, activeFiles.filter((name) => familyFor(name) === family)
291
321
  ]));
292
- const staleArtifacts = Object.fromEntries(artifactFamilies.map((family) => [
293
- family, files.filter((name) => familyFor(name) === family && isArtifactInvalidated(invalidation.document, family, name))
294
- ]));
322
+ const receipts = parseArtifactReceipts(content).rows;
323
+ const executionInputs = {};
324
+ for (const family of ['plan', 'code']) {
325
+ const output = latestArtifact(artifacts[family] ?? []);
326
+ if (!output)
327
+ continue;
328
+ executionInputs[family] = receipts.filter((receipt) => receipt.output === output
329
+ && receipt.event === `${family}.completed`
330
+ && completedFiles.has(receipt.input)
331
+ && artifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => receipt.input);
332
+ }
295
333
  const qualification = parseTaskQualification(content);
296
334
  if (!qualification.ok)
297
335
  return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: qualification.message };
@@ -362,9 +400,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
362
400
  reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
363
401
  }
364
402
  const reworkClassificationRequired = [];
365
- const receipts = parseArtifactReceipts(content).rows;
366
403
  for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']]) {
367
- const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && files.includes(receipt.output)
404
+ const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && completedFiles.has(receipt.output)
368
405
  && allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
369
406
  const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
370
407
  const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
@@ -415,8 +452,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
415
452
  }
416
453
  const facts = {
417
454
  taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
418
- staleArtifacts, reviewedInputs, artifactHashes,
419
- invalidation: invalidation.document, reworkIntents: rework.intents,
455
+ reviewedInputs, executionInputs, artifactHashes,
456
+ reworkIntents: rework.intents,
420
457
  unresolvedLedger,
421
458
  executionBusy: executionBusy || hasOpenLifecycleExecution(content),
422
459
  recommendedAction: null,
@@ -11,8 +11,6 @@ import { getOpenWorkflowWarnings, formatWorkflowWarningSummary } from '../workfl
11
11
  import { parseActivityLog, pairEntries, startedBackedRows } from '../activity-log.js';
12
12
  import { readRun } from '../orchestration.js';
13
13
  import { statusCard } from '../../server/display.js';
14
- import { invalidationBlocks, parseInvalidationDocument } from '../invalidation.js';
15
- import { parseReworkIntentDocument } from '../rework-intent.js';
16
14
  import { buildLifecycleFacts, recommendNext } from '../capabilities.js';
17
15
  const USAGE = `Usage: ai task status [--task <ref> | -t <ref>]
18
16
 
@@ -320,11 +318,6 @@ function renderStatus(model) {
320
318
  else {
321
319
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
322
320
  }
323
- lines.push('', 'Invalidation', ...renderPairs([
324
- ['status', model.invalidation?.status ?? 'unknown'],
325
- ['processed', `${model.invalidation?.processed ?? 0}/${model.invalidation?.total ?? 0}`],
326
- ['pending_intents', String(model.invalidation?.pendingIntents ?? 0)]
327
- ]));
328
321
  lines.push('', 'Recommendation', ...renderPairs([
329
322
  ['path', model.lifecyclePath ?? DASH],
330
323
  ['action', model.recommendation ?? DASH],
@@ -374,10 +367,6 @@ function renderStatus(model) {
374
367
  return lines;
375
368
  }
376
369
  function modelTone(model) {
377
- if (model.invalidation?.status === 'invalid')
378
- return 'danger';
379
- if (model.invalidation?.status === 'pending' || model.invalidation?.status === 'in-progress')
380
- return 'warning';
381
370
  if (model.orchestration.status === 'paused')
382
371
  return 'warning';
383
372
  if (model.orchestration.status === 'running')
@@ -399,21 +388,6 @@ function buildFromResolved(input) {
399
388
  const run = input.run ?? makeRunner(input.repoRoot);
400
389
  const artifacts = enumerateArtifacts(input.taskDir);
401
390
  const workflow = collectWorkflow(content, input.now);
402
- const invalidation = parseInvalidationDocument(content);
403
- const rework = parseReworkIntentDocument(content);
404
- const pendingIntents = rework.ok ? rework.intents.filter((intent) => intent.status === 'pending').length : 0;
405
- const invalidationInfo = !invalidation.ok
406
- ? { status: 'invalid', processed: 0, total: 0, pendingIntents }
407
- : {
408
- status: invalidation.present
409
- ? invalidationBlocks(invalidation.document)
410
- ? invalidation.document.operations.some((operation) => operation.status === 'in-progress') ? 'in-progress' : 'pending'
411
- : 'completed'
412
- : 'none',
413
- processed: invalidation.document.operations.reduce((sum, operation) => sum + operation.processed, 0),
414
- total: invalidation.document.operations.reduce((sum, operation) => sum + operation.total, 0),
415
- pendingIntents
416
- };
417
391
  const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
418
392
  const next = facts.ok ? recommendNext(facts.facts) : null;
419
393
  const recommendation = next?.action ?? DASH;
@@ -427,7 +401,6 @@ function buildFromResolved(input) {
427
401
  workflow,
428
402
  runtime: collectRuntime(input.taskDir, workflow, run),
429
403
  orchestration: collectOrchestration(input.taskDir),
430
- invalidation: invalidationInfo,
431
404
  recommendation,
432
405
  recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
433
406
  lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,
@@ -6,19 +6,34 @@ function gitText(repoRoot, args) {
6
6
  stdio: ['ignore', 'pipe', 'pipe']
7
7
  }).trim();
8
8
  }
9
+ function gitOutput(repoRoot, args) {
10
+ return execFileSync('git', [...args], {
11
+ cwd: repoRoot,
12
+ encoding: 'utf8',
13
+ stdio: ['ignore', 'pipe', 'pipe']
14
+ });
15
+ }
16
+ function commitPaths(repoRoot, head, pathspec) {
17
+ const args = ['diff-tree', '--no-commit-id', '--name-only', '-r', '-z', head];
18
+ if (pathspec && pathspec.length > 0)
19
+ args.push('--', ...pathspec);
20
+ return gitOutput(repoRoot, args).split('\0').filter(Boolean).sort();
21
+ }
22
+ function canonicalMessage(message) {
23
+ return message.replace(/\r\n/g, '\n').replace(/\n+$/, '');
24
+ }
9
25
  function checkpointCommitMatches(repoRoot, intent, head) {
10
26
  try {
11
27
  const parent = gitText(repoRoot, ['rev-parse', `${head}^`]);
12
28
  const tree = gitText(repoRoot, ['rev-parse', `${head}^{tree}`]);
13
- const message = gitText(repoRoot, ['show', '-s', '--format=%s', head]);
14
- const changed = gitText(repoRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', head])
15
- .split('\n')
16
- .filter(Boolean)
17
- .sort();
29
+ const message = canonicalMessage(gitOutput(repoRoot, ['show', '-s', '--format=%B', head]));
30
+ const changed = commitPaths(repoRoot, head);
31
+ const selected = commitPaths(repoRoot, head, intent.paths);
18
32
  return parent === intent.expectedHead
19
33
  && tree === intent.expectedTree
20
- && message === intent.message
21
- && JSON.stringify(changed) === JSON.stringify([...intent.paths].sort());
34
+ && message === canonicalMessage(intent.message)
35
+ && changed.length > 0
36
+ && JSON.stringify(changed) === JSON.stringify(selected);
22
37
  }
23
38
  catch {
24
39
  return false;
@@ -1,11 +1,18 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- function intentRoot(repoRoot) {
5
- return path.join(repoRoot, '.agents', 'workspace', '.task-commit-intents');
4
+ const MAX_INTENT_SIZE = 1024 * 1024;
5
+ const TASK_ID_PATTERN = /^TASK-\d{8}-\d{6}$/;
6
+ function intentPath(taskDir) {
7
+ return path.join(taskDir, '.checkpoint-intent.json');
6
8
  }
7
- function intentPath(repoRoot, taskId) {
8
- return path.join(intentRoot(repoRoot), `${taskId}.json`);
9
+ function assertTaskDirectory(taskDir, taskId) {
10
+ if (!TASK_ID_PATTERN.test(taskId) || path.basename(path.resolve(taskDir)) !== taskId) {
11
+ throw new Error('COMMIT_INTENT_INVALID: task directory does not match task ID');
12
+ }
13
+ const stat = fs.statSync(taskDir);
14
+ if (!stat.isDirectory())
15
+ throw new Error('COMMIT_INTENT_INVALID: task directory is not a directory');
9
16
  }
10
17
  function checkpointIntentDigest(identity) {
11
18
  return createHash('sha256').update(JSON.stringify({
@@ -23,8 +30,13 @@ function isIntent(value) {
23
30
  if (!value || typeof value !== 'object' || Array.isArray(value))
24
31
  return false;
25
32
  const intent = value;
33
+ const validState = ['prepared', 'committed', 'synced'].includes(String(intent.state));
34
+ const validHead = intent.committedHead === null
35
+ ? intent.state === 'prepared'
36
+ : typeof intent.committedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.committedHead)
37
+ && intent.state !== 'prepared';
26
38
  return intent.version === 1
27
- && typeof intent.taskId === 'string' && intent.taskId.length > 0
39
+ && typeof intent.taskId === 'string' && TASK_ID_PATTERN.test(intent.taskId)
28
40
  && typeof intent.branch === 'string' && intent.branch.length > 0
29
41
  && intent.mode === 'local'
30
42
  && typeof intent.expectedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.expectedHead)
@@ -33,28 +45,57 @@ function isIntent(value) {
33
45
  && typeof intent.message === 'string' && intent.message.length > 0
34
46
  && Number.isSafeInteger(intent.round) && Number(intent.round) > 0
35
47
  && typeof intent.digest === 'string' && /^[a-f0-9]{64}$/.test(intent.digest)
36
- && ['prepared', 'committed', 'synced'].includes(String(intent.state))
37
- && (intent.committedHead === null || (typeof intent.committedHead === 'string' && /^[a-f0-9]{40,64}$/.test(intent.committedHead)))
48
+ && validState && validHead
38
49
  && typeof intent.createdAt === 'string' && intent.createdAt.length > 0
39
50
  && typeof intent.updatedAt === 'string' && intent.updatedAt.length > 0
40
51
  && intent.digest === checkpointIntentDigest(intent);
41
52
  }
42
- function readCheckpointIntent(repoRoot, taskId) {
43
- const target = intentPath(repoRoot, taskId);
44
- if (!fs.existsSync(target))
45
- return null;
46
- const value = JSON.parse(fs.readFileSync(target, 'utf8'));
47
- if (!isIntent(value))
48
- throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
49
- return value;
53
+ function invalidIntent() {
54
+ throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
55
+ }
56
+ function readCheckpointIntent(taskDir, taskId) {
57
+ assertTaskDirectory(taskDir, taskId);
58
+ const target = intentPath(taskDir);
59
+ let before;
60
+ try {
61
+ before = fs.lstatSync(target);
62
+ }
63
+ catch (error) {
64
+ if (error.code === 'ENOENT')
65
+ return null;
66
+ throw error;
67
+ }
68
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_INTENT_SIZE)
69
+ return invalidIntent();
70
+ let descriptor;
71
+ try {
72
+ const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
73
+ descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
74
+ const opened = fs.fstatSync(descriptor);
75
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
76
+ || opened.size !== before.size || opened.size > MAX_INTENT_SIZE)
77
+ return invalidIntent();
78
+ const value = JSON.parse(fs.readFileSync(descriptor, 'utf8'));
79
+ if (!isIntent(value) || value.taskId !== taskId)
80
+ return invalidIntent();
81
+ return value;
82
+ }
83
+ catch (error) {
84
+ if (error instanceof Error && error.message.startsWith('COMMIT_INTENT_INVALID:'))
85
+ throw error;
86
+ return invalidIntent();
87
+ }
88
+ finally {
89
+ if (descriptor !== undefined)
90
+ fs.closeSync(descriptor);
91
+ }
50
92
  }
51
- function writeCheckpointIntent(repoRoot, value) {
93
+ function writeCheckpointIntent(taskDir, value) {
94
+ assertTaskDirectory(taskDir, value.taskId);
52
95
  if (!isIntent(value))
53
- throw new Error('COMMIT_INTENT_INVALID: checkpoint intent schema is invalid');
54
- const directory = intentRoot(repoRoot);
55
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
56
- const target = intentPath(repoRoot, value.taskId);
57
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
96
+ return invalidIntent();
97
+ const target = intentPath(taskDir);
98
+ const temporary = `${target}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
58
99
  fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
59
100
  try {
60
101
  fs.renameSync(temporary, target);
@@ -75,10 +116,10 @@ function updateCheckpointIntent(value, patch) {
75
116
  throw new Error('COMMIT_INTENT_INVALID: committed intent requires a committed head');
76
117
  return next;
77
118
  }
78
- function removeCheckpointIntent(repoRoot, taskId) {
79
- const target = intentPath(repoRoot, taskId);
119
+ function removeCheckpointIntent(taskDir, taskId) {
120
+ assertTaskDirectory(taskDir, taskId);
80
121
  try {
81
- fs.unlinkSync(target);
122
+ fs.unlinkSync(intentPath(taskDir));
82
123
  }
83
124
  catch (error) {
84
125
  if (error.code !== 'ENOENT')
@@ -96,5 +137,5 @@ function sameCheckpointIntent(left, right) {
96
137
  && left.round === right.round
97
138
  && JSON.stringify(left.paths) === JSON.stringify(right.paths);
98
139
  }
99
- export { checkpointIntentDigest, intentPath, intentRoot, isIntent, readCheckpointIntent, removeCheckpointIntent, sameCheckpointIntent, updateCheckpointIntent, writeCheckpointIntent };
140
+ export { checkpointIntentDigest, intentPath, isIntent, readCheckpointIntent, removeCheckpointIntent, sameCheckpointIntent, updateCheckpointIntent, writeCheckpointIntent };
100
141
  //# sourceMappingURL=commit-intent.js.map
@@ -206,7 +206,7 @@ function executeUnlocked(input, task, mode) {
206
206
  ? (() => {
207
207
  const identity = checkpointIdentity(input, task, delivery);
208
208
  try {
209
- pendingIntent = readCheckpointIntent(task.repoRoot, task.taskId);
209
+ pendingIntent = readCheckpointIntent(task.taskDir, task.taskId);
210
210
  }
211
211
  catch (error) {
212
212
  return {
@@ -215,6 +215,11 @@ function executeUnlocked(input, task, mode) {
215
215
  };
216
216
  }
217
217
  if (pendingIntent && !sameCheckpointIntent(pendingIntent, identity)) {
218
+ if (pendingIntent.state !== 'prepared')
219
+ return {
220
+ status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
221
+ error: { code: 'COMMIT_INTENT_INVALID', message: 'A committed checkpoint intent must be recovered with its original request' }
222
+ };
218
223
  const timestamp = checkpointTimestamp();
219
224
  pendingIntent = {
220
225
  version: 1, ...identity,
@@ -222,7 +227,7 @@ function executeUnlocked(input, task, mode) {
222
227
  state: 'prepared', committedHead: null, createdAt: timestamp, updatedAt: timestamp
223
228
  };
224
229
  try {
225
- writeCheckpointIntent(task.repoRoot, pendingIntent);
230
+ writeCheckpointIntent(task.taskDir, pendingIntent);
226
231
  }
227
232
  catch (error) {
228
233
  return {
@@ -231,6 +236,12 @@ function executeUnlocked(input, task, mode) {
231
236
  };
232
237
  }
233
238
  }
239
+ if (pendingIntent && pendingIntent.state !== 'prepared'
240
+ && !checkpointCommitMatches(task.repoRoot, pendingIntent, pendingIntent.committedHead))
241
+ return {
242
+ status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
243
+ error: { code: 'COMMIT_INTENT_INVALID', message: 'Committed checkpoint intent does not match its Git commit' }
244
+ };
234
245
  if (!pendingIntent) {
235
246
  const timestamp = checkpointTimestamp();
236
247
  pendingIntent = {
@@ -238,7 +249,7 @@ function executeUnlocked(input, task, mode) {
238
249
  committedHead: null, createdAt: timestamp, updatedAt: timestamp
239
250
  };
240
251
  try {
241
- writeCheckpointIntent(task.repoRoot, pendingIntent);
252
+ writeCheckpointIntent(task.taskDir, pendingIntent);
242
253
  }
243
254
  catch (error) {
244
255
  return {
@@ -264,7 +275,7 @@ function executeUnlocked(input, task, mode) {
264
275
  pendingIntent = updateCheckpointIntent(pendingIntent, {
265
276
  state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
266
277
  });
267
- writeCheckpointIntent(task.repoRoot, pendingIntent);
278
+ writeCheckpointIntent(task.taskDir, pendingIntent);
268
279
  return {
269
280
  status: 'no-op', changed: false, snapshot: inspected.snapshot,
270
281
  operations: [{ name: 'commit', status: 'no-op' }], error: null
@@ -284,10 +295,10 @@ function executeUnlocked(input, task, mode) {
284
295
  pendingIntent = updateCheckpointIntent(pendingIntent, {
285
296
  state: 'committed', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
286
297
  });
287
- writeCheckpointIntent(task.repoRoot, pendingIntent);
298
+ writeCheckpointIntent(task.taskDir, pendingIntent);
288
299
  }
289
300
  else if (created.status === 'no-op') {
290
- removeCheckpointIntent(task.repoRoot, task.taskId);
301
+ removeCheckpointIntent(task.taskDir, task.taskId);
291
302
  pendingIntent = null;
292
303
  }
293
304
  return created;
@@ -377,8 +388,8 @@ function executeUnlocked(input, task, mode) {
377
388
  const synced = updateCheckpointIntent(pendingIntent, {
378
389
  state: 'synced', committedHead: checkpointCommittedHead, updatedAt: checkpointTimestamp()
379
390
  });
380
- writeCheckpointIntent(task.repoRoot, synced);
381
- removeCheckpointIntent(task.repoRoot, task.taskId);
391
+ writeCheckpointIntent(task.taskDir, synced);
392
+ removeCheckpointIntent(task.taskDir, task.taskId);
382
393
  }
383
394
  }
384
395
  return {
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import { canonicalSemanticDigest } from './artifact-operations.js';
3
- import { inspectActivityLog } from './activity-log.js';
3
+ import { inspectActivityLog, pairEntries } from './activity-log.js';
4
4
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
5
  import { parseArtifactName } from './artifact-name.js';
6
6
  import { sha256File } from './artifact-receipts.js';
@@ -39,18 +39,40 @@ export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
39
39
  }
40
40
  }
41
41
  export function hasArtifactCompletionLog(taskContent, artifact, event) {
42
+ return artifactCompletionOrder(taskContent, artifact, event) !== null;
43
+ }
44
+ /** Returns the stable Activity Log position of a paired completion row. */
45
+ export function artifactCompletionOrder(taskContent, artifact, event) {
42
46
  const identity = parseArtifactName(artifact);
43
47
  if (!identity)
44
- return false;
45
- const action = event === 'review-code.completed' ? 'Review Code'
46
- : event === 'review-plan.completed' ? 'Review Plan'
47
- : event === 'review-analysis.completed' ? 'Review Analysis' : null;
48
- if (!action)
49
- return false;
48
+ return null;
49
+ const family = event === 'analysis.completed' || event === 'analyze.completed' ? 'analysis'
50
+ : event === 'review-analysis.completed' ? 'review-analysis'
51
+ : event === 'plan.completed' ? 'plan'
52
+ : event === 'review-plan.completed' ? 'review-plan'
53
+ : event === 'code.completed' ? 'code'
54
+ : event === 'review-code.completed' ? 'review-code' : null;
55
+ if (!family || family !== identity.family)
56
+ return null;
57
+ const action = family === 'analysis' ? 'Analyze Task'
58
+ : family === 'review-analysis' ? 'Review Analysis'
59
+ : family === 'plan' ? 'Plan Task'
60
+ : family === 'review-plan' ? 'Review Plan'
61
+ : family === 'code' ? 'Code Task' : 'Review Code';
50
62
  const { section, invalidEntries } = inspectActivityLog(taskContent);
51
63
  if (!section || invalidEntries.length > 0)
52
- return false;
53
- const expected = `${action} (Round ${identity.round})`;
54
- return section.entries.some((entry) => entry.step === expected && entry.note.includes(`→ ${artifact}`));
64
+ return null;
65
+ const ordered = section.entries.map((entry, sourceOrder) => ({ entry, sourceOrder }))
66
+ .sort((left, right) => Date.parse(left.entry.time.replace(' ', 'T')) - Date.parse(right.entry.time.replace(' ', 'T')) || left.sourceOrder - right.sourceOrder);
67
+ const entries = ordered.map(({ entry }) => entry);
68
+ const roundPattern = new RegExp(`^${action} \\(Round ${identity.round}(?:, [^)]+)?\\)$`);
69
+ const row = pairEntries(entries).find((candidate) => candidate.started !== ''
70
+ && candidate.done !== '' && roundPattern.test(candidate.step)
71
+ && candidate.note.includes(`→ ${artifact}`));
72
+ if (!row)
73
+ return null;
74
+ const doneIndex = ordered.findIndex(({ entry }) => entry.time === row.done
75
+ && entry.step === row.step && entry.note === row.note);
76
+ return doneIndex < 0 ? null : doneIndex;
55
77
  }
56
78
  //# sourceMappingURL=completion-facts.js.map
@@ -4,7 +4,7 @@ import { normalizeAgentToken } from '../agent-clients/tokens.js';
4
4
  import { isAgentClientId } from '../agent-clients/types.js';
5
5
  import { getAgentClientAdapter } from '../agent-clients/registry.js';
6
6
  import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, awaitOrchestrationDelegationActivation, beginOrResumeOrchestration, dispatchOrchestrationDelegation, OrchestrationStateError, pauseOrchestration, prepareOrchestrationDelegation, recoverPreparedOrchestrationDelegation, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusOrchestration } from './orchestration.js';
7
- import { applyTaskFinalization, commitPreparedTaskFinalization } from './finalization.js';
7
+ import { applyTaskFinalization } from './finalization.js';
8
8
  import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
9
9
  import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
10
10
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
@@ -274,7 +274,6 @@ export function createSandboxExecutorExecutionContext(params) {
274
274
  generation: params.generation,
275
275
  manifestPath: absolute('manifestPath', params.manifestPath),
276
276
  requestId: params.requestId,
277
- handoffDirectory: absolute('handoffDirectory', params.handoffDirectory ?? path.join(params.worktreeRoot, '.agent-infra-handoff')),
278
277
  ...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
279
278
  ...(params.lifecycleRecoveryAttestation === undefined ? {} : {
280
279
  lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
@@ -302,7 +301,6 @@ export function assertTaskControlExecutionContext(context) {
302
301
  absolute('worktreeRoot', context.worktreeRoot);
303
302
  absolute('runtimeDir', context.runtimeDir);
304
303
  absolute('manifestPath', context.manifestPath);
305
- absolute('handoffDirectory', context.handoffDirectory);
306
304
  if (path.resolve(context.runtimeDir) !== path.join(path.dirname(path.resolve(context.manifestPath)), 'runtime')) {
307
305
  invalidContext('runtimeDir is not bound to the manifest control root');
308
306
  }
@@ -434,12 +432,10 @@ export function dispatchTaskControlOperation(context, operation) {
434
432
  }
435
433
  if (operation.family === 'task-finalization') {
436
434
  operationTaskId(context, operation.request.taskRef);
437
- const finalize = context.source === 'sandbox-executor' ? commitPreparedTaskFinalization : applyTaskFinalization;
438
- return finalize(operation.request, {
435
+ return applyTaskFinalization(operation.request, {
439
436
  repoRoot: context.repoRoot,
440
437
  ...(context.source === 'sandbox-executor' ? {
441
438
  controlBinding: { generation: context.generation, requestId: context.requestId },
442
- handoffDirectory: context.handoffDirectory,
443
439
  manifestPath: context.manifestPath
444
440
  } : {}),
445
441
  preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)