@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
@@ -0,0 +1,150 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
4
+ export const SANDBOX_CONTROL_STATUS_MOUNT = '/run/agent-infra/control-status';
5
+ const TASK_MARKER_KEYS = [
6
+ 'AGENT_INFRA_SANDBOX',
7
+ 'AGENT_INFRA_TASK_ID',
8
+ 'AGENT_INFRA_CONTROL_TOKEN',
9
+ 'AGENT_INFRA_CONTROL_GENERATION',
10
+ 'AGENT_INFRA_CONTROL_DIR',
11
+ 'AGENT_INFRA_CONTROL_STATUS_DIR',
12
+ 'AGENT_INFRA_RUNTIME_DIR'
13
+ ];
14
+ const TASK_CONTROL_MARKER_KEYS = [
15
+ 'AGENT_INFRA_CONTROL_TOKEN',
16
+ 'AGENT_INFRA_CONTROL_GENERATION',
17
+ 'AGENT_INFRA_CONTROL_DIR',
18
+ 'AGENT_INFRA_CONTROL_STATUS_DIR'
19
+ ];
20
+ const TASK_CONTROL_CONFIG_KEYS = [
21
+ ...TASK_CONTROL_MARKER_KEYS,
22
+ 'AGENT_INFRA_CONTROL_ROOT_ID'
23
+ ];
24
+ function nativeDirectoryProbe(candidate) {
25
+ if (process.platform === 'linux') {
26
+ try {
27
+ // Linux sandboxes use this fixed mount as a host/sandbox trust anchor.
28
+ // Do not let a preload replace the ordinary node:fs probe and select host routing.
29
+ const binding = process.binding('fs');
30
+ if (!Function.prototype.toString.call(binding.internalModuleStat).includes('[native code]'))
31
+ return 'unknown';
32
+ const result = binding.internalModuleStat(candidate);
33
+ if (result === 1)
34
+ return 'present';
35
+ if (result === -2)
36
+ return 'absent';
37
+ return 'unknown';
38
+ }
39
+ catch {
40
+ return 'unknown';
41
+ }
42
+ }
43
+ try {
44
+ const stat = fs.lstatSync(candidate);
45
+ return stat.isDirectory() && !stat.isSymbolicLink() ? 'present' : 'unknown';
46
+ }
47
+ catch (error) {
48
+ if (error && typeof error === 'object' && 'code' in error
49
+ && (error.code === 'ENOENT' || error.code === 'ENOTDIR'))
50
+ return 'absent';
51
+ return 'unknown';
52
+ }
53
+ }
54
+ export function sandboxTaskMarkerState(env) {
55
+ const present = (key) => Boolean(env[key]);
56
+ const sandbox = env.AGENT_INFRA_SANDBOX === '1';
57
+ const taskId = present('AGENT_INFRA_TASK_ID');
58
+ const controls = TASK_CONTROL_MARKER_KEYS.every(present);
59
+ const runtime = present('AGENT_INFRA_RUNTIME_DIR');
60
+ const any = TASK_MARKER_KEYS.some(present);
61
+ if (!any)
62
+ return 'none';
63
+ if (!sandbox || !controls)
64
+ return 'incomplete';
65
+ if (taskId && runtime)
66
+ return 'task-bound';
67
+ if (!taskId && !runtime)
68
+ return 'branch-only';
69
+ return 'incomplete';
70
+ }
71
+ export function hasTaskBoundMarker(env = process.env) {
72
+ return sandboxTaskMarkerState(env) === 'task-bound';
73
+ }
74
+ export function hasSandboxControlMarker(env = process.env) {
75
+ return TASK_CONTROL_MARKER_KEYS.some((key) => Boolean(env[key]));
76
+ }
77
+ export function resolveSandboxControlTransport(env = process.env, options = {}) {
78
+ const markerState = sandboxTaskMarkerState(env);
79
+ const hasAnyMarker = markerState !== 'none'
80
+ || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
81
+ || Boolean(env.AGENT_INFRA_EXECUTOR_MANIFEST);
82
+ const fixedStatusDir = options.statusMountPath ?? SANDBOX_CONTROL_STATUS_MOUNT;
83
+ const fixedStatusProbe = path.isAbsolute(fixedStatusDir)
84
+ ? nativeDirectoryProbe(fixedStatusDir) : 'absent';
85
+ if (fixedStatusProbe === 'unknown') {
86
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
87
+ }
88
+ const fixedStatusMounted = fixedStatusProbe === 'present';
89
+ if (!hasAnyMarker) {
90
+ return fixedStatusMounted
91
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' }
92
+ : { kind: 'direct-host', reasonCode: null };
93
+ }
94
+ if (markerState === 'incomplete') {
95
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
96
+ }
97
+ const hasCompleteConfig = TASK_CONTROL_CONFIG_KEYS.every((key) => Boolean(env[key]));
98
+ if (!hasCompleteConfig) {
99
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
100
+ }
101
+ const configuredStatusDir = env.AGENT_INFRA_CONTROL_STATUS_DIR;
102
+ const configuredStatusProbe = configuredStatusDir && path.isAbsolute(configuredStatusDir)
103
+ ? nativeDirectoryProbe(configuredStatusDir) : 'absent';
104
+ if (configuredStatusProbe === 'unknown') {
105
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_UNAVAILABLE' };
106
+ }
107
+ const statusDir = configuredStatusDir && path.isAbsolute(configuredStatusDir)
108
+ ? configuredStatusProbe === 'present' ? configuredStatusDir : null
109
+ : fixedStatusMounted ? fixedStatusDir : null;
110
+ const statusMounted = statusDir !== null;
111
+ if (env.AGENT_INFRA_EXECUTOR_MANIFEST || env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING) {
112
+ return { kind: 'fail-closed', reasonCode: 'TASK_CONTROL_TRANSPORT_INVALID' };
113
+ }
114
+ const taskBound = Boolean(env.AGENT_INFRA_TASK_ID);
115
+ const runtime = Boolean(env.AGENT_INFRA_RUNTIME_DIR);
116
+ if (statusMounted) {
117
+ let sentinel;
118
+ try {
119
+ sentinel = readSandboxControlIdentitySentinel(statusDir);
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : '';
123
+ return { kind: 'fail-closed', reasonCode: message.endsWith('MISSING')
124
+ ? 'SANDBOX_CONTROL_IDENTITY_MISSING'
125
+ : 'SANDBOX_CONTROL_IDENTITY_MALFORMED' };
126
+ }
127
+ if (sentinel.generation !== env.AGENT_INFRA_CONTROL_GENERATION) {
128
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH' };
129
+ }
130
+ if (sentinel.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
131
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH' };
132
+ }
133
+ if (sentinel.mode === 'task-bound'
134
+ && (sentinel.taskId !== env.AGENT_INFRA_TASK_ID || !runtime)) {
135
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
136
+ }
137
+ if (sentinel.mode === 'branch-only' && (taskBound || runtime)) {
138
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH' };
139
+ }
140
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
141
+ }
142
+ return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' };
143
+ }
144
+ export function isSandbox(env = process.env, options = {}) {
145
+ const decision = resolveSandboxControlTransport(env, options);
146
+ if (decision.kind === 'fail-closed')
147
+ throw new Error(decision.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID');
148
+ return decision.kind !== 'direct-host';
149
+ }
150
+ //# sourceMappingURL=environment.js.map
@@ -567,6 +567,13 @@ function expectedMounts(params) {
567
567
  hostPaths: toolConfigDirCandidates(tool, config.project, branch),
568
568
  expectedRW: true
569
569
  }));
570
+ const hostState = tools.flatMap((tool) => (tool.hostStateMounts ?? []).map(({ hostSubdir, containerSubpath }) => ({
571
+ path: path.posix.join(tool.containerMount, containerSubpath),
572
+ expectedType: 'bind',
573
+ hostPaths: toolConfigDirCandidates(tool, config.project, branch)
574
+ .map((candidate) => path.join(candidate, hostSubdir)),
575
+ expectedRW: true
576
+ })));
570
577
  const staging = tools.flatMap((tool) => (tool.tmpfs?.seed ?? []).flatMap((seedEntry, index) => {
571
578
  const stagingPath = `/run/agent-infra/tmpfs-seeds/${tool.id}/${index}`;
572
579
  if (!actualMounts.has(stagingPath))
@@ -583,6 +590,7 @@ function expectedMounts(params) {
583
590
  ...core,
584
591
  ...persistentTools,
585
592
  ...live,
593
+ ...hostState,
586
594
  ...staging,
587
595
  ...tools
588
596
  .filter((tool) => tool.tmpfs)
@@ -19,7 +19,6 @@ import { getProcessStartTime } from '../server/process-state.js';
19
19
  import { releaseStaleShortIdRegistry } from '../task/short-id.js';
20
20
  import { withRepositoryMutationLock } from '../task/task-execution-lock.js';
21
21
  import { fetchSandboxRows } from './commands/list-running.js';
22
- import { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup } from '../task/intermediate-cleanup.js';
23
22
  import { createSandboxControlBindingEvidence } from './task-cleanup.js';
24
23
  import { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectRecoveredWorktree, inspectWorktrees, verifyWorktreePermit } from './worktree-safety.js';
25
24
  function projectToolDirs(config, tools) {
@@ -79,20 +79,6 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
79
79
  }
80
80
  return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
81
81
  }
82
- export function resolveSandboxReentryContext(params) {
83
- if (params.target.workspace.mode !== 'branch-only'
84
- || params.containerWorkspace.mode !== 'task-bound') {
85
- return { workspace: params.target.workspace, reentry: 'standard' };
86
- }
87
- const task = resolveTaskWorkspace(params.containerWorkspace.taskId, params.repoRoot);
88
- if (task.state !== 'completed' || task.branch !== params.target.branch) {
89
- return { workspace: params.target.workspace, reentry: 'standard' };
90
- }
91
- return {
92
- workspace: { mode: 'task-bound', taskId: task.taskId },
93
- reentry: 'completed'
94
- };
95
- }
96
82
  export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
97
83
  if (isRemovedHashShortIdInput(requestedRef)) {
98
84
  throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { resolveTaskWorkspace } from './task-resolver.js';
4
5
  import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
5
6
  export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
6
7
  'active',
@@ -73,15 +74,38 @@ function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot)
73
74
  export function assertSandboxTaskSource(repoRoot, taskId) {
74
75
  if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
75
76
  throw new Error('SANDBOX_TASK_SOURCE_INVALID');
76
- const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
77
- const source = path.join(activeRoot, taskId);
77
+ const activeSource = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
78
+ try {
79
+ const stat = fs.lstatSync(activeSource);
80
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
81
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} must be a real directory`);
82
+ }
83
+ const canonical = fs.realpathSync.native(activeSource);
84
+ const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
85
+ const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
86
+ const relative = path.relative(workspaceRoot, canonical);
87
+ if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
88
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
89
+ }
90
+ return canonical;
91
+ }
92
+ catch (error) {
93
+ if (error.code !== 'ENOENT')
94
+ throw error;
95
+ }
96
+ const task = resolveTaskWorkspace(taskId, repoRoot);
97
+ const source = task.state === 'archive'
98
+ ? path.dirname(path.dirname(task.taskMd))
99
+ : path.dirname(task.taskMd);
78
100
  const stat = fs.lstatSync(source);
79
101
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
80
102
  throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
81
103
  }
82
104
  const canonical = fs.realpathSync.native(source);
83
- if (path.dirname(canonical) !== activeRoot) {
84
- throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the active workspace`);
105
+ const workspaceRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
106
+ const relative = path.relative(workspaceRoot, canonical);
107
+ if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
108
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
85
109
  }
86
110
  return canonical;
87
111
  }
@@ -114,10 +138,13 @@ export function materializeSandboxWorkspaceView(params) {
114
138
  fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
115
139
  }
116
140
  const active = path.join(root, 'active');
141
+ const shortId = params.identity.mode === 'task-bound' && 'shortId' in params.identity
142
+ ? params.identity.shortId
143
+ : null;
117
144
  const registry = {
118
145
  version: 1,
119
- ids: params.identity.mode === 'task-bound'
120
- ? { [params.identity.shortId]: params.identity.taskId }
146
+ ids: shortId !== null && params.identity.mode === 'task-bound'
147
+ ? { [shortId]: params.identity.taskId }
121
148
  : {}
122
149
  };
123
150
  fs.writeFileSync(path.join(active, '.short-ids.json'), `${JSON.stringify(registry)}\n`, {
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecycle.js';
3
4
  import { parseArtifactName } from './artifact-name.js';
4
5
  import { resolveTaskRef } from './resolve-ref.js';
@@ -13,7 +14,7 @@ export function parseArtifactCommand(args) {
13
14
  }
14
15
  const fields = {};
15
16
  const allowed = {
16
- inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
17
+ inspect: ['--family', '--reason-code', '--source-finding', '--source-artifact', '--source-sha256'],
17
18
  init: ['--family', '--artifact', '--locale'],
18
19
  'preflight': ['--family', '--artifact'],
19
20
  'finalize-local': ['--family', '--artifact']
@@ -36,9 +37,13 @@ export function parseArtifactCommand(args) {
36
37
  const locale = fields['--locale'];
37
38
  if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en')
38
39
  throw new Error("option '--locale' must be 'zh-CN' or 'en'");
40
+ const reasonCode = fields['--reason-code'];
41
+ if (reasonCode !== undefined && !['user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'manual-review-supplement', 'retry', 'validation-rerun'].includes(reasonCode)) {
42
+ throw new Error("option '--reason-code' is invalid");
43
+ }
39
44
  return {
40
45
  taskRef, operation: operation, family: fields['--family'] ?? '',
41
- artifact: fields['--artifact'] ?? '', locale,
46
+ artifact: fields['--artifact'] ?? '', locale, reasonCode: reasonCode,
42
47
  sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
43
48
  sourceSha256: fields['--source-sha256']
44
49
  };
@@ -50,6 +55,7 @@ export function executeArtifactCommand(command, options = {}) {
50
55
  if (operation === 'inspect') {
51
56
  const result = resolveArtifactContext(taskRef, family, {
52
57
  repoRoot: options.repoRoot,
58
+ reasonCode: command.reasonCode,
53
59
  sourceFinding: command.sourceFinding,
54
60
  sourceArtifact: command.sourceArtifact,
55
61
  sourceSha256: command.sourceSha256
@@ -74,7 +80,16 @@ export function executeArtifactCommand(command, options = {}) {
74
80
  const input = { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskDir, family: schema.family, artifact };
75
81
  if (operation === 'init') {
76
82
  const context = resolveArtifactContext(taskRef, family, { repoRoot: resolved.repoRoot });
77
- if (context.status !== 'ready' || (context.next?.name !== artifact && !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round))) {
83
+ const existingCandidate = path.join(taskDir, artifact);
84
+ let matchingInitializedSkeleton = false;
85
+ try {
86
+ const existing = fs.readFileSync(existingCandidate, 'utf8');
87
+ matchingInitializedSkeleton = existing.includes(`<!-- artifact-context:${resolved.taskId}:${family}:${parsed.round} -->`);
88
+ }
89
+ catch { /* an absent candidate is created only for the selected identity */ }
90
+ if ((context.status !== 'ready' && !matchingInitializedSkeleton) || (context.next?.name !== artifact
91
+ && !hasOpenArtifactRound(fs.readFileSync(resolved.taskMdPath, 'utf8'), family, parsed.round)
92
+ && !matchingInitializedSkeleton)) {
78
93
  return fail('ARTIFACT_INIT_CONTEXT_INVALID', context.error?.message ?? `artifact '${artifact}' is not the next ${family} artifact`);
79
94
  }
80
95
  return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
@@ -4,15 +4,16 @@ import { ARTIFACT_FAMILY_CATALOG, artifactName, parseArtifactName } from './arti
4
4
  import { resolveTaskRef } from './resolve-ref.js';
5
5
  import { locateActivityLog, pairEntries, startedBackedRows } from './activity-log.js';
6
6
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.js';
7
+ import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
8
  import { parseVerdict } from './review-artifacts.js';
8
9
  import { extractSection, findSectionHeading } from './sections.js';
9
- import { receiptForOutput, sha256File } from './artifact-receipts.js';
10
- import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
10
+ import { receiptForOutput, receiptsForOutput, sha256File } from './artifact-receipts.js';
11
+ import { artifactCompletionOrder, hasArtifactCompletionFact, hasArtifactCompletionLog, parseCompletionFacts } from './completion-facts.js';
11
12
  import { inspectReviewIdentity } from './review-identity.js';
12
- import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
13
13
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
14
14
  import { parseReworkIntentDocument } from './rework-intent.js';
15
15
  import { selectArtifactDisposition } from './artifact-selection.js';
16
+ import { executionInputsMatchLatest } from './execution-inputs.js';
16
17
  const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
17
18
  const ARTIFACT_STEPS = {
18
19
  analysis: 'Analyze Task', 'review-analysis': 'Review Analysis',
@@ -21,28 +22,46 @@ const ARTIFACT_STEPS = {
21
22
  };
22
23
  /** Init, repair and finalization share the same authoritative round identity. */
23
24
  export function hasOpenArtifactRound(content, family, round) {
25
+ return openArtifactRounds(content, family).includes(round);
26
+ }
27
+ function openArtifactRounds(content, family) {
24
28
  const activity = locateActivityLog(content);
25
29
  const step = ARTIFACT_STEPS[family];
26
30
  if (!activity || !step)
27
- return false;
31
+ return [];
28
32
  const qualifier = family === 'code'
29
33
  ? '(?:, (?:fix for review-code(?:-r(?:[2-9]|[1-9]\\d+))?\\.md|decision II-[1-9]\\d*))?'
30
34
  : '';
31
- const expected = new RegExp(`^${step} \\(Round ${round}${qualifier}\\)$`);
32
- return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
35
+ const expected = new RegExp(`^${step} \\(Round ([1-9]\\d*)${qualifier}\\)$`);
36
+ return [...new Set(startedBackedRows(pairEntries(activity.entries))
37
+ .filter((row) => expected.test(row.step) && !row.done)
38
+ .map((row) => Number(expected.exec(row.step)?.[1])))].sort((left, right) => left - right);
33
39
  }
34
40
  function hasCompletedArtifactRound(content, family, round, name) {
35
- const activity = locateActivityLog(content);
36
- const step = ARTIFACT_STEPS[family];
37
- if (!activity || !step)
38
- return false;
39
- const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
40
- return startedBackedRows(pairEntries(activity.entries)).some((row) => expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`));
41
+ const event = completionEvent(family);
42
+ return Boolean(event && parseArtifactName(name)?.round === round
43
+ && artifactCompletionOrder(content, name, event) !== null
44
+ && hasArtifactCompletionFact(content, name, event));
45
+ }
46
+ function completionEvent(family) {
47
+ if (family === 'analysis')
48
+ return 'analyze.completed';
49
+ if (family === 'review-analysis')
50
+ return 'review-analysis.completed';
51
+ if (family === 'plan')
52
+ return 'plan.completed';
53
+ if (family === 'review-plan')
54
+ return 'review-plan.completed';
55
+ if (family === 'code')
56
+ return 'code.completed';
57
+ if (family === 'review-code')
58
+ return 'review-code.completed';
59
+ return null;
41
60
  }
42
61
  const BLOCKING_DIAGNOSTICS = new Set([
43
62
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
44
63
  'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
45
- 'FILESYSTEM_RACE', 'CATALOG_CONFLICT'
64
+ 'FILESYSTEM_RACE', 'CATALOG_CONFLICT', 'MULTIPLE_OPEN_ROUNDS'
46
65
  ]);
47
66
  function familySpec(family) {
48
67
  return artifactFamilyCatalog.find((item) => item.family === family) ?? null;
@@ -53,7 +72,7 @@ function escapeRegExp(value) {
53
72
  function failure(requestRef, family, error, extra = {}) {
54
73
  return {
55
74
  status: 'failed', changed: false, requestRef, taskId: null, taskDir: null,
56
- taskState: null, family, artifacts: [], latest: null, next: null,
75
+ taskState: null, family, artifacts: [], completed: [], latest: null, next: null, openRounds: [],
57
76
  reviewedInput: null, diagnostics: [], error, ...extra
58
77
  };
59
78
  }
@@ -82,19 +101,12 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
82
101
  taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
83
102
  });
84
103
  }
85
- let invalidation = { operations: [], targets: [] };
104
+ let taskContent;
86
105
  try {
87
- const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
88
- const parsed = parseInvalidationDocument(taskContent);
89
- if (!parsed.ok) {
90
- return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: parsed.message }, {
91
- taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
92
- });
93
- }
94
- invalidation = parsed.document;
106
+ taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
95
107
  }
96
108
  catch (error) {
97
- return failure(requestRef, family, { code: 'ARTIFACT_INVALIDATION_INVALID', message: String(error) }, {
109
+ return failure(requestRef, family, { code: 'ARTIFACT_DIRECTORY_READ_FAILED', message: String(error) }, {
98
110
  taskId: identity.taskId ?? null, taskDir, taskState: identity.taskState ?? null
99
111
  });
100
112
  }
@@ -133,8 +145,7 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
133
145
  }
134
146
  const identity = { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
135
147
  knownArtifacts.push(identity);
136
- if (!isArtifactInvalidated(invalidation, family, entry.name))
137
- artifacts.push(identity);
148
+ artifacts.push(identity);
138
149
  }
139
150
  catch (error) {
140
151
  diagnostics.push(diagnostic('FILESYSTEM_RACE', family, entry.name, String(error)));
@@ -142,19 +153,32 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
142
153
  }
143
154
  artifacts.sort((left, right) => left.round - right.round);
144
155
  knownArtifacts.sort((left, right) => left.round - right.round);
156
+ const openRounds = openArtifactRounds(taskContent, family);
145
157
  const knownRounds = new Set(knownArtifacts.map((item) => item.round));
146
- if (knownArtifacts.length > 0 && !knownRounds.has(1))
158
+ for (const round of openRounds)
159
+ knownRounds.add(round);
160
+ const max = Math.max(...knownRounds, 0);
161
+ if (max > 0 && !knownRounds.has(1))
147
162
  diagnostics.push(diagnostic('MISSING_BASE', family, null, `${family}.md is missing`));
148
- const max = knownArtifacts.at(-1)?.round ?? 0;
149
163
  for (let round = 1; round <= max; round += 1) {
150
164
  if (!knownRounds.has(round))
151
165
  diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
152
166
  }
153
- const latest = artifacts.at(-1) ?? null;
167
+ const event = completionEvent(family);
168
+ const completed = event
169
+ ? artifacts.flatMap((artifact) => {
170
+ const order = artifactCompletionOrder(taskContent, artifact.name, event);
171
+ return order !== null && hasArtifactCompletionFact(taskContent, artifact.path, event)
172
+ ? [{ artifact, order }] : [];
173
+ }).sort((left, right) => left.order - right.order).map(({ artifact }) => artifact)
174
+ : [];
175
+ if (openRounds.length > 1)
176
+ diagnostics.push(diagnostic('MULTIPLE_OPEN_ROUNDS', family, null, `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`));
177
+ const latest = completed.at(-1) ?? null;
154
178
  let hasReviewCodeReceipt = false;
155
179
  if (family === 'review-code' && latest) {
156
180
  try {
157
- hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name));
181
+ hasReviewCodeReceipt = Boolean(receiptForOutput(taskContent, latest.name));
158
182
  }
159
183
  catch {
160
184
  hasReviewCodeReceipt = false;
@@ -165,9 +189,9 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
165
189
  : null;
166
190
  return {
167
191
  status: 'ready', changed: false, requestRef, taskId: identity.taskId ?? null,
168
- taskDir, taskState: identity.taskState ?? null, family, artifacts, latest,
192
+ taskDir, taskState: identity.taskState ?? null, family, artifacts, completed, latest,
169
193
  next: { round: max + 1, name: artifactName(family, max + 1) },
170
- reviewedInput, diagnostics, error: null
194
+ openRounds, reviewedInput, diagnostics, error: null
171
195
  };
172
196
  }
173
197
  function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
@@ -184,11 +208,6 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
184
208
  const stat = fs.lstatSync(abs);
185
209
  if (!stat.isFile() || stat.isSymbolicLink())
186
210
  throw new Error('referenced input is not a regular file');
187
- const invalidation = parseInvalidationDocument(taskContent);
188
- if (!invalidation.ok)
189
- throw new Error(invalidation.message);
190
- if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name))
191
- throw new Error(`${input.name} is invalidated`);
192
211
  const actualSha256 = sha256File(abs);
193
212
  if (actualSha256 !== receipt.inputSha256) {
194
213
  throw new Error(`${input.name} content digest does not match receipt`);
@@ -270,11 +289,28 @@ const OPTIONAL_CONTEXT = {
270
289
  'manual-validation': ['review-code'],
271
290
  'validation-run': ['review-code']
272
291
  };
273
- function hasSelectionEvidence(content, family, options, context) {
292
+ function hasSelectionEvidence(content, family, options, context, pathState) {
274
293
  if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
275
294
  return true;
276
- if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt')
295
+ if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt'
296
+ || family === 'review-code' && options.reasonCode === 'manual-review-supplement')
277
297
  return true;
298
+ if ((family === 'plan' || family === 'code') && context.latest) {
299
+ const latestInputs = Object.fromEntries(context.inputs.map((input) => [input.family, input.name]));
300
+ const capturedInputs = receiptsForOutput(content, context.latest.name)
301
+ .filter((receipt) => receipt.event === `${family}.completed`)
302
+ .filter((receipt) => {
303
+ try {
304
+ return sha256File(path.join(context.taskDir, receipt.input)) === receipt.inputSha256;
305
+ }
306
+ catch {
307
+ return false;
308
+ }
309
+ })
310
+ .map((receipt) => receipt.input);
311
+ if (!executionInputsMatchLatest(family, pathState, latestInputs, capturedInputs))
312
+ return true;
313
+ }
278
314
  if (family.startsWith('review-')) {
279
315
  const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
280
316
  const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
@@ -337,16 +373,20 @@ function attachArtifactSelection(context, pathState, options) {
337
373
  catch (error) {
338
374
  return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
339
375
  }
340
- const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
341
- const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
342
- const open = openLatest || openNext;
343
- const selectedIdentity = openLatest ? context.latest : context.next;
376
+ const openRounds = openArtifactRounds(taskContent, family);
377
+ if (openRounds.length > 1)
378
+ return fail('ARTIFACT_TOPOLOGY_CONFLICT', `multiple incomplete ${family} rounds are open: ${openRounds.join(', ')}`);
379
+ const openRound = openRounds[0] ?? null;
380
+ const selectedIdentity = openRound === null ? context.next : {
381
+ family, round: openRound, name: artifactName(family, openRound),
382
+ path: path.join(context.taskDir, artifactName(family, openRound)), size: 0, mtimeMs: 0
383
+ };
344
384
  try {
345
- const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
385
+ const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context, pathState);
346
386
  const selection = selectArtifactDisposition({
347
387
  next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
348
388
  latest: context.latest,
349
- open,
389
+ open: openRound !== null,
350
390
  hasChangeEvidence
351
391
  });
352
392
  return {
@@ -377,7 +417,8 @@ function resolveArtifactContext(taskRef, family, options = {}) {
377
417
  return selected;
378
418
  if (selected.selection.disposition === 'reuse')
379
419
  return selected;
380
- if (selected.codeMode?.mode === 'error') {
420
+ if (selected.codeMode?.mode === 'error'
421
+ && selected.codeMode.message.includes('is not captured by the latest code input receipt')) {
381
422
  return {
382
423
  ...selected,
383
424
  status: 'ready',
@@ -435,32 +476,42 @@ function resolveCodeContext(inventory, options) {
435
476
  if (source.status === 'failed' || !source.latest)
436
477
  return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
437
478
  const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
479
+ try {
480
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
481
+ const hasPendingImplementation = parseImplementationInputs(taskContent).rows.some((input) => input.needsImplementation && input.status === 'pending');
482
+ if (hasPendingImplementation && !reviewCode.latest) {
483
+ const candidate = reviewCode.artifacts.at(-1)?.name ?? 'review-code.md';
484
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `cannot find completed Activity Log identity for ${candidate}`);
485
+ }
486
+ }
487
+ catch (error) {
488
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
489
+ }
438
490
  const latestCode = inventory.latest;
439
- const codeMax = latestCode?.round ?? 0;
491
+ const codeMax = inventory.next?.round ? inventory.next.round - 1 : latestCode?.round ?? 0;
440
492
  const reviewMax = reviewCode.latest?.round ?? 0;
441
493
  const inputs = [source.latest];
442
494
  const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
443
495
  const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
444
496
  if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
445
497
  inputs.push(sourceReview.latest);
446
- if (latestCode) {
498
+ if (inventory.openRounds.length > 0) {
447
499
  try {
448
500
  const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
449
- if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
450
- const review = reviewCode.latest;
451
- const openInputs = review ? [...inputs, review] : inputs;
452
- const openVerdict = review ? parseVerdict(review.path) : null;
453
- const openRow = locateActivityLog(taskContent)
454
- ? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
455
- .find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
456
- : null;
457
- const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
458
- const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
459
- const mode = decisionInput ? 'decision'
460
- : fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
461
- : 'init';
462
- return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${latestCode.round}.`, decisionInput);
463
- }
501
+ const openRound = inventory.openRounds[0];
502
+ const review = reviewCode.latest;
503
+ const openInputs = review ? [...inputs, review] : inputs;
504
+ const openVerdict = review ? parseVerdict(review.path) : null;
505
+ const openRow = locateActivityLog(taskContent)
506
+ ? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
507
+ .find((row) => row.step.startsWith(`Code Task (Round ${openRound}`) && !row.done)
508
+ : null;
509
+ const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
510
+ const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
511
+ const mode = decisionInput ? 'decision'
512
+ : fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
513
+ : 'init';
514
+ return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${openRound}.`, decisionInput);
464
515
  }
465
516
  catch (error) {
466
517
  return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
@@ -599,6 +650,13 @@ export function validateArtifactPublication(taskDir, family, name) {
599
650
  if (hasCompletedArtifactRound(task, family, existing.round, name)) {
600
651
  return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
601
652
  }
653
+ const event = completionEvent(family);
654
+ const frontmatter = parseTypedTaskFrontmatter(task);
655
+ const completionFactExists = event !== null && parseCompletionFacts(frontmatter.completion_facts)
656
+ .some((fact) => fact.event === event && fact.output === name);
657
+ if (event && completionFactExists && artifactCompletionOrder(task, name, event) !== null) {
658
+ return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completion evidence for '${name}' does not match its current content` };
659
+ }
602
660
  }
603
661
  catch (error) {
604
662
  return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };