@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
@@ -7,9 +7,10 @@ import {
7
7
  import { lifecycleIntentCatalog } from '../task/lifecycle.ts';
8
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
9
  import type { TaskLifecycleResult } from '../task/lifecycle.ts';
10
+ import type { LifecycleRecoveryResult } from '../task/lifecycle-recovery.ts';
10
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
11
12
 
12
- 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`;
13
+ 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`;
13
14
 
14
15
  function usageFailure(message: string): void {
15
16
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
@@ -61,9 +62,9 @@ async function taskLifecycle(args: string[] = []): Promise<void> {
61
62
  const result = await dispatchTaskControlOperation(
62
63
  createDirectHostExecutionContext({ repoRoot }),
63
64
  operation
64
- ) as TaskLifecycleResult & { humanOverride?: unknown };
65
+ ) as (TaskLifecycleResult | LifecycleRecoveryResult) & { humanOverride?: unknown };
65
66
  process.stdout.write(`${JSON.stringify(result)}\n`);
66
- if (result.status === 'failed') process.exitCode = 1;
67
+ if (result.status === 'failed' || result.status === 'owner-unknown' || result.status === 'conflict') process.exitCode = 1;
67
68
  }
68
69
 
69
70
  export { taskLifecycle };
@@ -136,7 +136,6 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
136
136
  descriptor('internal', 'task-activity', 'pr-review-terminate', 'task-bound', 'progress'),
137
137
  descriptor('internal', 'task-artifact', 'inspect', 'task-bound', 'diagnostic'),
138
138
  descriptor('internal', 'task-artifact', 'init', 'task-bound', 'artifact-write'),
139
- descriptor('internal', 'task-artifact', 'repair', 'task-bound', 'artifact-write'),
140
139
  descriptor('internal', 'task-artifact', 'finalize-local', 'task-bound', 'artifact-write'),
141
140
  descriptor('internal', 'task-orchestration', 'status', 'task-bound', 'diagnostic'),
142
141
  descriptor('internal', 'task-orchestration', 'progress', 'task-bound', 'progress'),
@@ -5,7 +5,7 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
5
5
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
6
6
  import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
7
7
 
8
- 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';
8
+ 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';
9
9
 
10
10
  function failUsage(message: string): void {
11
11
  process.stdout.write(`${JSON.stringify({
@@ -27,7 +27,7 @@ async function taskReview(args: string[] = []): Promise<void> {
27
27
  let request;
28
28
  try { request = parseReviewCommand(args); }
29
29
  catch (error) { failUsage(error instanceof Error ? error.message : String(error)); return; }
30
- const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
30
+ const { stage, artifact, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
31
31
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
32
32
  if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
33
33
  const resolved = resolveTaskRef(args[0]!);
@@ -36,7 +36,7 @@ async function taskReview(args: string[] = []): Promise<void> {
36
36
  let humanOverride: unknown = null;
37
37
  try {
38
38
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', async () => {
39
- let current = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
39
+ let current = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
40
40
  if (current.status !== 'failed' || !overrideTicket) return current;
41
41
  if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
42
42
  const consumed = await consumeHumanOverride({
@@ -45,7 +45,7 @@ async function taskReview(args: string[] = []): Promise<void> {
45
45
  target: overrideTarget, scope: overrideScope
46
46
  }, {
47
47
  effectExecutor: (capability) => {
48
- const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
48
+ const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
49
49
  current = retried;
50
50
  return retried.status === 'failed' || retried.status === 'planned'
51
51
  ? { 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'}` }
@@ -50,6 +50,7 @@ const MARKERS = {
50
50
  summary: (taskId: string) => `<!-- sync-issue:${taskId}:summary -->`,
51
51
  cancel: (taskId: string) => `<!-- sync-issue:${taskId}:cancel -->`
52
52
  };
53
+ const COMMENT_BYTE_LIMIT = 60_000;
53
54
 
54
55
  const ARTIFACT_TITLES: Record<string, string> = {
55
56
  analysis: '需求分析',
@@ -67,6 +68,10 @@ function normalizeCommentContent(content: string): string {
67
68
  return content.replace(/\r\n/g, '\n').replace(/\n+$/, '\n');
68
69
  }
69
70
 
71
+ function isTaskCommentTooLarge(content: string): boolean {
72
+ return Buffer.byteLength(content, 'utf8') > COMMENT_BYTE_LIMIT;
73
+ }
74
+
70
75
  function splitFrontmatter(content: string): { frontmatter: string | null; body: string } {
71
76
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
72
77
  if (!match) return { frontmatter: null, body: content };
@@ -269,7 +274,7 @@ function chunkArtifactComment(input: {
269
274
  byteLimit?: number;
270
275
  backfill?: boolean;
271
276
  }): RenderedChunk[] {
272
- const byteLimit = input.byteLimit || 60_000;
277
+ const byteLimit = input.byteLimit || COMMENT_BYTE_LIMIT;
273
278
  const identity = artifactIdentity(input.artifact);
274
279
  const safeBody = sanitizeCommentBody(input.body, 'artifact body');
275
280
  const single = buildArtifactChunk(
@@ -323,12 +328,6 @@ function flattenComments(value: unknown): RemoteComment[] {
323
328
  );
324
329
  }
325
330
 
326
- function issueNumberFromTask(content: string): number | null {
327
- const value = parseTaskFrontmatter(content).issue_number;
328
- const number = Number(value);
329
- return Number.isInteger(number) && number > 0 ? number : null;
330
- }
331
-
332
331
  function listRemoteComments(client: PlatformClient, repo: string, issue: number, cwd: string) {
333
332
  const result = client.json<unknown>([
334
333
  'api', '--paginate', '--slurp', `repos/${repo}/issues/${issue}/comments?per_page=100`
@@ -452,11 +451,18 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
452
451
  }
453
452
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
454
453
  let issueIdentityFromTask: ReturnType<typeof taskIssueIdentity>;
455
- try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent), undefined, options.runtimeVersion); }
454
+ try { issueIdentityFromTask = taskIssueIdentity(parseTaskFrontmatter(taskContent)); }
456
455
  catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
457
456
  if (!issueIdentityFromTask) {
458
457
  return platformResult('no-op', {
459
- error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid issue_number', retryable: false }
458
+ error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
459
+ });
460
+ }
461
+ if (options.kind === 'task' && isTaskCommentTooLarge(taskContent)) {
462
+ return platformResult('no-op', {
463
+ resource: { kind: 'issue', number: resourceIdentityNumber(issueIdentityFromTask), identity: issueIdentityFromTask },
464
+ operations: [{ name: `comment:${MARKERS.task(resolved.taskId)}`, status: 'skipped', reasonCode: 'COMMENT_PAYLOAD_TOO_LARGE' }],
465
+ error: null
460
466
  });
461
467
  }
462
468
  let desired: RenderedChunk[];
@@ -614,7 +620,7 @@ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: strin
614
620
  if (!resolved.ok) return platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } });
615
621
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
616
622
  let issueIdentity: ReturnType<typeof taskIssueIdentity>;
617
- try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content), undefined, options.runtimeVersion); }
623
+ try { issueIdentity = taskIssueIdentity(parseTaskFrontmatter(content)); }
618
624
  catch (error) { return platformResult('failed', { error: { ...taskIssueIdentityError(error), retryable: false } }); }
619
625
  if (!issueIdentity) return platformResult('no-op', { error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false } });
620
626
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
@@ -640,6 +646,7 @@ async function checkPlatformCommentOwner(taskRef: string, options: { cwd?: strin
640
646
  }
641
647
 
642
648
  export {
649
+ COMMENT_BYTE_LIMIT,
643
650
  MARKERS,
644
651
  chunkArtifactComment,
645
652
  findMarkerComments,
@@ -648,6 +655,7 @@ export {
648
655
  listPlatformComments,
649
656
  normalizeCommentContent,
650
657
  renderTaskComment,
658
+ isTaskCommentTooLarge,
651
659
  syncPlatformComment,
652
660
  validateRelatedMarkerSet,
653
661
  writeComment
@@ -85,7 +85,7 @@ async function resolvedContext(taskRef: string, options: SharedOptions) {
85
85
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
86
86
  const frontmatter = parseTaskFrontmatter(content);
87
87
  let issueIdentity: ResourceIdentity | null;
88
- try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
88
+ try { issueIdentity = taskIssueIdentity(frontmatter); }
89
89
  catch (error) {
90
90
  return { ok: false as const, output: result('failed', resolved.taskId, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) };
91
91
  }
@@ -3,9 +3,11 @@ 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.ts';
6
+ import { isResourceIdentity } from './resource-identity.ts';
7
+ import type { ResourceIdentity } from './resource-identity.ts';
6
8
 
7
9
  const REPORT_FILE_NAME = 'pr-change-report.json';
8
- const REPORT_VERSION = 1 as const;
10
+ const REPORT_VERSION = 2 as const;
9
11
  const CANONICAL_REPORT_PLACEHOLDER = '<!-- canonical-pr-change-report -->';
10
12
  const CANONICAL_REPORT_HEADING = '### PR 代码增减';
11
13
  const PRECHECK_IDS = [
@@ -83,7 +85,7 @@ type PrecheckCandidate = {
83
85
 
84
86
  type PullRequestIdentity = {
85
87
  repository: string;
86
- number: number;
88
+ resource: ResourceIdentity;
87
89
  base: { repository: string; ref: string; sha: string };
88
90
  head: { repository: string; ref: string; sha: string };
89
91
  };
@@ -279,8 +281,8 @@ function validatePrChangeReport(value: unknown): ValidationResult<PrChangeReport
279
281
  }
280
282
  const identity = value.identity;
281
283
  const identityRepository = identity.repository;
282
- const identityNumber = identity.number;
283
- if (!isNonEmptyString(identityRepository) || typeof identityNumber !== 'number' || !Number.isSafeInteger(identityNumber) || identityNumber <= 0) return invalid('change report identity is invalid');
284
+ const identityResource = identity.resource;
285
+ if (!isNonEmptyString(identityRepository) || !isResourceIdentity(identityResource)) return invalid('change report identity is invalid');
284
286
  const parseIdentityPart = (part: unknown, name: string): ValidationResult<{ repository: string; ref: string; sha: string }> => {
285
287
  if (!isRecord(part) || !isNonEmptyString(part.repository) || !isNonEmptyString(part.ref) || !isSha(part.sha)) return invalid(`change report identity.${name} is invalid`);
286
288
  return { ok: true, value: { repository: part.repository, ref: part.ref, sha: part.sha } };
@@ -315,7 +317,7 @@ function validatePrChangeReport(value: unknown): ValidationResult<PrChangeReport
315
317
  version: REPORT_VERSION,
316
318
  identity: {
317
319
  repository: identityRepository,
318
- number: identityNumber,
320
+ resource: identityResource,
319
321
  base: base.value,
320
322
  head: head.value
321
323
  },
@@ -16,6 +16,8 @@ import { platformResult } from './types.ts';
16
16
  import type { PlatformResult } from './types.ts';
17
17
  import type { OperationWarning } from '../task/operation-outcome.ts';
18
18
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.ts';
19
+ import { resourceIdentityEquals } from './resource-identity.ts';
20
+ import type { ResourceIdentity } from './resource-identity.ts';
19
21
  import type { PlatformChangeRequestSnapshot } from './snapshots.ts';
20
22
  import type { ChangeRequestSnapshot } from './provider-contract.ts';
21
23
  import {
@@ -102,12 +104,17 @@ function taskReportPath(taskDir: string): string {
102
104
  return path.join(taskDir, 'pr-change-report.json');
103
105
  }
104
106
 
105
- function readBoundTaskSnapshot(taskMdPath: string): { taskContent: string; boundFact: Extract<PrDeliveryFact, { state: 'bound' }> | null } {
107
+ function readBoundTaskSnapshot(taskMdPath: string): {
108
+ taskContent: string;
109
+ boundFact: Extract<PrDeliveryFact, { state: 'bound' }> | null;
110
+ factError: { code: string; message: string } | null;
111
+ } {
106
112
  const taskContent = fs.readFileSync(taskMdPath, 'utf8');
107
113
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
108
114
  const fact = readPrDeliveryFact(frontmatter);
109
115
  const boundFact = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact : null;
110
- return { taskContent, boundFact };
116
+ const factError = fact.status === 'invalid' ? { code: fact.error.code, message: fact.error.message } : null;
117
+ return { taskContent, boundFact, factError };
111
118
  }
112
119
 
113
120
  function boundPrNumberForWarning(taskMdPath: string): number | null {
@@ -189,7 +196,7 @@ function inspectionError(error: { code: string; message: string; retryable: bool
189
196
  async function inspectBoundPullRequest(
190
197
  context: PlatformResult,
191
198
  repoRoot: string,
192
- prNumber: number,
199
+ prIdentity: ResourceIdentity,
193
200
  loadedContext?: LoadedContext
194
201
  ): Promise<{ ok: true; value: PlatformChangeRequestSnapshot } | { ok: false; error: PlatformResult['error']; status: PlatformResult['status'] }> {
195
202
  if (!context.platform.repository) {
@@ -199,16 +206,15 @@ async function inspectBoundPullRequest(
199
206
  ? { ok: true as const, value: loadedContext }
200
207
  : await resolvePlatformProviderContext({ cwd: repoRoot });
201
208
  if (!loaded.ok) return { ok: false, status: loaded.context.status, error: loaded.context.error };
202
- const target = providerResourceToken(loaded.value.provider, 'pull-request', String(prNumber));
203
209
  const inspected = loaded.value.provider.changeRequests?.inspect
204
- ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target })
210
+ ? await loaded.value.provider.changeRequests.inspect({ context: providerOperationContext(loaded.value), target: prIdentity })
205
211
  : unsupportedProviderOperation(loaded.value.provider, 'changeRequests.inspect');
206
212
  if (!inspected.ok) {
207
213
  const error = providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED');
208
214
  return { ok: false, status: inspectionError(error), error };
209
215
  }
210
216
  const remote: ChangeRequestSnapshot = inspected.value;
211
- const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? prNumber;
217
+ const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
212
218
  const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
213
219
  const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
214
220
  return {
@@ -216,6 +222,7 @@ async function inspectBoundPullRequest(
216
222
  value: {
217
223
  repository: context.platform.repository,
218
224
  number,
225
+ identity: remote.identity ?? prIdentity,
219
226
  nodeId: remote.id,
220
227
  url: remote.displayUrl || '',
221
228
  state: remote.state === 'closed' ? 'closed' : 'open',
@@ -237,21 +244,22 @@ async function inspectBoundPullRequest(
237
244
  function sameIdentity(left: PlatformChangeRequestSnapshot, right: PlatformChangeRequestSnapshot): boolean {
238
245
  return JSON.stringify({
239
246
  repository: left.repository,
240
- number: left.number,
247
+ identity: left.identity ?? null,
241
248
  base: left.base,
242
249
  head: left.head
243
250
  }) === JSON.stringify({
244
251
  repository: right.repository,
245
- number: right.number,
252
+ identity: right.identity ?? null,
246
253
  base: right.base,
247
254
  head: right.head
248
255
  });
249
256
  }
250
257
 
251
258
  function identityFromSnapshot(snapshot: PlatformChangeRequestSnapshot) {
259
+ if (!snapshot.identity) throw new Error('pull-request snapshot has no canonical identity');
252
260
  return {
253
261
  repository: snapshot.repository,
254
- number: snapshot.number,
262
+ resource: snapshot.identity,
255
263
  base: { ...snapshot.base },
256
264
  head: { ...snapshot.head }
257
265
  };
@@ -286,7 +294,7 @@ function currentReportCheck(
286
294
  const digest = taskIntentDigest(taskContent);
287
295
  if (!digest.ok) return { ok: false, error: platformError(digest.error) };
288
296
  const reportIdentity = report.identity;
289
- if (reportIdentity.repository !== snapshot.repository || reportIdentity.number !== snapshot.number ||
297
+ if (reportIdentity.repository !== snapshot.repository || !resourceIdentityEquals(reportIdentity.resource, snapshot.identity) ||
290
298
  JSON.stringify(reportIdentity.base) !== JSON.stringify(snapshot.base) || JSON.stringify(reportIdentity.head) !== JSON.stringify(snapshot.head)) return {
291
299
  ok: false,
292
300
  error: { code: 'PR_CHANGE_REPORT_STALE', message: 'Change report identity does not match the authoritative pull request snapshot', retryable: false }
@@ -343,14 +351,22 @@ async function summaryContext(taskRef: string, options: SummaryOptions = {}): Pr
343
351
  };
344
352
  const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
345
353
  const frontmatter = parseTypedTaskFrontmatter(taskContent);
346
- const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
347
- const prNumber = fact.status === 'valid' && fact.fact.state === 'bound' ? resourceIdentityNumber(fact.fact.identity.resource) : null;
348
354
  const reportPath = taskReportPath(resolved.taskDir);
355
+ const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
356
+ if (fact.status === 'invalid') return {
357
+ ...platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }),
358
+ task: { id: resolved.taskId, prNumber: null },
359
+ pullRequest: null,
360
+ changeReport: { status: 'invalid', path: reportPath, taskIntentSha256: null, reason: fact.error.message },
361
+ artifacts: []
362
+ };
363
+ const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
364
+ const prNumber = resourceIdentityNumber(prIdentity);
349
365
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
350
366
  const context = loaded.ok ? loaded.value.context : loaded.context;
351
367
  let pullRequest: PlatformChangeRequestSnapshot | null = null;
352
- if (loaded.ok && prNumber && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
353
- const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
368
+ if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
369
+ const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
354
370
  if (inspected.ok) pullRequest = inspected.value;
355
371
  }
356
372
  const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
@@ -370,12 +386,13 @@ async function summaryContext(taskRef: string, options: SummaryOptions = {}): Pr
370
386
 
371
387
  async function summaryCommentState(taskRef: string, options: SummaryOptions = {}): Promise<SummaryCommentStateResult> {
372
388
  const context = await summaryContext(taskRef, options);
373
- if (!context.task.prNumber || !context.pullRequest) return { ...context, comment: null };
389
+ const prIdentity = context.pullRequest?.identity;
390
+ if (!prIdentity) return { ...context, comment: null };
374
391
  const loaded = await resolvePlatformProviderContext({ cwd: options.cwd, client: options.client });
375
392
  if (!loaded.ok || !loaded.value.provider.comments?.list) return { ...context, comment: null };
376
393
  const listed = await loaded.value.provider.comments.list({
377
394
  context: providerOperationContext(loaded.value),
378
- parent: providerResourceToken(loaded.value.provider, 'pull-request', String(context.task.prNumber))
395
+ parent: prIdentity
379
396
  });
380
397
  if (!listed.ok) return { ...context, comment: null };
381
398
  const matches = listed.value.filter((comment) => comment.body.includes(summaryMarker(context.task.id!)));
@@ -397,24 +414,38 @@ type ReportWriteOptions = {
397
414
  async function reportWrite(taskRef: string, options: ReportWriteOptions): Promise<ReportWriteResult> {
398
415
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
399
416
  if (!resolved.ok) return { ...platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }), report: null };
417
+ let preflight: ReturnType<typeof readBoundTaskSnapshot>;
418
+ try {
419
+ preflight = readBoundTaskSnapshot(resolved.taskMdPath);
420
+ } catch (error) {
421
+ return {
422
+ ...platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } }),
423
+ report: null
424
+ };
425
+ }
426
+ if (preflight.factError) return {
427
+ ...platformResult('failed', { error: { code: preflight.factError.code, message: preflight.factError.message, retryable: false } }),
428
+ report: null
429
+ };
400
430
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
401
431
  const context = loaded.ok ? loaded.value.context : loaded.context;
402
432
  if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) return { ...context, report: null };
403
433
  let knownPrNumber: number | null = null;
404
434
  try {
405
435
  return await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, options.agent, async () => {
406
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
407
- if (!boundFact) return {
408
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
436
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
437
+ if (factError) return {
438
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: factError.code, message: factError.message, retryable: false }),
409
439
  report: null
410
440
  };
411
- const boundPrNumber = resourceIdentityNumber(boundFact.identity.resource);
412
- if (!boundPrNumber) return {
413
- ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false }),
441
+ if (!boundFact) return {
442
+ ...basePlatformResult('failed', context, resolved.taskId, null, { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }),
414
443
  report: null
415
444
  };
445
+ const boundPrIdentity = boundFact.identity.resource;
446
+ const boundPrNumber = resourceIdentityNumber(boundPrIdentity);
416
447
  knownPrNumber = boundPrNumber;
417
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
448
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
418
449
  if (!initial.ok) return { ...basePlatformResult(initial.status, context, resolved.taskId, boundPrNumber, initial.error), report: null };
419
450
  const digest = taskIntentDigest(taskContent);
420
451
  if (!digest.ok) return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(digest.error)), report: null };
@@ -438,7 +469,7 @@ async function reportWrite(taskRef: string, options: ReportWriteOptions): Promis
438
469
  };
439
470
  const built = buildPrChangeReport(identityFromSnapshot(initial.value), digest.value.sha256, mechanical.value, candidate.value);
440
471
  if (!built.ok) return { ...basePlatformResult('failed', context, resolved.taskId, boundPrNumber, platformError(built.error)), report: null };
441
- const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
472
+ const final = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrIdentity, loaded.value);
442
473
  if (!final.ok) return { ...basePlatformResult(final.status, context, resolved.taskId, boundPrNumber, final.error), report: null };
443
474
  if (!sameIdentity(initial.value, final.value)) return {
444
475
  ...basePlatformResult('blocked', context, resolved.taskId, boundPrNumber, { code: 'PR_CHANGE_REPORT_HEAD_RACE', message: 'Pull request identity changed while generating the change report', retryable: true }),
@@ -535,26 +566,27 @@ async function syncPullRequestSummary(
535
566
  }
536
567
  try {
537
568
  const execute = async (): Promise<PullRequestSummaryResult> => {
538
- const { taskContent, boundFact } = readBoundTaskSnapshot(resolved.taskMdPath);
569
+ const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
570
+ if (factError) return {
571
+ ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
572
+ result: null, warnings: []
573
+ };
539
574
  if (!boundFact) return {
540
575
  ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
541
576
  result: null, warnings: []
542
577
  };
543
578
  const prIdentity = boundFact.identity.resource;
544
579
  const boundPrNumber = resourceIdentityNumber(prIdentity);
545
- if (!boundPrNumber) return {
546
- ...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NUMBER_INVALID', message: 'Bound pull request has no numeric identifier', retryable: false } }),
547
- result: null, warnings: []
548
- };
549
580
  knownPrNumber = boundPrNumber;
550
581
  const prNumber = boundPrNumber;
551
- const initial = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
582
+ const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
552
583
  if (!initial.ok) return fail(initial.status, context, initial.error);
553
584
  const manual = options.manualValidation;
554
585
  const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
555
586
  if (hasFinalManualValidation && (!manual || manual.phase !== 'final')) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
556
587
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation)) 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);
557
588
  if (manual) {
589
+ if (prNumber === null) return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
558
590
  if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha)) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
559
591
  if (manual.phase === 'final') {
560
592
  if (manual.authority !== 'coordinator') return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires coordinator authority', retryable: false }, prNumber);
@@ -621,7 +653,7 @@ async function syncPullRequestSummary(
621
653
  ? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
622
654
  : null;
623
655
  const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route } as const;
624
- const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, boundPrNumber, loaded.value);
656
+ const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
625
657
  if (!beforeWrite.ok) return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
626
658
  if (!sameIdentity(initial.value, beforeWrite.value)) return fail('blocked', context, {
627
659
  code: 'PR_SUMMARY_HEAD_RACE',
@@ -651,7 +683,7 @@ async function syncPullRequestSummary(
651
683
  })
652
684
  : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
653
685
  if (!written.ok) return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
654
- const after = await inspectBoundPullRequest(context, resolved.repoRoot, prNumber, loaded.value);
686
+ const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
655
687
  const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
656
688
  const compensateWrittenSummary = async () => reconciliation.action === 'create'
657
689
  ? written.value.remoteId && loaded.value.provider.comments?.delete
@@ -376,7 +376,7 @@ async function resolvedContext(taskRef: string, options: InspectionOptions) {
376
376
  }) };
377
377
  const fact = factRead.status === 'valid' ? factRead.fact : null;
378
378
  let issueIdentity: ResourceIdentity | null;
379
- try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
379
+ try { issueIdentity = taskIssueIdentity(frontmatter); }
380
380
  catch (error) { return { ok: false as const, output: result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }) }; }
381
381
  const issueNumber = resourceIdentityNumber(issueIdentity);
382
382
  const prIdentity = fact?.state === 'bound' ? fact.identity.resource : null;
@@ -941,7 +941,7 @@ async function resolveExternalPullRequest(taskRef: string, options: ResolveExter
941
941
  if (!base.issueIdentity) return externalResult(result('failed', base.resolved.taskId, null, base.prNumber, {
942
942
  platform: base.context.platform,
943
943
  capabilities: base.context.capabilities,
944
- error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid issue_number', retryable: false }
944
+ error: { code: 'EXTERNAL_DELIVERY_ISSUE_REQUIRED', message: 'External delivery requires a valid platform issue identity', retryable: false }
945
945
  }));
946
946
  const inspected = base.provider.changeRequests?.listClosing
947
947
  ? await base.provider.changeRequests.listClosing({
@@ -1430,7 +1430,7 @@ function skipPlatformPullRequestFactUnlocked(
1430
1430
  });
1431
1431
  }
1432
1432
  let issueIdentity: ResourceIdentity | null;
1433
- try { issueIdentity = taskIssueIdentity(frontmatter, undefined, options.runtimeVersion); }
1433
+ try { issueIdentity = taskIssueIdentity(frontmatter); }
1434
1434
  catch (error) { return result('failed', resolved.taskId, null, null, { error: { ...taskIssueIdentityError(error), retryable: false } }); }
1435
1435
  const issueNumber = resourceIdentityNumber(issueIdentity);
1436
1436
  const existing = readPrDeliveryFact(frontmatter, options.runtimeVersion);
@@ -1,13 +1,8 @@
1
- import type { ProviderIdentityDeclaration, ResourceIdentity } from './resource-identity.ts';
1
+ import type { ResourceIdentity } from './resource-identity.ts';
2
2
  import { parseResourceIdentity } from './resource-identity.ts';
3
- import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
4
3
  import { parse as parseYaml } from 'yaml';
5
4
 
6
- function taskIssueIdentity(
7
- frontmatter: Record<string, string | number | boolean | null>,
8
- _declaration?: ProviderIdentityDeclaration,
9
- runtimeVersion = VERSION
10
- ): ResourceIdentity | null {
5
+ function taskIssueIdentity(frontmatter: Record<string, string | number | boolean | null>): ResourceIdentity | null {
11
6
  const serialized = frontmatter.platform_issue_identity;
12
7
  if (typeof serialized === 'string' && serialized.trim()) {
13
8
  try {
@@ -20,13 +15,7 @@ function taskIssueIdentity(
20
15
  return null;
21
16
  }
22
17
  }
23
- const legacy = frontmatter.issue_number;
24
- const number = typeof legacy === 'number' ? legacy : typeof legacy === 'string' && /^[1-9]\d*$/u.test(legacy) ? Number(legacy) : NaN;
25
- // TODO(compat): Remove the numeric issue_number fallback before the first stable v1.0.0 release.
26
- if (Number.isSafeInteger(number) && number > 0 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
27
- throw legacyCompatibilityError('issue_number', runtimeVersion);
28
- }
29
- return Number.isSafeInteger(number) && number > 0 ? { kind: 'number', value: number } : null;
18
+ return null;
30
19
  }
31
20
 
32
21
  function taskIssueIdentityError(error: unknown): { code: string; message: string } {
@@ -12,6 +12,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
12
12
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
13
13
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
14
14
  import { taskIssueIdentity } from "./task-identities.ts";
15
+ import { isTaskCommentTooLarge } from "./issue-comments.ts";
15
16
  import {
16
17
  CONTROL_MARKER_PATTERN,
17
18
  canonicalizeCommentBody,
@@ -113,8 +114,8 @@ async function buildSyncContext({ taskDir, config, artifactFile }: any, shared:
113
114
  }
114
115
  const prIdentity = fact.status === "valid" && fact.fact.state === "bound" ? fact.fact.identity.resource : null;
115
116
  const prNumber = resourceIdentityNumber(prIdentity);
116
- if (config.when === "issue_number_exists" && !issueIdentity) {
117
- return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no issue_number") };
117
+ if (config.when === "platform_issue_identity_exists" && !issueIdentity) {
118
+ return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no platform_issue_identity") };
118
119
  }
119
120
  if (config.when === "pr_fact_bound" && !prIdentity) {
120
121
  return { earlyReturn: shared.passResult(CHECK_TYPE, "Skipped: task has no verified bound pull request") };
@@ -541,6 +542,9 @@ function checkTaskCommentContent(context: any, remoteData: any, shared: Verifica
541
542
  if (!context.config.verify_task_comment_content) {
542
543
  return null;
543
544
  }
545
+ if (isTaskCommentTooLarge(context.task.content)) {
546
+ return null;
547
+ }
544
548
 
545
549
  const taskMarker = `<!-- sync-issue:${context.task.metadata.id}:task -->`;
546
550
  const comment = findCommentByMarker(remoteData.comments, taskMarker);
@@ -37,11 +37,6 @@ export class HostResolutionError extends Error {
37
37
  }
38
38
  }
39
39
 
40
- function toPositiveNumber(value: unknown): number | null {
41
- const n = Number(value);
42
- return Number.isInteger(n) && n > 0 ? n : null;
43
- }
44
-
45
40
  /**
46
41
  * Parse the PR body for closing-issue references. Handles the conventional
47
42
  * `Closes #1` / `Fixes: #2, #3` shapes (case-insensitive, comma/space
@@ -68,7 +63,7 @@ export function extractClosingIssueNumbers(body: string): number[] {
68
63
  /**
69
64
  * Scan `.agents/workspace/active/{task-id}/task.md` files for tasks bound to the
70
65
  * PR number or to one of the PR's closing issues. A task is emitted at most once;
71
- * a verified fact identity hit takes priority over the issue-number reverse lookup.
66
+ * a verified fact identity hit takes priority over the issue-identity reverse lookup.
72
67
  */
73
68
  export function collectHostCandidates(input: {
74
69
  prNumber?: number;
@@ -97,7 +92,7 @@ export function collectHostCandidates(input: {
97
92
  let frontmatter: ReturnType<typeof parseTypedTaskFrontmatter>;
98
93
  try { frontmatter = parseTypedTaskFrontmatter(content); } catch { continue; }
99
94
  const issueIdentity = taskIssueIdentity(frontmatter);
100
- const issueNumber = resourceIdentityNumber(issueIdentity) ?? toPositiveNumber(frontmatter.issue_number);
95
+ const issueNumber = resourceIdentityNumber(issueIdentity);
101
96
  const fact = readPrDeliveryFact(frontmatter);
102
97
  const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
103
98
  const prNumber = resourceIdentityNumber(prIdentity);
@@ -209,8 +204,8 @@ function isTrusted(head: HeadState): boolean {
209
204
  /**
210
205
  * Derive the S1(a) trust factor `taskIssueMatches` (PL-3) mechanically from
211
206
  * resolved data: a unique host is trusted on the issue dimension only when the
212
- * task's bound issue_number is among the PR's closing issues. Ambiguous/none
213
- * hosts and tasks without a positive issue_number cannot be confirmed -> false
207
+ * task's bound issue identity is among the PR's closing issues. Ambiguous/none
208
+ * hosts and tasks without a numeric issue identity cannot be confirmed -> false
214
209
  * (fail closed to S2/audit). `resolve-host` emits this value so the prompt layer
215
210
  * does not derive the trust factor ad hoc (CD-2).
216
211
  */