@fitlab-ai/agent-infra 0.9.15 → 0.10.0

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 (184) hide show
  1. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  3. package/dist/lib/fs/durable-write.js +2 -0
  4. package/dist/lib/internal/cli-route-inventory.js +1 -1
  5. package/dist/lib/internal/manual-validation.js +17 -8
  6. package/dist/lib/internal/task-artifact.js +1 -1
  7. package/dist/lib/internal/task-lifecycle.js +2 -2
  8. package/dist/lib/internal/task-operation-registry.js +0 -1
  9. package/dist/lib/internal/task-review.js +4 -4
  10. package/dist/lib/platform/issue-comments.js +16 -10
  11. package/dist/lib/platform/issues.js +1 -1
  12. package/dist/lib/platform/pr-change-report.js +5 -4
  13. package/dist/lib/platform/pr-summary.js +64 -32
  14. package/dist/lib/platform/pull-requests.js +3 -3
  15. package/dist/lib/platform/task-identities.js +2 -9
  16. package/dist/lib/platform/verification-sync.js +6 -2
  17. package/dist/lib/pr-review/evidence-grading.js +4 -8
  18. package/dist/lib/sandbox/commands/rm.js +21 -2
  19. package/dist/lib/sandbox/control/server.js +29 -9
  20. package/dist/lib/sandbox/control/state.js +7 -1
  21. package/dist/lib/sandbox/control/workflow-executor.js +4 -14
  22. package/dist/lib/sandbox/index.js +1 -2
  23. package/dist/lib/sandbox/removal.js +116 -1011
  24. package/dist/lib/sandbox/workspace-identity.js +17 -1
  25. package/dist/lib/task/artifact-command.js +6 -28
  26. package/dist/lib/task/artifact-operations.js +16 -230
  27. package/dist/lib/task/artifact-recovery.js +430 -0
  28. package/dist/lib/task/artifact-repair-intent.js +73 -39
  29. package/dist/lib/task/commit-operation.js +31 -4
  30. package/dist/lib/task/control-authority.js +38 -7
  31. package/dist/lib/task/control-recovery.js +14 -2
  32. package/dist/lib/task/create-service.js +1 -8
  33. package/dist/lib/task/delegation-receipts.js +64 -3
  34. package/dist/lib/task/events.js +164 -22
  35. package/dist/lib/task/human-override.js +5 -8
  36. package/dist/lib/task/intermediate-cleanup.js +66 -8
  37. package/dist/lib/task/lifecycle-recovery.js +549 -0
  38. package/dist/lib/task/lifecycle.js +12 -3
  39. package/dist/lib/task/local-artifact-finalization.js +148 -123
  40. package/dist/lib/task/manual-validation-receipt.js +5 -1
  41. package/dist/lib/task/orchestration.js +52 -2
  42. package/dist/lib/task/pr-delivery-fact.js +12 -56
  43. package/dist/lib/task/recovery-warning.js +19 -0
  44. package/dist/lib/task/review-command.js +1 -0
  45. package/dist/lib/task/review-finalization.js +104 -77
  46. package/dist/lib/task/short-id.js +15 -7
  47. package/dist/lib/task/task-execution-lock.js +1 -1
  48. package/dist/lib/task/verification-engine.js +3 -14
  49. package/dist/lib/task/workflow-command.js +0 -1
  50. package/dist/lib/version.js +1 -11
  51. package/dist/lifecycle-build-manifest.json +3 -3
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
  53. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  54. package/lib/fs/durable-write.ts +1 -0
  55. package/lib/internal/cli-route-inventory.ts +1 -1
  56. package/lib/internal/manual-validation.ts +17 -8
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +4 -3
  59. package/lib/internal/task-operation-registry.ts +0 -1
  60. package/lib/internal/task-review.ts +4 -4
  61. package/lib/platform/issue-comments.ts +18 -10
  62. package/lib/platform/issues.ts +1 -1
  63. package/lib/platform/pr-change-report.ts +7 -5
  64. package/lib/platform/pr-summary.ts +64 -32
  65. package/lib/platform/pull-requests.ts +3 -3
  66. package/lib/platform/task-identities.ts +3 -14
  67. package/lib/platform/verification-sync.ts +6 -2
  68. package/lib/pr-review/evidence-grading.ts +4 -9
  69. package/lib/sandbox/commands/rm.ts +34 -2
  70. package/lib/sandbox/control/protocol.ts +5 -0
  71. package/lib/sandbox/control/server.ts +30 -10
  72. package/lib/sandbox/control/state.ts +9 -1
  73. package/lib/sandbox/control/workflow-executor.ts +4 -14
  74. package/lib/sandbox/index.ts +1 -2
  75. package/lib/sandbox/removal.ts +126 -1128
  76. package/lib/sandbox/workspace-identity.ts +19 -3
  77. package/lib/task/artifact-command.ts +8 -27
  78. package/lib/task/artifact-operations.ts +21 -251
  79. package/lib/task/artifact-recovery.ts +519 -0
  80. package/lib/task/artifact-repair-intent.ts +99 -51
  81. package/lib/task/commit-operation.ts +35 -4
  82. package/lib/task/control-authority.ts +47 -14
  83. package/lib/task/control-recovery.ts +13 -1
  84. package/lib/task/create-service.ts +1 -10
  85. package/lib/task/delegation-receipts.ts +73 -2
  86. package/lib/task/events.ts +187 -23
  87. package/lib/task/human-override.ts +5 -8
  88. package/lib/task/intermediate-cleanup.ts +50 -12
  89. package/lib/task/lifecycle-recovery.ts +691 -0
  90. package/lib/task/lifecycle.ts +13 -4
  91. package/lib/task/local-artifact-finalization.ts +176 -132
  92. package/lib/task/manual-validation-receipt.ts +5 -1
  93. package/lib/task/orchestration.ts +65 -0
  94. package/lib/task/pr-delivery-fact.ts +15 -62
  95. package/lib/task/recovery-warning.ts +21 -0
  96. package/lib/task/review-command.ts +1 -0
  97. package/lib/task/review-finalization.ts +106 -87
  98. package/lib/task/short-id.ts +27 -5
  99. package/lib/task/task-execution-lock.ts +1 -1
  100. package/lib/task/verification-engine.ts +3 -17
  101. package/lib/task/verification-types.ts +0 -2
  102. package/lib/task/workflow-command.ts +0 -1
  103. package/lib/version.ts +1 -15
  104. package/package.json +1 -1
  105. package/templates/.agents/rules/create-issue.en.md +1 -1
  106. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  107. package/templates/.agents/rules/issue-sync.en.md +4 -2
  108. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  109. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  110. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  111. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  112. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  113. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  114. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  115. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  116. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  117. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  118. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  119. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  120. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  121. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  122. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  123. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  124. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  125. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  126. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  127. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  131. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  132. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  133. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  134. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  135. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  136. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  137. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  138. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  139. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  140. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  141. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  142. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  143. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  144. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  145. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  146. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  147. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  148. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  149. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  150. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  151. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  152. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  153. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  156. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  157. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  158. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  159. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  160. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  161. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  162. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  163. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  164. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  165. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  166. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  167. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  168. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  169. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  170. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  171. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  172. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  173. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  174. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  175. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  176. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  177. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  178. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  179. package/templates/.agents/templates/task.en.md +1 -1
  180. package/templates/.agents/templates/task.zh-CN.md +1 -1
  181. package/templates/.claude/commands/import-issue.en.md +1 -1
  182. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  183. package/templates/.claude/commands/restore-task.en.md +1 -1
  184. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
@@ -22,13 +22,14 @@ function copyRegular(source, destination, mode) {
22
22
  function controlFromEnvironment() {
23
23
  const token = process.env.AGENT_INFRA_CONTROL_TOKEN;
24
24
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
25
+ const rootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
25
26
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
26
27
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
27
28
  const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
28
- if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
29
+ if (!token || !generation || !rootId || !channelDir || !statusDir || !runtimeDir) {
29
30
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
30
31
  }
31
- return { token, generation, channelDir, statusDir, runtimeDir };
32
+ return { token, generation, rootId, channelDir, statusDir, runtimeDir };
32
33
  }
33
34
  function verifyRuntime(runtimeDir) {
34
35
  let stat;
@@ -188,6 +189,9 @@ function isolatedEnvironment(home, shimDir, contextPath, taskId, control, provid
188
189
  AGENT_INFRA_TASK_ID: taskId,
189
190
  AGENT_INFRA_CONTROL_TOKEN: control.token,
190
191
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
192
+ ...(control.rootId || sourceEnvironment.AGENT_INFRA_CONTROL_ROOT_ID
193
+ ? { AGENT_INFRA_CONTROL_ROOT_ID: control.rootId ?? sourceEnvironment.AGENT_INFRA_CONTROL_ROOT_ID }
194
+ : {}),
191
195
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
192
196
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
193
197
  AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
@@ -6,9 +6,19 @@ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js'
6
6
  const LOCK_RETRY_MS = 10;
7
7
  const LOCK_TIMEOUT_MS = 1_000;
8
8
  const LOCK_STALE_MS = 30_000;
9
+ const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
9
10
  function digest(value) {
10
11
  return crypto.createHash('sha256').update(value).digest('hex');
11
12
  }
13
+ function isRecoveryConsumer(value) {
14
+ return RECOVERY_CONSUMER_RE.test(value);
15
+ }
16
+ function recoveryConsumer(taskId, receiptId) {
17
+ if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
18
+ throw new Error('Codex lifecycle recovery claim identity is invalid');
19
+ }
20
+ return `lifecycle-recovery:${taskId}:${receiptId}`;
21
+ }
12
22
  function readRecord(file) {
13
23
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
14
24
  if (value.schemaVersion !== 1
@@ -202,11 +212,13 @@ function createCodexLifecycleStore(options) {
202
212
  }
203
213
  function read(childThreadId) {
204
214
  const matches = findByChild(childThreadId);
205
- if (matches.length !== 1)
215
+ if (matches.length === 0)
206
216
  throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
217
+ if (matches.length > 1)
218
+ throw new Error(`Codex lifecycle child '${childThreadId}' is ambiguous`);
207
219
  return readRecord(matches[0]);
208
220
  }
209
- function consume(childThreadId, consumer, expectedHookDefinitionHash) {
221
+ function consumeInternal(childThreadId, consumer, expectedHookDefinitionHash) {
210
222
  if (!consumer.trim())
211
223
  throw new Error('Codex lifecycle consumer is required');
212
224
  return withWriteLock(() => {
@@ -240,6 +252,32 @@ function createCodexLifecycleStore(options) {
240
252
  return next;
241
253
  });
242
254
  }
255
+ function consume(childThreadId, consumer, expectedHookDefinitionHash) {
256
+ if (!consumer.trim())
257
+ throw new Error('Codex lifecycle consumer is required');
258
+ if (consumer.startsWith('lifecycle-recovery:')) {
259
+ throw new Error('Codex lifecycle recovery claims require claimRecovery');
260
+ }
261
+ return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
262
+ }
263
+ function claimRecovery(childThreadId, taskId, receiptId, expectedHookDefinitionHash) {
264
+ return consumeInternal(childThreadId, recoveryConsumer(taskId, receiptId), expectedHookDefinitionHash);
265
+ }
266
+ function releaseRecovery(childThreadId, consumer) {
267
+ if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer))
268
+ return false;
269
+ return withWriteLock(() => {
270
+ const matches = findByChild(childThreadId);
271
+ if (matches.length !== 1)
272
+ return false;
273
+ const file = matches[0];
274
+ const current = readRecord(file);
275
+ if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt)
276
+ return false;
277
+ fs.unlinkSync(file);
278
+ return true;
279
+ });
280
+ }
243
281
  function expireBefore(cutoff) {
244
282
  return withWriteLock(() => {
245
283
  let changed = 0;
@@ -247,6 +285,8 @@ function createCodexLifecycleStore(options) {
247
285
  const current = readRecord(file);
248
286
  if (current.updatedAt >= cutoff)
249
287
  continue;
288
+ if (current.consumer && isRecoveryConsumer(current.consumer))
289
+ continue;
250
290
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
251
291
  fs.unlinkSync(file);
252
292
  changed += 1;
@@ -266,7 +306,7 @@ function createCodexLifecycleStore(options) {
266
306
  return changed;
267
307
  });
268
308
  }
269
- return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
309
+ return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
270
310
  }
271
311
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
272
312
  //# sourceMappingURL=store.js.map
@@ -2,6 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  export function fsyncDirectory(directory) {
5
+ if (process.platform === 'win32')
6
+ return;
5
7
  const descriptor = fs.openSync(directory, 'r');
6
8
  try {
7
9
  fs.fsyncSync(descriptor);
@@ -54,7 +54,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
54
54
  'task-ledger': ['decision-next-id', 'stage-status', 'finding-upsert', 'finding-respond', 'finding-review', 'decision-upsert', 'rework-intent-upsert'],
55
55
  'task-warning': ['list', 'add', 'set-status'],
56
56
  'task-activity': ['pr-review-inspect', 'pr-review-start', 'pr-review-complete', 'pr-review-terminate'],
57
- 'task-artifact': ['inspect', 'init', 'repair', 'finalize-local'],
57
+ 'task-artifact': ['inspect', 'init', 'finalize-local'],
58
58
  'task-orchestration': ['status', 'progress'],
59
59
  'task-review': ['finalize-summary'],
60
60
  'task-event': ['event'],
@@ -22,15 +22,24 @@ function printFailure(format, error) {
22
22
  process.stderr.write(`${error.code}: ${error.message}\n`);
23
23
  process.exitCode = 1;
24
24
  }
25
+ const MANUAL_VALIDATION_STATUS_SECTION = /^###\s+(?:⚠️\s+(?:需人工校验|Manual Validation Required)|✅\s+(?:人工验证已通过|无需人工校验|Manual Validation Passed|No Manual Validation Required)|⏳\s+(?:人工验证待收尾|Manual Validation Pending))\s*$[\s\S]*?(?=^#{1,3}\s|(?![\s\S]))/gmu;
25
26
  function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
26
- const withoutPrevious = body
27
- .replace(/^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/gmu, '')
28
- .replace(/^###\s+⏳\s+(?:Manual Validation Pending|人工验证待收尾)\s*$/gmu, '')
29
- .replace(/Manual validation passed(?:\s+→)?/giu, 'Manual validation pending');
27
+ const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
30
28
  const section = phase === 'pending'
31
- ? `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
32
- : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
33
- return `${withoutPrevious.replace(/\s+$/u, '')}\n\n${section}\n`;
29
+ ? chinese
30
+ ? `### ⏳ 人工验证待收尾\n\n人工验证证据已暂存;事务 ${transactionId} 正在等待最终完成。`
31
+ : `### ⏳ Manual Validation Pending\n\nManual validation evidence is staged; transaction ${transactionId} is awaiting final promotion.`
32
+ : chinese
33
+ ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
34
+ : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
35
+ let inserted = false;
36
+ const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
37
+ if (inserted)
38
+ return '';
39
+ inserted = true;
40
+ return `${section}\n\n`;
41
+ });
42
+ return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
34
43
  }
35
44
  function openManualValidationStarted(taskMdPath) {
36
45
  let content;
@@ -242,7 +251,7 @@ async function executeManualValidationTransactionLocked(taskRef, values, cwd, re
242
251
  if (!['applied', 'no-op'].includes(promoted.status))
243
252
  transactionFailure(promoted.error ?? { code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary promotion failed' });
244
253
  const postWrite = await summaryCommentState(taskRef, { cwd, client: options.client });
245
- if (!postWrite.comment?.body.includes('### ✅ Manual Validation Passed') || postWrite.pullRequest?.head.sha !== receipt.prHeadSha || !manualValidationFinalSummaryProjectionMatches(postWrite.comment.body, receipt))
254
+ if (!/^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.test(postWrite.comment?.body ?? '') || postWrite.pullRequest?.head.sha !== receipt.prHeadSha || !manualValidationFinalSummaryProjectionMatches(postWrite.comment?.body ?? '', receipt))
246
255
  transactionFailure({ code: 'MANUAL_VALIDATION_TRANSACTION_RECOVERY_REQUIRED', message: 'final summary post-write verification failed' });
247
256
  transitionAndPersist('committed', { postWriteVerified: true });
248
257
  return result('applied', null, { transaction, receipt, idempotent: false });
@@ -1,6 +1,6 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
3
- const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> repair --family <family> --artifact <artifact> --expected-sha256 <sha256> --expected-semantic-digest <digest>\n agent-infra-internal task-artifact <N | TASK-id> finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, repair, or finalize a workflow artifact without changing task state.\n`;
3
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> finalize-local --family <analysis|plan|code> --artifact <artifact> [--recovery-id <id>]\n\nInspect, initialize, or finalize a workflow artifact without changing task state.\n`;
4
4
  function taskArtifact(args = []) {
5
5
  if (!ensureInternalHandlerRoute('task-artifact', args))
6
6
  return;
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
2
2
  import { lifecycleIntentCatalog } from '../task/lifecycle.js';
3
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
5
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --stage <stage> --round <round> --artifact <artifact> --reason <reason>\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
6
6
  function usageFailure(message) {
7
7
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
8
8
  process.stderr.write(USAGE);
@@ -54,7 +54,7 @@ async function taskLifecycle(args = []) {
54
54
  }
55
55
  const result = await dispatchTaskControlOperation(createDirectHostExecutionContext({ repoRoot }), operation);
56
56
  process.stdout.write(`${JSON.stringify(result)}\n`);
57
- if (result.status === 'failed')
57
+ if (result.status === 'failed' || result.status === 'owner-unknown' || result.status === 'conflict')
58
58
  process.exitCode = 1;
59
59
  }
60
60
  export { taskLifecycle };
@@ -96,7 +96,6 @@ function internalTaskRoutes() {
96
96
  descriptor('internal', 'task-activity', 'pr-review-terminate', 'task-bound', 'progress'),
97
97
  descriptor('internal', 'task-artifact', 'inspect', 'task-bound', 'diagnostic'),
98
98
  descriptor('internal', 'task-artifact', 'init', 'task-bound', 'artifact-write'),
99
- descriptor('internal', 'task-artifact', 'repair', 'task-bound', 'artifact-write'),
100
99
  descriptor('internal', 'task-artifact', 'finalize-local', 'task-bound', 'artifact-write'),
101
100
  descriptor('internal', 'task-orchestration', 'status', 'task-bound', 'diagnostic'),
102
101
  descriptor('internal', 'task-orchestration', 'progress', 'task-bound', 'progress'),
@@ -4,7 +4,7 @@ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
6
6
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
7
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
7
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> finalize-summary --stage <analysis|plan|code> --artifact <review-*.md> [--recovery-id <id>] [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
8
8
  function failUsage(message) {
9
9
  process.stdout.write(`${JSON.stringify({
10
10
  status: 'failed',
@@ -30,7 +30,7 @@ async function taskReview(args = []) {
30
30
  failUsage(error instanceof Error ? error.message : String(error));
31
31
  return;
32
32
  }
33
- const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
33
+ const { stage, artifact, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
34
34
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
35
35
  if (dryRunConflict) {
36
36
  failUsage(dryRunConflict.message);
@@ -45,7 +45,7 @@ async function taskReview(args = []) {
45
45
  let humanOverride = null;
46
46
  try {
47
47
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', async () => {
48
- let current = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
48
+ let current = finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
49
49
  if (current.status !== 'failed' || !overrideTicket)
50
50
  return current;
51
51
  if (!overrideTarget || !overrideScope) {
@@ -58,7 +58,7 @@ async function taskReview(args = []) {
58
58
  target: overrideTarget, scope: overrideScope
59
59
  }, {
60
60
  effectExecutor: (capability) => {
61
- const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
61
+ const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
62
62
  current = retried;
63
63
  return retried.status === 'failed' || retried.status === 'planned'
64
64
  ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
@@ -18,6 +18,7 @@ const MARKERS = {
18
18
  summary: (taskId) => `<!-- sync-issue:${taskId}:summary -->`,
19
19
  cancel: (taskId) => `<!-- sync-issue:${taskId}:cancel -->`
20
20
  };
21
+ const COMMENT_BYTE_LIMIT = 60_000;
21
22
  const ARTIFACT_TITLES = {
22
23
  analysis: '需求分析',
23
24
  'review-analysis': '需求分析审查',
@@ -32,6 +33,9 @@ const ARTIFACT_TITLES = {
32
33
  function normalizeCommentContent(content) {
33
34
  return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
34
35
  }
36
+ function isTaskCommentTooLarge(content) {
37
+ return Buffer.byteLength(content, 'utf8') > COMMENT_BYTE_LIMIT;
38
+ }
35
39
  function splitFrontmatter(content) {
36
40
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
37
41
  if (!match)
@@ -219,7 +223,7 @@ function buildArtifactChunk(taskId, stem, title, agent, content, part, total, ch
219
223
  return { marker, body, content, part, total };
220
224
  }
221
225
  function chunkArtifactComment(input) {
222
- const byteLimit = input.byteLimit || 60_000;
226
+ const byteLimit = input.byteLimit || COMMENT_BYTE_LIMIT;
223
227
  const identity = artifactIdentity(input.artifact);
224
228
  const safeBody = sanitizeCommentBody(input.body, 'artifact body');
225
229
  const single = buildArtifactChunk(input.taskId, identity.stem, identity.title, input.agent, safeBody, 1, 1, false, Boolean(input.backfill));
@@ -257,11 +261,6 @@ function flattenComments(value) {
257
261
  const flattened = value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
258
262
  return flattened.filter((entry) => Boolean(entry && typeof entry === 'object' && (typeof entry.id === 'number' || typeof entry.id === 'string') && typeof entry.body === 'string'));
259
263
  }
260
- function issueNumberFromTask(content) {
261
- const value = parseTaskFrontmatter(content).issue_number;
262
- const number = Number(value);
263
- return Number.isInteger(number) && number > 0 ? number : null;
264
- }
265
264
  function listRemoteComments(client, repo, issue, cwd) {
266
265
  const result = client.json([
267
266
  'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
@@ -374,14 +373,21 @@ async function syncPlatformComment(taskRef, options) {
374
373
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
375
374
  let issueIdentityFromTask;
376
375
  try {
377
- issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent), undefined, options.runtimeVersion);
376
+ issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent));
378
377
  }
379
378
  catch (error) {
380
379
  return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } });
381
380
  }
382
381
  if (!issueIdentityFromTask) {
383
382
  return platformResult('no-op', {
384
- error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
383
+ error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
384
+ });
385
+ }
386
+ if (options.kind === 'task' && isTaskCommentTooLarge(taskContent)) {
387
+ return platformResult('no-op', {
388
+ resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
389
+ operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
390
+ error: null
385
391
  });
386
392
  }
387
393
  let desired;
@@ -541,7 +547,7 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
541
547
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
542
548
  let issueIdentity;
543
549
  try {
544
- issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content), undefined, options.runtimeVersion);
550
+ issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content));
545
551
  }
546
552
  catch (error) {
547
553
  return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } });
@@ -572,5 +578,5 @@ async function checkPlatformCommentOwner(taskRef, options = {}) {
572
578
  }
573
579
  return platformResult('no-op', { ...contextFields(context), resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentity), identity: issueIdentity }, error: null });
574
580
  }
575
- export { MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, syncPlatformComment, validateRelatedMarkerSet, writeComment };
581
+ export { COMMENT_BYTE_LIMIT, MARKERS, chunkArtifactComment, findMarkerComments, listRemoteComments, checkPlatformCommentOwner, listPlatformComments, normalizeCommentContent, renderTaskComment, isTaskCommentTooLarge, syncPlatformComment, validateRelatedMarkerSet, writeComment };
576
582
  //# sourceMappingURL=issue-comments.js.map
@@ -32,7 +32,7 @@ async function resolvedContext(taskRef, options) {
32
32
  const frontmatter = parseTaskFrontmatter(content);
33
33
  let issueIdentity;
34
34
  try {
35
- issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion);
35
+ issueIdentity = taskIssueIdentity(frontmatter);
36
36
  }
37
37
  catch (error) {
38
38
  return { ok: false, output: result('failed', resolved.taskId, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
@@ -3,8 +3,9 @@ import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { escapeHtmlText } from './comment-safety.js';
6
+ import { isResourceIdentity } from './resource-identity.js';
6
7
  const REPORT_FILE_NAME = 'pr-change-report.json';
7
- const REPORT_VERSION = 1;
8
+ const REPORT_VERSION = 2;
8
9
  const CANONICAL_REPORT_PLACEHOLDER = '<!-- canonical-pr-change-report -->';
9
10
  const CANONICAL_REPORT_HEADING = '### PR 代码增减';
10
11
  const PRECHECK_IDS = [
@@ -211,8 +212,8 @@ function validatePrChangeReport(value) {
211
212
  }
212
213
  const identity = value.identity;
213
214
  const identityRepository = identity.repository;
214
- const identityNumber = identity.number;
215
- if (!isNonEmptyString(identityRepository) || typeof identityNumber !== 'number' || !Number.isSafeInteger(identityNumber) || identityNumber <= 0)
215
+ const identityResource = identity.resource;
216
+ if (!isNonEmptyString(identityRepository) || !isResourceIdentity(identityResource))
216
217
  return invalid('change report identity is invalid');
217
218
  const parseIdentityPart = (part, name) => {
218
219
  if (!isRecord(part) || !isNonEmptyString(part.repository) || !isNonEmptyString(part.ref) || !isSha(part.sha))
@@ -259,7 +260,7 @@ function validatePrChangeReport(value) {
259
260
  version: REPORT_VERSION,
260
261
  identity: {
261
262
  repository: identityRepository,
262
- number: identityNumber,
263
+ resource: identityResource,
263
264
  base: base.value,
264
265
  head: head.value
265
266
  },
@@ -11,6 +11,7 @@ import { normalizeCommentContent } from './issue-comments.js';
11
11
  import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
12
12
  import { platformResult } from './types.js';
13
13
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
14
+ import { resourceIdentityEquals } from './resource-identity.js';
14
15
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
15
16
  import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
16
17
  import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
@@ -48,7 +49,8 @@ function readBoundTaskSnapshot(taskMdPath) {
48
49
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
49
50
  const fact = readPrDeliveryFact(frontmatter);
50
51
  const boundFact = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact : null;
51
- return { taskContent, boundFact };
52
+ const factError = fact.status === 'invalid' ? { code: fact.error.code, message: fact.error.message } : null;
53
+ return { taskContent, boundFact, factError };
52
54
  }
53
55
  function boundPrNumberForWarning(taskMdPath) {
54
56
  try {
@@ -117,7 +119,7 @@ function isHardSummaryFailure(error) {
117
119
  function inspectionError(error) {
118
120
  return error.retryable ? 'blocked' : 'failed';
119
121
  }
120
- async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContext) {
122
+ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedContext) {
121
123
  if (!context.platform.repository) {
122
124
  return { ok: false, status: context.status, error: context.error };
123
125
  }
@@ -126,16 +128,15 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
126
128
  : await resolvePlatformProviderContext({ cwd: repoRoot });
127
129
  if (!loaded.ok)
128
130
  return { ok: false, status: loaded.context.status, error: loaded.context.error };
129
- const target = providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber));
130
131
  const inspected = loaded.value.provider.changeRequests?.inspect
131
- ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target })
132
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: prIdentity })
132
133
  : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
133
134
  if (!inspected.ok) {
134
135
  const error = providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED');
135
136
  return { ok: false, status: inspectionError(error), error };
136
137
  }
137
138
  const remote = inspected.value;
138
- const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? prNumber;
139
+ const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
139
140
  const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
140
141
  const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
141
142
  return {
@@ -143,6 +144,7 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
143
144
  value: {
144
145
  repository: context.platform.repository,
145
146
  number,
147
+ identity: remote.identity ?? prIdentity,
146
148
  nodeId: remote.id,
147
149
  url: remote.displayUrl || '',
148
150
  state: remote.state === 'closed' ? 'closed' : 'open',
@@ -163,20 +165,22 @@ async function inspectBoundPullRequest(context, repoRoot, prNumber, loadedContex
163
165
  function sameIdentity(left, right) {
164
166
  return JSON.stringify({
165
167
  repository: left.repository,
166
- number: left.number,
168
+ identity: left.identity ?? null,
167
169
  base: left.base,
168
170
  head: left.head
169
171
  }) === JSON.stringify({
170
172
  repository: right.repository,
171
- number: right.number,
173
+ identity: right.identity ?? null,
172
174
  base: right.base,
173
175
  head: right.head
174
176
  });
175
177
  }
176
178
  function identityFromSnapshot(snapshot) {
179
+ if (!snapshot.identity)
180
+ throw new Error('pull-request snapshot has no canonical identity');
177
181
  return {
178
182
  repository: snapshot.repository,
179
- number: snapshot.number,
183
+ resource: snapshot.identity,
180
184
  base: { ...snapshot.base },
181
185
  head: { ...snapshot.head }
182
186
  };
@@ -205,7 +209,7 @@ function currentReportCheck(report, snapshot, taskContent, repoRoot) {
205
209
  if (!digest.ok)
206
210
  return { ok: false, error: platformError(digest.error) };
207
211
  const reportIdentity = report.identity;
208
- if (reportIdentity.repository !== snapshot.repository || reportIdentity.number !== snapshot.number ||
212
+ if (reportIdentity.repository !== snapshot.repository || !resourceIdentityEquals(reportIdentity.resource, snapshot.identity) ||
209
213
  JSON.stringify(reportIdentity.base) !== JSON.stringify(snapshot.base) || JSON.stringify(reportIdentity.head) !== JSON.stringify(snapshot.head))
210
214
  return {
211
215
  ok: false,
@@ -263,14 +267,23 @@ async function summaryContext(taskRef, options = {}) {
263
267
  };
264
268
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
265
269
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
266
- const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
267
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? resourceIdentityNumber(fact.fact.identity.resource) : null;
268
270
  const reportPath = taskReportPath(resolved.taskDir);
271
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
272
+ if (fact.status === 'invalid')
273
+ return {
274
+ ...platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }),
275
+ task: { id: resolved.taskId, prNumber: null },
276
+ pullRequest: null,
277
+ changeReport: { status: 'invalid', path: reportPath, taskIntentSha256: null, reason: fact.error.message },
278
+ artifacts: []
279
+ };
280
+ const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
281
+ const prNumber = resourceIdentityNumber(prIdentity);
269
282
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
270
283
  const context = loaded.ok ? loaded.value.context : loaded.context;
271
284
  let pullRequest = null;
272
- if (loaded.ok && prNumber && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
273
- const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
285
+ if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
286
+ const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
274
287
  if (inspected.ok)
275
288
  pullRequest = inspected.value;
276
289
  }
@@ -290,14 +303,15 @@ async function summaryContext(taskRef, options = {}) {
290
303
  }
291
304
  async function summaryCommentState(taskRef, options = {}) {
292
305
  const context = await summaryContext(taskRef, options);
293
- if (!context.task.prNumber || !context.pullRequest)
306
+ const prIdentity = context.pullRequest?.identity;
307
+ if (!prIdentity)
294
308
  return { ...context, comment: null };
295
309
  const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
296
310
  if (!loaded.ok || !loaded.value.provider.comments?.list)
297
311
  return { ...context, comment: null };
298
312
  const listed = await loaded.value.provider.comments.list({
299
313
  context: providerOperationContext(loaded.value),
300
- parent: providerResourceToken(loaded.value.provider, 'pull-request', String(context.task.prNumber))
314
+ parent: prIdentity
301
315
  });
302
316
  if (!listed.ok)
303
317
  return { ...context, comment: null };
@@ -311,6 +325,21 @@ async function reportWrite(taskRef, options) {
311
325
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
312
326
  if (!resolved.ok)
313
327
  return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), report: null };
328
+ let preflight;
329
+ try {
330
+ preflight = readBoundTaskSnapshot(resolved.taskMdPath);
331
+ }
332
+ catch (error) {
333
+ return {
334
+ ...platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } }),
335
+ report: null
336
+ };
337
+ }
338
+ if (preflight.factError)
339
+ return {
340
+ ...platformResult('failed', { error: { code: preflight.factError.code, message: preflight.factError.message, retryable: false } }),
341
+ report: null
342
+ };
314
343
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
315
344
  const context = loaded.ok ? loaded.value.context : loaded.context;
316
345
  if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status))
@@ -318,20 +347,21 @@ async function reportWrite(taskRef, options) {
318
347
  let knownPrNumber = null;
319
348
  try {
320
349
  return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
321
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
322
- if (!boundFact)
350
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
351
+ if (factError)
323
352
  return {
324
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
353
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: factError.code, message: factError.message, retryable: false }),
325
354
  report: null
326
355
  };
327
- const boundPrNumber = resourceIdentityNumber(boundFact.identity.resource);
328
- if (!boundPrNumber)
356
+ if (!boundFact)
329
357
  return {
330
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false }),
358
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
331
359
  report: null
332
360
  };
361
+ const boundPrIdentity = boundFact.identity.resource;
362
+ const boundPrNumber = resourceIdentityNumber(boundPrIdentity);
333
363
  knownPrNumber = boundPrNumber;
334
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
364
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
335
365
  if (!initial.ok)
336
366
  return { ...basePlatformResult(initial.status, context, resolved.taskId, boundPrNumber, initial.error), report: null };
337
367
  const digest = taskIntentDigest(taskContent);
@@ -364,7 +394,7 @@ async function reportWrite(taskRef, options) {
364
394
  const built = buildPrChangeReport(identityFromSnapshot(initial.value), digest.value.sha256, mechanical.value, candidate.value);
365
395
  if (!built.ok)
366
396
  return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(built.error)), report: null };
367
- const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
397
+ const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
368
398
  if (!final.ok)
369
399
  return { ...basePlatformResult(final.status, context, resolved.taskId, boundPrNumber, final.error), report: null };
370
400
  if (!sameIdentity(initial.value, final.value))
@@ -466,7 +496,12 @@ async function syncPullRequestSummary(taskRef, options) {
466
496
  }
467
497
  try {
468
498
  const execute = async () => {
469
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
499
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
500
+ if (factError)
501
+ return {
502
+ ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
503
+ result: null, warnings: []
504
+ };
470
505
  if (!boundFact)
471
506
  return {
472
507
  ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
@@ -474,14 +509,9 @@ async function syncPullRequestSummary(taskRef, options) {
474
509
  };
475
510
  const prIdentity = boundFact.identity.resource;
476
511
  const boundPrNumber = resourceIdentityNumber(prIdentity);
477
- if (!boundPrNumber)
478
- return {
479
- ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false } }),
480
- result: null, warnings: []
481
- };
482
512
  knownPrNumber = boundPrNumber;
483
513
  const prNumber = boundPrNumber;
484
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
514
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
485
515
  if (!initial.ok)
486
516
  return fail(initial.status, context, initial.error);
487
517
  const manual = options.manualValidation;
@@ -491,6 +521,8 @@ async function syncPullRequestSummary(taskRef, options) {
491
521
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
492
522
  return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
493
523
  if (manual) {
524
+ if (prNumber === null)
525
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
494
526
  if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
495
527
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
496
528
  if (manual.phase === 'final') {
@@ -570,7 +602,7 @@ async function syncPullRequestSummary(taskRef, options) {
570
602
  ? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
571
603
  : null;
572
604
  const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route };
573
- const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
605
+ const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
574
606
  if (!beforeWrite.ok)
575
607
  return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
576
608
  if (!sameIdentity(initial.value, beforeWrite.value))
@@ -605,7 +637,7 @@ async function syncPullRequestSummary(taskRef, options) {
605
637
  : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
606
638
  if (!written.ok)
607
639
  return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
608
- const after = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
640
+ const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
609
641
  const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
610
642
  const compensateWrittenSummary = async () => reconciliation.action === 'create'
611
643
  ? written.value.remoteId && loaded.value.provider.comments?.delete