@fitlab-ai/agent-infra 0.11.6 → 0.11.8

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 (192) hide show
  1. package/dist/bin/internal-cli.js +8 -2
  2. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  3. package/dist/lib/agent-clients/config.js +11 -3
  4. package/dist/lib/defaults.json +6 -3
  5. package/dist/lib/git/primary-worktree.js +58 -0
  6. package/dist/lib/internal/cli-route-inventory.js +3 -0
  7. package/dist/lib/internal/platform-pr-review.js +20 -3
  8. package/dist/lib/internal/task-operation-registry.js +3 -1
  9. package/dist/lib/internal/task-platform-recovery.js +56 -0
  10. package/dist/lib/merge.js +25 -5
  11. package/dist/lib/platform/completion-backfill.js +2 -1
  12. package/dist/lib/platform/in-label-sync.js +12 -1
  13. package/dist/lib/platform/issue-comments.js +58 -4
  14. package/dist/lib/platform/issues.js +105 -5
  15. package/dist/lib/platform/operation-coordinator.js +74 -0
  16. package/dist/lib/platform/pr-review.js +107 -1
  17. package/dist/lib/platform/pr-summary.js +79 -57
  18. package/dist/lib/platform/pull-requests.js +125 -7
  19. package/dist/lib/sandbox/commands/create.js +6 -3
  20. package/dist/lib/sandbox/commands/ls.js +4 -3
  21. package/dist/lib/sandbox/commands/rm.js +1 -1
  22. package/dist/lib/sandbox/config.js +22 -4
  23. package/dist/lib/sandbox/control/server.js +6 -2
  24. package/dist/lib/sandbox/git-metadata.js +50 -0
  25. package/dist/lib/sandbox/index.js +1 -1
  26. package/dist/lib/sandbox/mounts.js +12 -0
  27. package/dist/lib/sandbox/recovery.js +11 -0
  28. package/dist/lib/sandbox/removal.js +58 -20
  29. package/dist/lib/sandbox/tools.js +29 -7
  30. package/dist/lib/server/config.js +7 -27
  31. package/dist/lib/server/daemon.js +4 -3
  32. package/dist/lib/server/process-control.js +4 -2
  33. package/dist/lib/server/runner.js +4 -3
  34. package/dist/lib/task/activity-intent.js +4 -3
  35. package/dist/lib/task/artifact-lifecycle.js +42 -10
  36. package/dist/lib/task/artifact-receipts.js +62 -22
  37. package/dist/lib/task/capabilities.js +22 -4
  38. package/dist/lib/task/completion-facts.js +56 -0
  39. package/dist/lib/task/create-service.js +11 -5
  40. package/dist/lib/task/create.js +60 -79
  41. package/dist/lib/task/events.js +268 -195
  42. package/dist/lib/task/finalization-artifacts.js +4 -1
  43. package/dist/lib/task/finalization-handoff.js +2 -1
  44. package/dist/lib/task/finalization.js +157 -24
  45. package/dist/lib/task/invalidation-command.js +4 -2
  46. package/dist/lib/task/invalidation.js +38 -9
  47. package/dist/lib/task/lifecycle.js +38 -2
  48. package/dist/lib/task/local-artifact-finalization.js +17 -3
  49. package/dist/lib/task/platform-operation-journal.js +152 -0
  50. package/dist/lib/task/platform-operation-recovery.js +459 -0
  51. package/dist/lib/task/qualification-audit.js +23 -105
  52. package/dist/lib/task/review-finalization.js +7 -0
  53. package/dist/lib/task/review-identity.js +69 -0
  54. package/dist/lib/task/sections.js +6 -5
  55. package/dist/lib/task/task-execution-lock.js +6 -3
  56. package/dist/lib/task/verification-engine.js +14 -63
  57. package/dist/lib/task/verification.js +35 -38
  58. package/dist/lib/task/workflow-warning-intents.js +7 -1
  59. package/dist/lifecycle-build-manifest.json +3 -3
  60. package/lib/agent-clients/adapters/codex.ts +1 -1
  61. package/lib/agent-clients/config.ts +9 -3
  62. package/lib/defaults.json +6 -3
  63. package/lib/git/primary-worktree.ts +62 -0
  64. package/lib/internal/cli-route-inventory.ts +2 -0
  65. package/lib/internal/platform-pr-review.ts +14 -3
  66. package/lib/internal/task-operation-registry.ts +3 -1
  67. package/lib/internal/task-platform-recovery.ts +53 -0
  68. package/lib/merge.ts +26 -6
  69. package/lib/platform/completion-backfill.ts +2 -1
  70. package/lib/platform/in-label-sync.ts +12 -0
  71. package/lib/platform/issue-comments.ts +59 -3
  72. package/lib/platform/issues.ts +103 -5
  73. package/lib/platform/operation-coordinator.ts +96 -0
  74. package/lib/platform/pr-review.ts +113 -1
  75. package/lib/platform/pr-summary.ts +22 -1
  76. package/lib/platform/pull-requests.ts +132 -8
  77. package/lib/sandbox/commands/create.ts +7 -8
  78. package/lib/sandbox/commands/ls.ts +4 -3
  79. package/lib/sandbox/commands/rm.ts +1 -1
  80. package/lib/sandbox/config.ts +23 -5
  81. package/lib/sandbox/control/server.ts +5 -2
  82. package/lib/sandbox/git-metadata.ts +66 -0
  83. package/lib/sandbox/index.ts +1 -1
  84. package/lib/sandbox/mounts.ts +14 -0
  85. package/lib/sandbox/recovery.ts +13 -0
  86. package/lib/sandbox/removal.ts +57 -19
  87. package/lib/sandbox/tool-types.ts +1 -1
  88. package/lib/sandbox/tools.ts +36 -7
  89. package/lib/server/config.ts +7 -29
  90. package/lib/server/daemon.ts +5 -3
  91. package/lib/server/process-control.ts +4 -2
  92. package/lib/server/runner.ts +7 -4
  93. package/lib/server/server.schema.json +0 -4
  94. package/lib/task/activity-intent.ts +4 -3
  95. package/lib/task/artifact-lifecycle.ts +35 -11
  96. package/lib/task/artifact-receipts.ts +55 -15
  97. package/lib/task/capabilities.ts +17 -3
  98. package/lib/task/completion-facts.ts +66 -0
  99. package/lib/task/create-service.ts +12 -6
  100. package/lib/task/create.ts +54 -69
  101. package/lib/task/events.ts +248 -182
  102. package/lib/task/finalization-artifacts.ts +4 -1
  103. package/lib/task/finalization-handoff.ts +2 -1
  104. package/lib/task/finalization.ts +154 -21
  105. package/lib/task/invalidation-command.ts +4 -2
  106. package/lib/task/invalidation.ts +35 -10
  107. package/lib/task/lifecycle.ts +35 -3
  108. package/lib/task/local-artifact-finalization.ts +13 -3
  109. package/lib/task/platform-operation-journal.ts +230 -0
  110. package/lib/task/platform-operation-recovery.ts +435 -0
  111. package/lib/task/qualification-audit.ts +21 -114
  112. package/lib/task/review-finalization.ts +7 -0
  113. package/lib/task/review-identity.ts +85 -0
  114. package/lib/task/sections.ts +6 -6
  115. package/lib/task/task-execution-lock.ts +7 -3
  116. package/lib/task/verification-engine.ts +13 -86
  117. package/lib/task/verification.ts +34 -38
  118. package/lib/task/workflow-warning-intents.ts +9 -1
  119. package/package.json +1 -1
  120. package/templates/.agents/README.en.md +19 -17
  121. package/templates/.agents/README.zh-CN.md +18 -16
  122. package/templates/.agents/rules/README.en.md +2 -1
  123. package/templates/.agents/rules/README.zh-CN.md +2 -1
  124. package/templates/.agents/rules/decision-qualification.en.md +4 -3
  125. package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
  126. package/templates/.agents/rules/task-management.en.md +12 -0
  127. package/templates/.agents/rules/task-management.zh-CN.md +12 -0
  128. package/templates/.agents/rules/validation-output.en.md +19 -0
  129. package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
  130. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
  131. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  132. package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
  133. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  134. package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -2
  135. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -3
  136. package/templates/.agents/skills/code-task/SKILL.en.md +5 -3
  137. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +4 -4
  138. package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
  139. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
  140. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  142. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  143. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  144. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  145. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  146. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  147. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  148. package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
  149. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  150. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
  151. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  152. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
  153. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  154. package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
  155. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
  157. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  158. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  159. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  160. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
  161. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
  162. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
  163. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
  164. package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
  165. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
  166. package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
  167. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
  168. package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
  169. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
  170. package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
  171. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
  172. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
  173. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
  174. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
  175. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
  176. package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
  177. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
  178. package/templates/.agents/skills/review-pr/SKILL.en.md +13 -11
  179. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +13 -11
  180. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +3 -3
  181. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +3 -3
  182. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -0
  183. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -0
  184. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +19 -6
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
  191. package/templates/.agents/templates/task.en.md +2 -2
  192. package/templates/.agents/templates/task.zh-CN.md +2 -2
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { createHash } from 'node:crypto';
5
4
  import { execFileSync } from 'node:child_process';
5
+ import { resolvePrimaryWorktreeRoot } from '../git/primary-worktree.js';
6
6
  const ENV_PREFIX = 'AGENT_INFRA_SERVER_';
7
7
  // Keys whose presence in the *committed* server.json is treated as a leaked
8
8
  // secret. Secrets belong in .agents/server.local.json or the environment.
@@ -16,17 +16,7 @@ export const DEFAULT_SERVER_CONFIG = {
16
16
  function isPlainObject(value) {
17
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
18
18
  }
19
- // Daemon runtime state (log + PID) lives OUTSIDE the repo, under the user's home
20
- // directory, keyed by the .airc.json "project" AND a stable hash of the repo
21
- // root path:
22
- // ~/.agent-infra/logs/<project>/<repo-hash>/server.log
23
- // ~/.agent-infra/run/<project>/<repo-hash>/server.pid
24
- // The <project> segment groups a project's checkouts for readability; the
25
- // <repo-hash> segment guarantees that two checkouts/worktrees of the same
26
- // project (same "project" but different absolute path) get ISOLATED runtime
27
- // dirs, so they never read/control each other's daemon. Using os.homedir() +
28
- // path.join keeps this correct on Windows too (C:\Users\<name>\.agent-infra\...).
29
- // An explicit log.path in server.json/.local/env still overrides the log default.
19
+ // Runtime state is shared by all worktrees through the primary checkout.
30
20
  function resolveProjectKey(repoRoot) {
31
21
  try {
32
22
  const airc = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
@@ -39,12 +29,8 @@ function resolveProjectKey(repoRoot) {
39
29
  }
40
30
  return path.basename(repoRoot);
41
31
  }
42
- // Short, stable, filesystem-safe discriminator for a checkout's absolute path.
43
- function repoKey(repoRoot) {
44
- return createHash('sha256').update(repoRoot).digest('hex').slice(0, 12);
45
- }
46
- function runtimePath(repoRoot, projectKey, kind, file) {
47
- return path.join(homedir(), '.agent-infra', kind, projectKey, repoKey(repoRoot), file);
32
+ function runtimePath(projectKey, kind, file) {
33
+ return path.join(homedir(), '.agent-infra', kind, projectKey, file);
48
34
  }
49
35
  function detectRepoRoot() {
50
36
  try {
@@ -147,7 +133,7 @@ export function validateServerConfig(committed) {
147
133
  return { ok: true };
148
134
  }
149
135
  export function loadServerConfig({ rootDir } = {}) {
150
- const repoRoot = rootDir ?? detectRepoRoot();
136
+ const repoRoot = resolvePrimaryWorktreeRoot(rootDir ?? detectRepoRoot());
151
137
  const agentsDir = path.join(repoRoot, '.agents');
152
138
  const committed = readJsonIfPresent(path.join(agentsDir, 'server.json'));
153
139
  const validation = validateServerConfig(committed);
@@ -160,17 +146,11 @@ export function loadServerConfig({ rootDir } = {}) {
160
146
  merged = deepMerge(merged, envOverrides(process.env));
161
147
  const projectKey = resolveProjectKey(repoRoot);
162
148
  const log = isPlainObject(merged.log) ? merged.log : {};
163
- // No explicit log.path → default under ~/.agent-infra/logs/<project>/.
164
- // Explicit relative path resolves against the repo root; absolute is used as-is.
165
- const explicitPath = typeof log.path === 'string' ? log.path : null;
166
- const resolvedLogPath = explicitPath === null
167
- ? runtimePath(repoRoot, projectKey, 'logs', 'server.log')
168
- : (path.isAbsolute(explicitPath) ? explicitPath : path.join(repoRoot, explicitPath));
169
149
  return {
170
150
  repoRoot,
171
- pidFile: runtimePath(repoRoot, projectKey, 'run', 'server.pid'),
151
+ pidFile: runtimePath(projectKey, 'run', 'server.pid'),
172
152
  log: {
173
- path: resolvedLogPath,
153
+ path: runtimePath(projectKey, 'logs', 'server.log'),
174
154
  rotateAtBytes: typeof log.rotateAtBytes === 'number' ? log.rotateAtBytes : DEFAULT_ROTATE_BYTES
175
155
  },
176
156
  heartbeatMs: typeof merged.heartbeatMs === 'number' ? merged.heartbeatMs : DEFAULT_SERVER_CONFIG.heartbeatMs,
@@ -61,7 +61,7 @@ export function createMessageDispatcher(options) {
61
61
  title: `ai ${plan.argv.join(' ')}`,
62
62
  chunkChars: typeof options.config.stream?.chunkChars === 'number' ? options.config.stream.chunkChars : 4000,
63
63
  throttleMs: typeof options.config.stream?.throttleMs === 'number' ? options.config.stream.throttleMs : 1500
64
- }, (emit) => runAiImpl(plan.argv, { onChunk: emit }), (outbound) => replyOutbound(message, outbound));
64
+ }, (emit) => runAiImpl(plan.argv, { onChunk: emit, cwd: options.config.repoRoot }), (outbound) => replyOutbound(message, outbound));
65
65
  }
66
66
  };
67
67
  }
@@ -75,15 +75,16 @@ export function createMessageDispatcher(options) {
75
75
  // - runDaemon() awaits a shutdown promise that only resolves once a
76
76
  // SIGINT/SIGTERM handler has finished graceful cleanup. We never unref()
77
77
  // the only keep-alive timer (that would let the process exit immediately).
78
- export async function runDaemon() {
78
+ export async function runDaemon({ rootDir } = {}) {
79
79
  let config;
80
80
  try {
81
- config = loadServerConfig();
81
+ config = loadServerConfig({ rootDir });
82
82
  }
83
83
  catch (error) {
84
84
  process.stderr.write(`${errorMessage(error)}\n`);
85
85
  process.exit(1);
86
86
  }
87
+ process.chdir(config.repoRoot);
87
88
  const logger = createLogger(config.log);
88
89
  logger.info(`daemon starting agent-infra ${VERSION} pid=${process.pid}`);
89
90
  const ownStartTime = getProcessStartTime(process.pid);
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { spawn, execFileSync } from 'node:child_process';
3
4
  import { loadServerConfig } from './config.js';
4
5
  import { runDaemon } from './daemon.js';
@@ -65,7 +66,7 @@ export async function start({ foreground = false } = {}) {
65
66
  removePidFileIfMatches(pidPath, existing.snapshot);
66
67
  }
67
68
  if (foreground) {
68
- await runDaemon();
69
+ await runDaemon({ rootDir: config.repoRoot });
69
70
  return;
70
71
  }
71
72
  const cliEntry = process.argv[1];
@@ -75,8 +76,9 @@ export async function start({ foreground = false } = {}) {
75
76
  // Re-spawn ourselves detached. process.execArgv is forwarded so the dev path
76
77
  // (node --experimental-strip-types ./bin/cli.ts) and the built path
77
78
  // (node dist/bin/cli.js) both work.
78
- const child = spawn(process.execPath, [...process.execArgv, cliEntry, 'server', '__daemon'], {
79
+ const child = spawn(process.execPath, [...process.execArgv, path.resolve(cliEntry), 'server', '__daemon'], {
79
80
  detached: true,
81
+ cwd: config.repoRoot,
80
82
  stdio: 'ignore'
81
83
  });
82
84
  child.unref();
@@ -22,12 +22,13 @@ function resolveCommand(file) {
22
22
  function needsShell(file) {
23
23
  return process.platform === 'win32' && /\.(?:bat|cmd)$/i.test(file);
24
24
  }
25
- function spawnCapture(file, args, onChunk) {
25
+ function spawnCapture(file, args, onChunk, cwd) {
26
26
  return new Promise((resolve, reject) => {
27
27
  const resolvedFile = resolveCommand(file);
28
28
  const child = spawn(resolvedFile, args, {
29
29
  stdio: ['ignore', 'pipe', 'pipe'],
30
- shell: needsShell(resolvedFile)
30
+ shell: needsShell(resolvedFile),
31
+ ...(cwd === undefined ? {} : { cwd })
31
32
  });
32
33
  let stdout = '';
33
34
  let stderr = '';
@@ -70,6 +71,6 @@ function spawnCapture(file, args, onChunk) {
70
71
  });
71
72
  }
72
73
  export async function runAi(args, options = {}) {
73
- return (options.spawn ?? spawnCapture)('ai', args, options.onChunk);
74
+ return (options.spawn ?? spawnCapture)('ai', args, options.onChunk, options.cwd);
74
75
  }
75
76
  //# sourceMappingURL=runner.js.map
@@ -11,6 +11,7 @@ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
11
11
  const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
12
12
  const OUTCOMES = new Set(['aborted', 'superseded']);
13
13
  const SUCCESS_STATUSES = new Set(['applied', 'no-op']);
14
+ const LOCAL_COMPLETION_STATUSES = new Set(['applied', 'no-op', 'blocked', 'failed']);
14
15
  const ARTIFACT_STATUSES = new Set([
15
16
  'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
16
17
  ]);
@@ -102,7 +103,7 @@ function readSnapshot(taskRef, repoRoot) {
102
103
  }
103
104
  if (terminal.length === 1) {
104
105
  const note = terminal[0].note;
105
- const terminalMatchesStatus = (note.startsWith('Verdict: ') && SUCCESS_STATUSES.has(parsed.status))
106
+ const terminalMatchesStatus = (note.startsWith('Verdict: ') && LOCAL_COMPLETION_STATUSES.has(parsed.status))
106
107
  || (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
107
108
  || (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
108
109
  || (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
@@ -233,8 +234,8 @@ function applyLocked(intent, normalizedAgent, options) {
233
234
  if (roundState.startedAgent === null) {
234
235
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
235
236
  }
236
- if (intent.kind === 'pr-review-complete' && !SUCCESS_STATUSES.has(roundState.status)) {
237
- return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must be applied or no-op before completion`, state.taskId);
237
+ if (intent.kind === 'pr-review-complete' && !LOCAL_COMPLETION_STATUSES.has(roundState.status)) {
238
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must record a publication outcome before completion`, state.taskId);
238
239
  }
239
240
  if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
240
241
  return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
@@ -7,6 +7,8 @@ import { parseImplementationInputs, selectPendingImplementationInput } from './i
7
7
  import { parseVerdict } from './review-artifacts.js';
8
8
  import { extractSection, findSectionHeading } from './sections.js';
9
9
  import { receiptForOutput, sha256File } from './artifact-receipts.js';
10
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
11
+ import { inspectReviewIdentity } from './review-identity.js';
10
12
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
11
13
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
14
  import { parseReworkIntentDocument } from './rework-intent.js';
@@ -149,7 +151,16 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
149
151
  diagnostics.push(diagnostic('ROUND_GAP', family, null, `${artifactName(family, round)} is missing`));
150
152
  }
151
153
  const latest = artifacts.at(-1) ?? null;
152
- const reviewedInput = family.startsWith('review-') && latest
154
+ let hasReviewCodeReceipt = false;
155
+ if (family === 'review-code' && latest) {
156
+ try {
157
+ hasReviewCodeReceipt = Boolean(receiptForOutput(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'), latest.name));
158
+ }
159
+ catch {
160
+ hasReviewCodeReceipt = false;
161
+ }
162
+ }
163
+ const reviewedInput = family.startsWith('review-') && latest && (family !== 'review-code' || hasReviewCodeReceipt)
153
164
  ? resolveReviewedInput(taskDir, latest, family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code', diagnostics)
154
165
  : null;
155
166
  return {
@@ -248,14 +259,16 @@ function assertWritableInventory(inventory) {
248
259
  };
249
260
  }
250
261
  const REQUIRED_INPUT = {
251
- 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
262
+ 'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan'
252
263
  };
253
264
  const OPTIONAL_CONTEXT = {
254
- analysis: { family: 'review-analysis' },
255
- plan: { family: 'review-plan' },
256
- 'review-code': { family: 'review-plan' },
257
- 'manual-validation': { family: 'review-code' },
258
- 'validation-run': { family: 'review-code' }
265
+ analysis: ['review-analysis'],
266
+ plan: ['review-analysis', 'review-plan'],
267
+ 'review-plan': ['review-analysis'],
268
+ code: ['review-plan'],
269
+ 'review-code': ['code', 'plan', 'review-plan'],
270
+ 'manual-validation': ['review-code'],
271
+ 'validation-run': ['review-code']
259
272
  };
260
273
  function hasSelectionEvidence(content, family, options, context) {
261
274
  if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
@@ -265,6 +278,20 @@ function hasSelectionEvidence(content, family, options, context) {
265
278
  if (family.startsWith('review-')) {
266
279
  const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
267
280
  const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
281
+ if (family === 'review-code') {
282
+ if (!context.latest)
283
+ return false;
284
+ try {
285
+ const reportContent = fs.readFileSync(context.latest.path, 'utf8');
286
+ const identity = inspectReviewIdentity(context.taskDir, reportContent, options.repoRoot);
287
+ return identity.status !== 'matched'
288
+ || !hasArtifactCompletionFact(content, context.latest.path, 'review-code.completed')
289
+ || !hasArtifactCompletionLog(content, context.latest.name, 'review-code.completed');
290
+ }
291
+ catch {
292
+ return true;
293
+ }
294
+ }
268
295
  if (currentInput && context.reviewedInput?.name !== currentInput.name)
269
296
  return true;
270
297
  }
@@ -387,9 +414,10 @@ function resolveArtifactContext(taskRef, family, options = {}) {
387
414
  }
388
415
  const optional = OPTIONAL_CONTEXT[inventory.family];
389
416
  if (optional) {
390
- const context = inspectTaskArtifacts(taskRef, optional.family, options);
391
- if (context.status === 'ready' && context.latest) {
392
- inputs.push(context.latest);
417
+ for (const family of optional) {
418
+ const context = inspectTaskArtifacts(taskRef, family, options);
419
+ if (context.status === 'ready' && context.latest)
420
+ inputs.push(context.latest);
393
421
  }
394
422
  }
395
423
  if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
@@ -411,6 +439,10 @@ function resolveCodeContext(inventory, options) {
411
439
  const codeMax = latestCode?.round ?? 0;
412
440
  const reviewMax = reviewCode.latest?.round ?? 0;
413
441
  const inputs = [source.latest];
442
+ const sourceReviewFamily = inputFamily === 'analysis' ? 'review-analysis' : 'review-plan';
443
+ const sourceReview = inspectTaskArtifacts(taskRef, sourceReviewFamily, options);
444
+ if (sourceReview.status === 'ready' && sourceReview.latest && sourceReview.reviewedInput?.name === source.latest.name)
445
+ inputs.push(sourceReview.latest);
414
446
  if (latestCode) {
415
447
  try {
416
448
  const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
@@ -5,17 +5,21 @@ import { extractSection, findSectionHeading, parseTable } from './sections.js';
5
5
  const RECEIPT_SECTION_ALIASES = ['产物生命周期收据', 'Artifact Lifecycle Receipts'];
6
6
  const RECEIPT_COLUMNS = ['event', 'output', 'input', 'input_sha256', 'completed_at'];
7
7
  const RECEIPT_EVENTS = new Set([
8
+ 'analysis.completed',
9
+ 'plan.completed',
8
10
  'review-analysis.completed',
9
11
  'review-plan.completed',
12
+ 'code.completed',
10
13
  'review-code.completed',
11
- 'code.completed'
12
14
  ]);
13
15
  const SHA256_RE = /^[a-f0-9]{64}$/;
14
16
  const RECEIPT_SHAPES = {
17
+ 'analysis.completed': { output: 'analysis', input: 'review-analysis' },
18
+ 'plan.completed': { output: 'plan', input: 'analysis' },
15
19
  'review-analysis.completed': { output: 'review-analysis', input: 'analysis' },
16
20
  'review-plan.completed': { output: 'review-plan', input: 'plan' },
17
- 'review-code.completed': { output: 'review-code', input: 'code' },
18
- 'code.completed': { output: 'code', input: 'plan' }
21
+ 'code.completed': { output: 'code', input: 'plan' },
22
+ 'review-code.completed': { output: 'review-code', input: 'code' }
19
23
  };
20
24
  class ArtifactReceiptError extends Error {
21
25
  code = 'ARTIFACT_RECEIPT_INVALID';
@@ -37,8 +41,16 @@ function validateReceiptShape(event, output, input) {
37
41
  throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
38
42
  const shape = RECEIPT_SHAPES[event];
39
43
  const inputMatches = event === 'code.completed'
40
- ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
41
- : inputIdentity.family === shape.input;
44
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan' || inputIdentity.family === 'review-code'
45
+ : event === 'plan.completed'
46
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'review-analysis' || inputIdentity.family === 'review-plan'
47
+ : event === 'analysis.completed'
48
+ ? inputIdentity.family === 'review-analysis'
49
+ : event === 'review-plan.completed'
50
+ ? inputIdentity.family === 'plan' || inputIdentity.family === 'review-analysis'
51
+ : event === 'review-code.completed'
52
+ ? inputIdentity.family === 'code' || inputIdentity.family === 'plan' || inputIdentity.family === 'review-plan'
53
+ : inputIdentity.family === shape.input;
42
54
  if (outputIdentity.family !== shape.output || !inputMatches) {
43
55
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
44
56
  }
@@ -52,7 +64,9 @@ function parseArtifactReceipts(content) {
52
64
  table = parseTable(content, {
53
65
  sectionAliases: [...RECEIPT_SECTION_ALIASES],
54
66
  columns: [...RECEIPT_COLUMNS],
55
- keyColumn: 'output'
67
+ // A completed artifact can have multiple lifecycle inputs. Pair
68
+ // uniqueness is checked below because the table parser supports one key.
69
+ keyColumn: null
56
70
  });
57
71
  }
58
72
  catch (error) {
@@ -84,29 +98,55 @@ function parseArtifactReceipts(content) {
84
98
  completedAt
85
99
  };
86
100
  });
101
+ const edges = new Set();
102
+ for (const row of rows) {
103
+ const edge = `${row.output}\0${row.input}`;
104
+ if (edges.has(edge))
105
+ throw new ArtifactReceiptError(`duplicate receipt edge '${row.output}' <- '${row.input}'`);
106
+ edges.add(edge);
107
+ }
87
108
  return { present: true, rows };
88
109
  }
110
+ function receiptsForOutput(content, output) {
111
+ return parseArtifactReceipts(content).rows.filter((row) => row.output === output);
112
+ }
89
113
  function receiptForOutput(content, output) {
90
- const parsed = parseArtifactReceipts(content);
91
- return parsed.rows.find((row) => row.output === output) ?? null;
114
+ return receiptsForOutput(content, output)[0] ?? null;
92
115
  }
93
116
  function upsertArtifactReceipt(content, receipt) {
94
- if (!RECEIPT_EVENTS.has(receipt.event))
95
- throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
96
- if (!receipt.output || !receipt.input)
97
- throw new ArtifactReceiptError('receipt output and input are required');
98
- validateReceiptShape(receipt.event, receipt.output, receipt.input);
99
- if (!SHA256_RE.test(receipt.inputSha256))
100
- throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
101
- if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
102
- throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
117
+ return upsertArtifactReceipts(content, [receipt]);
118
+ }
119
+ function upsertArtifactReceipts(content, receiptsToAdd) {
120
+ if (receiptsToAdd.length === 0)
121
+ throw new ArtifactReceiptError('at least one receipt is required');
122
+ const additions = new Map();
123
+ for (const receipt of receiptsToAdd) {
124
+ if (!RECEIPT_EVENTS.has(receipt.event))
125
+ throw new ArtifactReceiptError(`unknown receipt event '${receipt.event}'`);
126
+ if (!receipt.output || !receipt.input)
127
+ throw new ArtifactReceiptError('receipt output and input are required');
128
+ validateReceiptShape(receipt.event, receipt.output, receipt.input);
129
+ if (!SHA256_RE.test(receipt.inputSha256))
130
+ throw new ArtifactReceiptError(`receipt digest for '${receipt.output}' is invalid`);
131
+ if (!receipt.completedAt || Number.isNaN(Date.parse(receipt.completedAt.replace(' ', 'T')))) {
132
+ throw new ArtifactReceiptError(`receipt completion time for '${receipt.output}' is invalid`);
133
+ }
134
+ const key = `${receipt.output}\0${receipt.input}`;
135
+ const duplicate = additions.get(key);
136
+ if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(receipt))
137
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' has conflicting evidence`);
138
+ additions.set(key, receipt);
103
139
  }
104
140
  const existing = parseArtifactReceipts(content).rows;
105
- const previous = existing.find((row) => row.output === receipt.output);
106
- if (previous && JSON.stringify(previous) !== JSON.stringify(receipt)) {
107
- throw new ArtifactReceiptError(`receipt for '${receipt.output}' already exists with different evidence`);
141
+ for (const receipt of additions.values()) {
142
+ const previous = existing.find((row) => row.output === receipt.output && row.input === receipt.input);
143
+ if (previous && JSON.stringify(previous) !== JSON.stringify(receipt))
144
+ throw new ArtifactReceiptError(`receipt for '${receipt.output}' from '${receipt.input}' already exists with different evidence`);
108
145
  }
109
- const rows = previous ? existing : [...existing, receipt];
146
+ const rows = [...existing];
147
+ for (const receipt of additions.values())
148
+ if (!rows.some((row) => row.output === receipt.output && row.input === receipt.input))
149
+ rows.push(receipt);
110
150
  const heading = findSectionHeading(content, [...RECEIPT_SECTION_ALIASES]);
111
151
  const body = [
112
152
  `| ${RECEIPT_COLUMNS.join(' | ')} |`,
@@ -115,5 +155,5 @@ function upsertArtifactReceipt(content, receipt) {
115
155
  ].join('\n');
116
156
  return { aliases: [...RECEIPT_SECTION_ALIASES], heading, body };
117
157
  }
118
- export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, sha256Bytes, sha256File, upsertArtifactReceipt };
158
+ export { ArtifactReceiptError, RECEIPT_SECTION_ALIASES, parseArtifactReceipts, receiptForOutput, receiptsForOutput, sha256Bytes, sha256File, upsertArtifactReceipt, upsertArtifactReceipts };
119
159
  //# sourceMappingURL=artifact-receipts.js.map
@@ -12,6 +12,8 @@ import { hasOpenLifecycleExecution } from './activity-log.js';
12
12
  import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.js';
13
13
  import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.js';
14
14
  import { parseImplementationInputs } from './implementation-inputs.js';
15
+ import { hasArtifactCompletionFact, hasArtifactCompletionLog } from './completion-facts.js';
16
+ import { inspectReviewIdentity } from './review-identity.js';
15
17
  const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
16
18
  const REASONS = new Set([
17
19
  'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
@@ -139,13 +141,15 @@ function canStart(action, facts, trigger) {
139
141
  return allow('plan-review-approved');
140
142
  }
141
143
  if (action === 'review-code') {
142
- return hasArtifact(facts, 'code') ? allow('code-artifact') : deny('CODE_ARTIFACT_REQUIRED');
144
+ return allow(hasArtifact(facts, 'code') ? 'optional-code-context' : 'review-current-git-diff');
143
145
  }
144
146
  if (action === 'manual-validation' || action === 'validation-run') {
145
147
  if (!hasArtifact(facts, 'review-code'))
146
148
  return deny('CODE_REVIEW_REQUIRED');
147
- if (!reviewMatchesLatest(facts, 'code', 'review-code'))
149
+ if (hasArtifact(facts, 'code') && !reviewMatchesLatest(facts, 'code', 'review-code'))
148
150
  return deny('CODE_REVIEW_NOT_LATEST');
151
+ if (!facts.reviewCodeIdentityValid)
152
+ return deny('CODE_REVIEW_IDENTITY_INVALID');
149
153
  if (facts.reviews['review-code'] !== 'approved')
150
154
  return deny('CODE_REVIEW_NOT_APPROVED');
151
155
  if (facts.unresolvedLedger.code > 0)
@@ -309,7 +313,13 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
309
313
  continue;
310
314
  }
311
315
  const audit = parseQualificationAudit(auditContent);
312
- if (!audit.ok || !audit.audit.present || !audit.audit.snapshot) {
316
+ if (!audit.ok) {
317
+ qualificationStaleArtifacts.push(name);
318
+ continue;
319
+ }
320
+ if (!audit.audit.present)
321
+ continue;
322
+ if (!audit.audit.snapshot) {
313
323
  qualificationStaleArtifacts.push(name);
314
324
  continue;
315
325
  }
@@ -324,6 +334,7 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
324
334
  }
325
335
  const reviews = {};
326
336
  const reviewedInputs = {};
337
+ let reviewCodeIdentityValid = false;
327
338
  for (const family of ['review-analysis', 'review-plan', 'review-code']) {
328
339
  const latest = latestArtifact(artifacts[family] ?? []);
329
340
  if (!latest)
@@ -337,6 +348,12 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
337
348
  && (artifacts[expectedFamily] ?? []).includes(input)
338
349
  && artifactHashes[input] === receipt.inputSha256)
339
350
  reviewedInputs[family] = input;
351
+ if (family === 'review-code') {
352
+ const identity = inspectReviewIdentity(taskDir, reviewContent);
353
+ reviewCodeIdentityValid = identity.status === 'matched'
354
+ && hasArtifactCompletionFact(content, path.join(taskDir, latest), 'review-code.completed')
355
+ && hasArtifactCompletionLog(content, latest, 'review-code.completed');
356
+ }
340
357
  const parsed = parseReviewSummary(reviewContent);
341
358
  if (!parsed.ok)
342
359
  continue;
@@ -407,7 +424,8 @@ function buildLifecycleFacts(taskDir, content, taskState = 'active', executionBu
407
424
  qualificationStaleArtifacts,
408
425
  pathState,
409
426
  reworkClassificationRequired,
410
- resolvedHumanDecisions
427
+ resolvedHumanDecisions,
428
+ reviewCodeIdentityValid
411
429
  };
412
430
  facts.recommendedAction = recommendNext(facts).action;
413
431
  return { ok: true, facts };
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import { canonicalSemanticDigest } from './artifact-operations.js';
3
+ import { inspectActivityLog } from './activity-log.js';
4
+ import { parseTypedTaskFrontmatter } from './frontmatter.js';
5
+ import { parseArtifactName } from './artifact-name.js';
6
+ import { sha256File } from './artifact-receipts.js';
7
+ export function parseCompletionFacts(value) {
8
+ if (typeof value !== 'string')
9
+ return [];
10
+ try {
11
+ const parsed = JSON.parse(value);
12
+ if (!Array.isArray(parsed))
13
+ return [];
14
+ return parsed.filter((fact) => Boolean(fact && typeof fact === 'object' && !Array.isArray(fact)
15
+ && typeof fact.event === 'string'
16
+ && typeof fact.output === 'string'
17
+ && /^[a-f0-9]{64}$/u.test(fact.outputSha256)
18
+ && /^[a-f0-9]{64}$/u.test(fact.semanticDigest)
19
+ && typeof fact.requestId === 'string'
20
+ && typeof fact.result === 'string'));
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ export function hasArtifactCompletionFact(taskContent, artifactPath, event) {
27
+ try {
28
+ const frontmatter = parseTypedTaskFrontmatter(taskContent);
29
+ const output = artifactPath.split(/[\\/]/u).at(-1) ?? '';
30
+ const content = fs.readFileSync(artifactPath, 'utf8');
31
+ const outputSha256 = sha256File(artifactPath);
32
+ const semanticDigest = canonicalSemanticDigest(content);
33
+ return parseCompletionFacts(frontmatter.completion_facts).some((fact) => fact.event === event && fact.output === output
34
+ && fact.outputSha256 === outputSha256 && fact.semanticDigest === semanticDigest
35
+ && fact.requestId.length > 0);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function hasArtifactCompletionLog(taskContent, artifact, event) {
42
+ const identity = parseArtifactName(artifact);
43
+ 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;
50
+ const { section, invalidEntries } = inspectActivityLog(taskContent);
51
+ 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}`));
55
+ }
56
+ //# sourceMappingURL=completion-facts.js.map
@@ -3,6 +3,7 @@ import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
3
3
  import { VERSION } from '../version.js';
4
4
  import { applyWorkflowWarningIntent } from './workflow-warning-intents.js';
5
5
  import { createLocalTask } from './create.js';
6
+ import { TaskExecutionLockError } from './task-execution-lock.js';
6
7
  import { verifyTaskEvent } from './verification.js';
7
8
  const DEFAULT_DEPENDENCIES = {
8
9
  createIssue: createPlatformIssue,
@@ -48,9 +49,10 @@ export function parseTaskCreateResult(value) {
48
49
  if (!['applied', 'no-op', 'degraded', 'failed', 'blocked'].includes(result.status)
49
50
  || typeof result.changed !== 'boolean'
50
51
  || !result.task || typeof result.task !== 'object' || Array.isArray(result.task)
51
- || !exactKeys(result.task, ['id', 'shortId'])
52
+ || !exactKeys(result.task, ['id', 'shortId', 'state'])
52
53
  || !validTaskReference(result.task.id)
53
54
  || !validTaskReference(result.task.shortId)
55
+ || ![null, 'active', 'blocked', 'completed', 'archive'].includes(result.task.state)
54
56
  || !Array.isArray(result.operations)
55
57
  || !result.operations.every((operation) => {
56
58
  if (!operation || typeof operation !== 'object' || Array.isArray(operation))
@@ -93,7 +95,7 @@ export function taskCreateFailure(error, control) {
93
95
  return {
94
96
  status: error.retryable ? 'blocked' : 'failed',
95
97
  changed: false,
96
- task: { id: null, shortId: null },
98
+ task: { id: null, shortId: null, state: null },
97
99
  issue: null,
98
100
  operations: [],
99
101
  warnings: [],
@@ -150,12 +152,16 @@ async function createTask(value, options) {
150
152
  }
151
153
  catch (error) {
152
154
  const message = error instanceof Error ? error.message : String(error);
153
- const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
154
- return taskCreateFailure({ code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' });
155
+ const lockBusy = error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY';
156
+ const code = lockBusy ? error.code : /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
157
+ return taskCreateFailure({ code, message, retryable: lockBusy || code === 'TASK_CREATE_LOCK_TIMEOUT' });
155
158
  }
156
159
  const operations = [{
157
160
  name: 'task:local', status: local.status, reasonCode: null
158
161
  }];
162
+ if (local.status === 'no-op' && local.task.state !== 'active') {
163
+ return { status: 'no-op', changed: false, task: local.task, issue: null, operations, warnings: [], error: null };
164
+ }
159
165
  let issue = null;
160
166
  const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
161
167
  operations.push(...platformOperations('platform-create', created.operations));
@@ -199,7 +205,7 @@ async function createTask(value, options) {
199
205
  task: local.task, issue, operations, warnings: [], error: verified.error
200
206
  };
201
207
  return {
202
- status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
208
+ status: 'degraded',
203
209
  changed: local.changed,
204
210
  task: local.task,
205
211
  issue,