@fitlab-ai/agent-infra 0.11.0 → 0.11.2

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 (146) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/manual-validation.js +6 -2
  3. package/dist/lib/internal/platform-comment.js +59 -0
  4. package/dist/lib/internal/sandbox-control.js +4 -131
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-operation-registry.js +1 -0
  7. package/dist/lib/internal/task-review.js +5 -5
  8. package/dist/lib/internal/task-validate.js +0 -6
  9. package/dist/lib/platform/comment-safety.js +7 -1
  10. package/dist/lib/platform/github-provider.js +13 -3
  11. package/dist/lib/platform/issue-comments.js +120 -13
  12. package/dist/lib/platform/issues.js +3 -4
  13. package/dist/lib/platform/provider-contract.d.ts +2 -0
  14. package/dist/lib/platform/provider-validation.js +7 -2
  15. package/dist/lib/platform/verification-sync.js +161 -156
  16. package/dist/lib/sandbox/control/executor.js +0 -38
  17. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  18. package/dist/lib/sandbox/control/task-view.js +3 -1
  19. package/dist/lib/sandbox/control/workflow-executor.js +32 -41
  20. package/dist/lib/task/artifact-command.js +6 -6
  21. package/dist/lib/task/artifact-lifecycle.js +15 -52
  22. package/dist/lib/task/artifact-operations.js +27 -1
  23. package/dist/lib/task/capabilities.js +0 -2
  24. package/dist/lib/task/codex-orchestration.js +10 -208
  25. package/dist/lib/task/commit-operation.js +3 -18
  26. package/dist/lib/task/control-authority.js +1 -8
  27. package/dist/lib/task/delegation-receipts.js +14 -76
  28. package/dist/lib/task/events.js +147 -278
  29. package/dist/lib/task/finalization-state.js +2 -1
  30. package/dist/lib/task/finalization.js +273 -49
  31. package/dist/lib/task/gate-policy.js +94 -0
  32. package/dist/lib/task/lifecycle-execution.js +2 -11
  33. package/dist/lib/task/local-artifact-finalization.js +31 -245
  34. package/dist/lib/task/orchestration.js +8 -24
  35. package/dist/lib/task/review-artifacts.js +12 -6
  36. package/dist/lib/task/review-command.js +0 -1
  37. package/dist/lib/task/review-finalization.js +47 -218
  38. package/dist/lib/task/verification-engine.js +28 -17
  39. package/dist/lib/task/verification.js +25 -9
  40. package/dist/lib/task/workflow-command.js +4 -1
  41. package/dist/lib/task/workflow-dispatch.js +55 -3
  42. package/dist/lifecycle-build-manifest.json +3 -3
  43. package/lib/internal/cli-route-inventory.ts +1 -1
  44. package/lib/internal/manual-validation.ts +6 -2
  45. package/lib/internal/platform-comment.ts +38 -0
  46. package/lib/internal/sandbox-control.ts +3 -145
  47. package/lib/internal/task-artifact.ts +1 -1
  48. package/lib/internal/task-operation-registry.ts +1 -0
  49. package/lib/internal/task-review.ts +5 -5
  50. package/lib/internal/task-validate.ts +0 -5
  51. package/lib/platform/comment-safety.ts +8 -0
  52. package/lib/platform/github-provider.ts +11 -2
  53. package/lib/platform/issue-comments.ts +132 -13
  54. package/lib/platform/issues.ts +3 -4
  55. package/lib/platform/provider-contract.ts +2 -0
  56. package/lib/platform/provider-validation.ts +6 -2
  57. package/lib/platform/verification-sync.ts +178 -179
  58. package/lib/sandbox/control/executor.ts +0 -44
  59. package/lib/sandbox/control/finalization-response.ts +10 -2
  60. package/lib/sandbox/control/task-view.ts +3 -1
  61. package/lib/sandbox/control/workflow-executor.ts +31 -48
  62. package/lib/task/artifact-command.ts +6 -7
  63. package/lib/task/artifact-lifecycle.ts +12 -49
  64. package/lib/task/artifact-operations.ts +27 -1
  65. package/lib/task/capabilities.ts +0 -1
  66. package/lib/task/codex-orchestration.ts +8 -241
  67. package/lib/task/commit-operation.ts +3 -23
  68. package/lib/task/control-authority.ts +1 -8
  69. package/lib/task/delegation-receipts.ts +12 -79
  70. package/lib/task/events.ts +155 -287
  71. package/lib/task/finalization-state.ts +2 -1
  72. package/lib/task/finalization.ts +277 -57
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/lifecycle-execution.ts +10 -21
  75. package/lib/task/local-artifact-finalization.ts +29 -285
  76. package/lib/task/orchestration.ts +12 -41
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +0 -1
  79. package/lib/task/review-finalization.ts +46 -200
  80. package/lib/task/verification-engine.ts +24 -14
  81. package/lib/task/verification-types.ts +7 -0
  82. package/lib/task/verification.ts +25 -9
  83. package/lib/task/workflow-command.ts +4 -1
  84. package/lib/task/workflow-dispatch.ts +46 -3
  85. package/package.json +2 -2
  86. package/templates/.agents/rules/issue-sync.en.md +2 -0
  87. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  88. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  89. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  90. package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
  91. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
  92. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  93. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  94. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  95. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  96. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  97. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  98. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  101. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  102. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  105. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  106. package/templates/.agents/skills/commit/config/verify.json +1 -5
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  108. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  109. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  110. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  111. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  112. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  113. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  114. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  115. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  116. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  117. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  118. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  119. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  120. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  121. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  122. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  123. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  124. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  125. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  129. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  133. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  134. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  135. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  136. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  137. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  139. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  140. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  141. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  142. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  143. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  144. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  145. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  146. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -43,7 +43,7 @@ export const INTERNAL_HANDLER_ROUTE_SELECTORS = Object.freeze({
43
43
  'platform-security': ['read', 'dismiss'],
44
44
  'platform-metadata': ['init-labels', 'init-milestones'],
45
45
  'platform-context': ['resolve'],
46
- 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'sync'],
46
+ 'platform-comment': ['list', 'recover', 'owner', 'backfill', 'stage-summary', 'sync'],
47
47
  'platform-issue': ['inspect', 'create', 'bind', 'sync'],
48
48
  'platform-pr': ['inspect', 'summary-context', 'resolve-external', 'create', 'bind', 'skip', 'sync', 'change-report', 'summary-sync', 'sync-in-labels'],
49
49
  'platform-pr-review': ['inspect', 'list', 'publish-pr', 'publish-task'],
@@ -23,6 +23,7 @@ function printFailure(format, error) {
23
23
  process.exitCode = 1;
24
24
  }
25
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;
26
+ const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
26
27
  function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evidenceDigest = '', prHeadSha = '') {
27
28
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
28
29
  const section = phase === 'pending'
@@ -33,11 +34,14 @@ function manualSummaryBody(body, phase, transactionId, receiptDigest = '', evide
33
34
  ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
34
35
  : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
35
36
  let inserted = false;
36
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
37
+ const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
37
38
  if (inserted)
38
39
  return '';
39
40
  inserted = true;
40
- return `${section}\n\n`;
41
+ const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
42
+ return trailingPlaceholder
43
+ ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
44
+ : `${section}\n\n`;
41
45
  });
42
46
  return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
43
47
  }
@@ -1,14 +1,19 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
3
4
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
4
5
  import { checkPlatformCommentOwner, listPlatformComments, syncPlatformComment } from '../platform/issue-comments.js';
6
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.js';
7
+ import { platformResult } from '../platform/types.js';
5
8
  import { recoverTaskFromComments } from '../platform/task-recovery.js';
6
9
  import { backfillCompletionComments } from '../platform/completion-backfill.js';
7
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
11
+ import { resolveTaskRef } from '../task/resolve-ref.js';
8
12
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
9
13
  agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
10
14
  agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
11
15
  agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
16
+ agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
12
17
  agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
13
18
  `;
14
19
  function fail(message) {
@@ -63,6 +68,7 @@ async function platformComment(args = []) {
63
68
  internalHandlerRoute('platform-comment', 'recover', operation),
64
69
  internalHandlerRoute('platform-comment', 'owner', operation),
65
70
  internalHandlerRoute('platform-comment', 'backfill', operation),
71
+ internalHandlerRoute('platform-comment', 'stage-summary', operation),
66
72
  internalHandlerRoute('platform-comment', 'sync', operation)
67
73
  ].some(Boolean)) {
68
74
  fail('a valid operation is required');
@@ -177,6 +183,59 @@ async function platformComment(args = []) {
177
183
  finish(await backfillCompletionComments(taskRef, { cwd, agent: normalizedAgent }));
178
184
  return;
179
185
  }
186
+ if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
187
+ const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
188
+ if (unexpected) {
189
+ fail(`stage-summary does not accept '--${unexpected}'`);
190
+ return;
191
+ }
192
+ if (typeof parsed.values.bodyFile !== 'string') {
193
+ fail('stage-summary requires --body-file');
194
+ return;
195
+ }
196
+ let body;
197
+ try {
198
+ body = readBodyFile(parsed.values.bodyFile, cwd);
199
+ }
200
+ catch (error) {
201
+ fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
202
+ return;
203
+ }
204
+ const canonical = canonicalizeSummaryBody(body);
205
+ if (!canonical.ok) {
206
+ fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`);
207
+ return;
208
+ }
209
+ body = canonical.value;
210
+ const resolved = resolveTaskRef(taskRef, { repoRoot: cwd });
211
+ if (!resolved.ok || resolved.state !== 'active') {
212
+ fail('stage-summary requires an active task');
213
+ return;
214
+ }
215
+ const target = path.join(resolved.taskDir, '.delivery-summary.json');
216
+ const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
217
+ const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
218
+ const next = `${JSON.stringify(value)}\n`;
219
+ if (previous === next) {
220
+ finish(platformResult('no-op'));
221
+ return;
222
+ }
223
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
224
+ try {
225
+ fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
226
+ fs.renameSync(temporary, target);
227
+ }
228
+ catch (error) {
229
+ try {
230
+ fs.unlinkSync(temporary);
231
+ }
232
+ catch { /* preserve primary error */ }
233
+ fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
234
+ return;
235
+ }
236
+ finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
237
+ return;
238
+ }
180
239
  if (!internalHandlerRoute('platform-comment', 'sync', operation)) {
181
240
  fail('operation is not registered');
182
241
  return;
@@ -1,118 +1,10 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskControl, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
1
+ import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
5
2
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
6
3
  import { serveSandboxControl } from '../sandbox/control/server.js';
7
4
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
8
- import { controllerProofFromContext, verifyCodexSandboxControllerContextWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
9
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
10
6
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
11
7
  import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.js';
12
- import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
13
- import { routeOrchestration } from '../task/orchestration.js';
14
- import { parseArtifactName } from '../task/artifact-name.js';
15
- function valueAfter(args, flag) {
16
- for (let index = 0; index < args.length; index += 1) {
17
- if (args[index] === flag)
18
- return args[index + 1];
19
- if (args[index]?.startsWith(`${flag}=`))
20
- return args[index].slice(flag.length + 1);
21
- }
22
- return undefined;
23
- }
24
- function withoutValue(args, flag) {
25
- const result = [];
26
- for (let index = 0; index < args.length; index += 1) {
27
- if (args[index] === flag) {
28
- index += 1;
29
- continue;
30
- }
31
- if (args[index]?.startsWith(`${flag}=`))
32
- continue;
33
- result.push(args[index]);
34
- }
35
- return result;
36
- }
37
- function lifecycleIds(authorityRef, taskId) {
38
- const digest = (label) => createHash('sha256')
39
- .update(`${label}\0${taskId}\0${authorityRef}`, 'utf8')
40
- .digest('hex');
41
- return { operationId: digest('operation'), lifecycleRequestId: digest('lifecycle-request') };
42
- }
43
- function lifecycleAuthorityIdentity(authorityRef, taskId, requestId, phase, family, artifact, round) {
44
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
45
- if (!authorityRef || !contextPath)
46
- return undefined;
47
- const routed = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() });
48
- const buildIdentity = computeLifecycleBuildIdentity(process.cwd());
49
- const hookDefinitionHash = createHash('sha256')
50
- .update(readFileSync(path.join(process.cwd(), '.codex', 'hooks.json')))
51
- .digest('hex');
52
- const ids = lifecycleIds(authorityRef, taskId);
53
- return {
54
- version: 1,
55
- requestId,
56
- operationId: ids.operationId,
57
- phase,
58
- taskId,
59
- family,
60
- artifact,
61
- round,
62
- lifecycleRequestId: ids.lifecycleRequestId,
63
- authorityRef,
64
- expectedControlGeneration: routed.context.controlGeneration,
65
- expectedControllerInstanceDigest: routed.context.controllerInstanceDigest,
66
- expectedBuildIdentityDigest: createHash('sha256').update(JSON.stringify(buildIdentity)).digest('hex'),
67
- expectedHookDefinitionHash: hookDefinitionHash
68
- };
69
- }
70
- function lifecycleAuthoritySelector(args) {
71
- if (!isCanonicalCodexPrepare(args))
72
- return undefined;
73
- const authorityRef = valueAfter(args, '--capability-ref');
74
- const taskId = args[0];
75
- if (!authorityRef || !taskId)
76
- return undefined;
77
- const routed = routeOrchestration(taskId);
78
- if (!routed.next || !['analysis', 'plan', 'code'].includes(routed.next.stage))
79
- return undefined;
80
- return lifecycleAuthorityIdentity(authorityRef, taskId, randomUUID(), 'orchestration.prepare', routed.next.stage === 'analysis' ? 'analysis' : routed.next.stage === 'plan' ? 'plan' : 'code', routed.next.artifact, routed.next.round);
81
- }
82
- function lifecycleWorkflowAuthoritySelector(originalArgs, workflow) {
83
- const authorityRef = valueAfter(originalArgs, '--capability-ref');
84
- if (!authorityRef)
85
- return undefined;
86
- if (workflow.operation === 'artifact-finalize-local') {
87
- const family = valueAfter(workflow.args, '--family');
88
- const artifact = valueAfter(workflow.args, '--artifact');
89
- const parsed = artifact ? parseArtifactName(artifact) : null;
90
- if ((family !== 'analysis' && family !== 'plan' && family !== 'code')
91
- || !parsed || parsed.family !== family)
92
- return undefined;
93
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'artifact.finalize-local', family, artifact, parsed.round);
94
- }
95
- if (workflow.operation !== 'event')
96
- return undefined;
97
- const completed = /^(analysis|plan|code)\.completed$/u.exec(workflow.args[1] ?? '');
98
- const artifact = valueAfter(workflow.args, '--artifact');
99
- const parsed = artifact ? parseArtifactName(artifact) : null;
100
- if (!completed || !parsed || parsed.family !== completed[1])
101
- return undefined;
102
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'task-event.completed', completed[1], artifact, parsed.round);
103
- }
104
- function isCanonicalCodexPrepare(args) {
105
- if (args[1] !== 'prepare')
106
- return false;
107
- const clients = [];
108
- for (let index = 2; index < args.length; index += 1) {
109
- if (args[index] === '--client')
110
- clients.push(args[index + 1] ?? '');
111
- else if (args[index]?.startsWith('--client='))
112
- clients.push(args[index].slice('--client='.length));
113
- }
114
- return clients.length === 1 && clients[0] === 'codex';
115
- }
116
8
  function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
117
9
  process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
118
10
  }
@@ -251,7 +143,7 @@ async function sandboxControl(args) {
251
143
  }
252
144
  let workflow;
253
145
  try {
254
- const args = withoutValue(commandArgs.slice(1), '--capability-ref');
146
+ const args = commandArgs.slice(1);
255
147
  if (/^\d+$/.test(args[0] ?? '')) {
256
148
  const resolved = resolveVisibleActiveShortId(args[0]);
257
149
  if (resolved !== taskId)
@@ -267,11 +159,7 @@ async function sandboxControl(args) {
267
159
  }
268
160
  let response;
269
161
  try {
270
- const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
271
- response = requestSandboxTaskWorkflow({
272
- workflow,
273
- ...(authority ? { authority } : {})
274
- });
162
+ response = requestSandboxTaskWorkflow({ workflow });
275
163
  }
276
164
  catch (error) {
277
165
  if (!(error instanceof SandboxControlClientError))
@@ -287,22 +175,7 @@ async function sandboxControl(args) {
287
175
  }
288
176
  let response;
289
177
  try {
290
- if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
291
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
292
- const proof = contextPath
293
- ? controllerProofFromContext(verifyCodexSandboxControllerContextWithWarnings(contextPath).context)
294
- : null;
295
- const authority = lifecycleAuthoritySelector(commandArgs);
296
- response = requestSandboxTaskControl({
297
- family,
298
- args: commandArgs,
299
- controllerProof: authority ? null : proof,
300
- ...(authority ? { authority } : {})
301
- });
302
- }
303
- else {
304
- response = requestSandboxControl({ family, args: commandArgs });
305
- }
178
+ response = requestSandboxControl({ family, args: commandArgs });
306
179
  }
307
180
  catch (error) {
308
181
  if (!(error instanceof SandboxControlClientError))
@@ -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> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact> [--recovery-id <id>]\n\nInspect, initialize, preflight, 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> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, 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;
@@ -50,6 +50,7 @@ function internalTaskRoutes() {
50
50
  descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
51
51
  descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
52
52
  descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
53
+ descriptor('internal', 'platform-comment', 'stage-summary', 'task-bound', 'artifact-write'),
53
54
  descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
54
55
  descriptor('internal', 'platform-issue', 'inspect', 'task-bound', 'diagnostic'),
55
56
  descriptor('internal', 'platform-issue', 'create', 'task-bound', 'remote-write'),
@@ -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> <preflight|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';
7
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--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, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
33
+ const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
34
34
  const operation = args[1];
35
35
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
36
36
  if (dryRunConflict) {
@@ -47,8 +47,8 @@ async function taskReview(args = []) {
47
47
  try {
48
48
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
49
49
  let current = operation === 'preflight'
50
- ? preflightReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
- : finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
50
+ ? preflightReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
+ : finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
52
52
  if (operation === 'preflight')
53
53
  return current;
54
54
  if (current.status !== 'failed' || !overrideTicket)
@@ -63,7 +63,7 @@ async function taskReview(args = []) {
63
63
  target: overrideTarget, scope: overrideScope
64
64
  }, {
65
65
  effectExecutor: (capability) => {
66
- const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
66
+ const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
67
67
  current = retried;
68
68
  return retried.status === 'failed' || retried.status === 'planned'
69
69
  ? { 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'}` }
@@ -135,12 +135,6 @@ function snapshotValidation(repoRoot, commit, options) {
135
135
  cleanupError = error;
136
136
  }
137
137
  }
138
- try {
139
- git(repoRoot, ['worktree', 'prune']);
140
- }
141
- catch (error) {
142
- cleanupError ??= error;
143
- }
144
138
  try {
145
139
  fs.rmSync(tempRoot, { recursive: true, force: true });
146
140
  }
@@ -171,6 +171,12 @@ function sanitizeMarkdownDocument(value, options = {}) {
171
171
  function canonicalizeCommentBody(value) {
172
172
  return sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
173
173
  }
174
+ function canonicalizeSummaryBody(value) {
175
+ const sanitized = canonicalizeCommentBody(value);
176
+ return sanitized.ok
177
+ ? ok(`${sanitized.value.replace(/\n+$/, '')}\n`)
178
+ : sanitized;
179
+ }
174
180
  function renderSafeCodeFence(value, language = '', reservedMarkers = [CONTROL_MARKER_PATTERN]) {
175
181
  const content = markerSafe(normalized(value), reservedMarkers);
176
182
  const longestRun = Math.max(0, ...(content.match(/`+/g) || []).map((run) => run.length));
@@ -225,5 +231,5 @@ function splitDocumentPlaceholder(value, placeholder) {
225
231
  }
226
232
  return ok({ prefix: source.slice(0, position), suffix: source.slice(position + placeholder.length) });
227
233
  }
228
- export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
234
+ export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, canonicalizeSummaryBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
229
235
  //# sourceMappingURL=comment-safety.js.map
@@ -278,13 +278,14 @@ function issueSnapshot(value, graph) {
278
278
  displayUrl: value.url
279
279
  };
280
280
  }
281
- function commentSnapshot(value) {
281
+ function commentSnapshot(value, createdSequence = null) {
282
282
  return {
283
283
  id: String(value.id),
284
284
  author: value.user?.login ? { id: String(value.user.login), name: String(value.user.login) } : null,
285
285
  body: String(value.body || ''),
286
286
  createdAt: String(value.created_at || ''),
287
- updatedAt: String(value.updated_at || value.created_at || '')
287
+ updatedAt: String(value.updated_at || value.created_at || ''),
288
+ createdSequence
288
289
  };
289
290
  }
290
291
  function createReceipt(remoteId) {
@@ -555,7 +556,16 @@ function createGitHubOperations(client) {
555
556
  if (!response.ok)
556
557
  return response;
557
558
  const values = Array.isArray(response.value) ? response.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) : [];
558
- return { ok: true, value: values.filter((entry) => entry && entry.id !== undefined).map(commentSnapshot) };
559
+ const comments = values.filter((entry) => entry && typeof entry === 'object');
560
+ let previous = 0;
561
+ const ordered = comments.length === values.length && comments.every((entry) => {
562
+ const id = Number(entry.id);
563
+ if (!Number.isSafeInteger(id) || id <= previous)
564
+ return false;
565
+ previous = id;
566
+ return true;
567
+ });
568
+ return { ok: true, value: comments.map((comment) => commentSnapshot(comment, ordered ? Number(comment.id) : null)) };
559
569
  },
560
570
  async write({ context, parent, body, existingComment }) {
561
571
  const number = resourceIdentityNumber(parent);
@@ -10,7 +10,7 @@ import { resourceIdentityNumber } from './resource-identity.js';
10
10
  import { taskIssueIdentity, taskIssueIdentityError } from './task-identities.js';
11
11
  import { recordForArtifact, recordForCancel, recordForSummary, recordForTask, renderActivityRecoveryMetadata } from './activity-recovery.js';
12
12
  import { projectTaskComment } from './task-comment-projection.js';
13
- import { canonicalizeCommentBody, escapeHtmlText, fenceRanges, renderSafeCodeFence } from './comment-safety.js';
13
+ import { canonicalizeCommentBody, canonicalizeSummaryBody, escapeHtmlText, fenceRanges, renderSafeCodeFence } from './comment-safety.js';
14
14
  function providerCommentId(id, provider) {
15
15
  return provider.identity?.comment === 'number' && /^\d+$/.test(id) ? Number(id) : id;
16
16
  }
@@ -373,6 +373,46 @@ function relatedComments(comments, prefix) {
373
373
  return first === base || (first.startsWith(chunkNamespace) && first.endsWith(' -->'));
374
374
  });
375
375
  }
376
+ function taskManagedComment(comment, taskId) {
377
+ const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
378
+ return marker === MARKERS.task(taskId)
379
+ || marker === MARKERS.summary(taskId)
380
+ || marker.startsWith(`<!-- sync-issue:${taskId}:`);
381
+ }
382
+ function summaryPosition(comments, taskId) {
383
+ const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
384
+ const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
385
+ if (summary.length !== 1)
386
+ return { ok: false, known: false, summary: null };
387
+ if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
388
+ return { ok: false, known: false, summary: summary[0] };
389
+ }
390
+ const sequence = summary[0].createdSequence;
391
+ return { ok: managed.every((comment) => sequence >= comment.createdSequence), known: true, summary: summary[0] };
392
+ }
393
+ function summaryDigest(comment) {
394
+ const body = normalizeCommentContent(comment.body)
395
+ .replace(/^<!-- sync-issue:[^\n]+:summary -->\n## [^\n]+\n\n> [^\n]+\n\n/u, '')
396
+ .replace(/^<details><summary>恢复元数据<\/summary>[\s\S]*?<\/details>\n\n/u, '')
397
+ .replace(/\n---\n\*[^\n]*\*$/u, '');
398
+ const canonical = canonicalizeSummaryBody(body);
399
+ return canonical.ok ? createHash('sha256').update(canonical.value).digest('hex') : '';
400
+ }
401
+ async function listedComments(provider, loaded, parent) {
402
+ return provider.comments?.list
403
+ ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response) => response.ok
404
+ ? {
405
+ ok: true,
406
+ value: response.value.map((comment) => ({
407
+ id: providerCommentId(comment.id, provider),
408
+ body: comment.body,
409
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
410
+ createdSequence: comment.createdSequence
411
+ }))
412
+ }
413
+ : response)
414
+ : unsupportedProviderOperation(provider, 'comments.list');
415
+ }
376
416
  function validateRelatedMarkerSet(comments, prefix) {
377
417
  const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
378
418
  if (new Set(markers).size !== markers.length)
@@ -442,18 +482,7 @@ async function syncPlatformComment(taskRef, options) {
442
482
  if (!hasResolvedPlatformContext(context) || !loaded.ok)
443
483
  return context;
444
484
  const issue = resourceIdentityNumber(issueIdentityFromTask);
445
- const listed = loaded.value.provider.comments?.list
446
- ? await loaded.value.provider.comments.list({
447
- context: providerOperationContext(loaded.value),
448
- parent: issueIdentityFromTask
449
- }).then((response) => response.ok
450
- ? { ok: true, value: response.value.map((comment) => ({
451
- id: providerCommentId(comment.id, loaded.value.provider),
452
- body: comment.body,
453
- user: comment.author?.name ? { login: comment.author.name } : undefined
454
- })) }
455
- : response)
456
- : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
485
+ const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
457
486
  if (!listed.ok) {
458
487
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
459
488
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
@@ -481,6 +510,25 @@ async function syncPlatformComment(taskRef, options) {
481
510
  });
482
511
  }
483
512
  }
513
+ if (options.verifyOnly) {
514
+ if (options.kind !== 'summary' || !options.summaryAuthorization) {
515
+ return platformResult('failed', {
516
+ ...contextFields(context), resource: { kind: 'issue', number: issue },
517
+ error: { code: 'SUMMARY_VERIFICATION_INVALID', message: 'summary verification requires durable authorization', retryable: false }
518
+ });
519
+ }
520
+ const position = summaryPosition(listed.value, resolved.taskId);
521
+ if (!position.ok || !position.summary || summaryDigest(position.summary) !== options.summaryAuthorization.sha256) {
522
+ return platformResult('failed', {
523
+ ...contextFields(context), resource: { kind: 'issue', number: issue },
524
+ error: { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary marker, body digest, or managed comment order is invalid', retryable: true }
525
+ });
526
+ }
527
+ return platformResult('no-op', {
528
+ ...contextFields(context), changed: false, resource: { kind: 'issue', number: issue },
529
+ comment: { kind: options.kind, marker: desired[0].marker, ids: [position.summary.id], parts: 1 }, error: null
530
+ });
531
+ }
484
532
  // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
485
533
  if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
486
534
  const operations = existing.map((comment) => ({
@@ -555,6 +603,65 @@ async function syncPlatformComment(taskRef, options) {
555
603
  }
556
604
  operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
557
605
  }
606
+ if (options.kind === 'summary') {
607
+ const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
608
+ if (!refreshed.ok) {
609
+ return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
610
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
611
+ });
612
+ }
613
+ const position = summaryPosition(refreshed.value, resolved.taskId);
614
+ if (!position.ok && !position.known) {
615
+ return platformResult('blocked', {
616
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
617
+ error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
618
+ });
619
+ }
620
+ if (!position.ok && position.summary) {
621
+ const owner = position.summary.user?.login;
622
+ if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
623
+ || !owner || owner !== context.platform.currentUser) {
624
+ return platformResult('failed', {
625
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
626
+ error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
627
+ });
628
+ }
629
+ const deleted = loaded.value.provider.comments?.delete
630
+ ? await loaded.value.provider.comments.delete({
631
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
632
+ comment: { kind: 'id', value: String(position.summary.id) },
633
+ mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
634
+ })
635
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
636
+ if (!deleted.ok) {
637
+ return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
638
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
639
+ });
640
+ }
641
+ const written = loaded.value.provider.comments?.write
642
+ ? await loaded.value.provider.comments.write({
643
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0].body,
644
+ mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
645
+ })
646
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
647
+ if (!written.ok) {
648
+ return platformResult(written.error.retryable ? 'blocked' : 'failed', {
649
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
650
+ });
651
+ }
652
+ ids.splice(0, ids.length, providerCommentId(written.value.remoteId, loaded.value.provider));
653
+ operations.push({ name: `comment:${desired[0].marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
654
+ const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
655
+ if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
656
+ const error = reconciled.ok
657
+ ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
658
+ : reconciled.error;
659
+ return platformResult(error.retryable ? 'blocked' : 'failed', {
660
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
661
+ });
662
+ }
663
+ }
664
+ }
558
665
  const changed = operations.some((operation) => operation.status === 'applied');
559
666
  const result = platformResult(changed ? 'applied' : 'no-op', {
560
667
  ...contextFields(context),
@@ -538,11 +538,10 @@ async function syncPlatformIssue(taskRef, options) {
538
538
  if (failure)
539
539
  return result('failed', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: { code: failure.reasonCode || 'ISSUE_SYNC_FAILED', message: `Operation ${failure.name} failed`, retryable: false } });
540
540
  const planned = plan.operations.filter((operation) => operation.status === 'planned');
541
- const skipped = plan.operations.some((operation) => operation.status === 'skipped');
542
541
  if (options.dryRun)
543
- return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
542
+ return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
543
  if (planned.length === 0)
545
- return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
544
+ return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
546
545
  const payload = applyRestOperations(snapshot, planned);
547
546
  const updated = base.provider.issues?.update
548
547
  ? await base.provider.issues.update({ context: providerOperationContext(base.loadedContext), target: base.issueIdentity, currentLabels: snapshot.labels, patch: payload, mutation: { idempotencyKey: `issue:update:${base.resolved.taskId}` } })
@@ -577,7 +576,7 @@ async function syncPlatformIssue(taskRef, options) {
577
576
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
578
577
  });
579
578
  }
580
- return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
579
+ return result('applied', base.resolved.taskId, base.issueNumber, {
581
580
  changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
582
581
  resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
583
582
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
@@ -87,6 +87,8 @@ type RemoteCommentSnapshot = {
87
87
  body: string;
88
88
  createdAt: string;
89
89
  updatedAt: string;
90
+ /** Strict creation order when the provider can prove it for this list snapshot. */
91
+ createdSequence: number | null;
90
92
  };
91
93
  type ChangeRequestSnapshot = {
92
94
  id: string;
@@ -189,13 +189,18 @@ function issueType(value, label) {
189
189
  }
190
190
  function validateComment(value) {
191
191
  const item = record(value, 'comment');
192
- exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt'], 'comment');
192
+ exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt', 'createdSequence'], 'comment');
193
193
  return {
194
194
  id: stringValue(item.id, 'comment.id'),
195
195
  author: author(item.author, 'comment.author'),
196
196
  body: stringValue(item.body, 'comment.body', true),
197
197
  createdAt: utcTimestamp(item.createdAt, 'comment.createdAt'),
198
- updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt')
198
+ updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt'),
199
+ createdSequence: item.createdSequence === null ? null : (() => {
200
+ if (!Number.isSafeInteger(item.createdSequence) || Number(item.createdSequence) < 1)
201
+ throw new Error('comment.createdSequence must be a positive safe integer or null');
202
+ return Number(item.createdSequence);
203
+ })()
199
204
  };
200
205
  }
201
206
  function validateChangeRequest(value, expectedKind, requireIdentity = false) {