@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
@@ -44,6 +44,7 @@ function printFailure(format: OutputFormat, error: { code: string; message: stri
44
44
  }
45
45
 
46
46
  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;
47
+ const TRAILING_CANONICAL_REPORT_PLACEHOLDER = /\n*<!--\s*canonical-pr-change-report\s*-->\s*$/u;
47
48
 
48
49
  function manualSummaryBody(body: string, phase: 'pending' | 'final', transactionId: string, receiptDigest = '', evidenceDigest = '', prHeadSha = ''): string {
49
50
  const chinese = /###\s+(?:⚠️\s+需人工校验|✅\s+(?:人工验证已通过|无需人工校验)|⏳\s+人工验证待收尾)\s*$/mu.test(body);
@@ -55,10 +56,13 @@ function manualSummaryBody(body: string, phase: 'pending' | 'final', transaction
55
56
  ? `### ✅ 人工验证已通过\n\n人工验证已通过;transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`
56
57
  : `### ✅ Manual Validation Passed\n\nManual validation passed; transaction=${transactionId}; receipt=${receiptDigest}; evidence=${evidenceDigest}; head=${prHeadSha}.`;
57
58
  let inserted = false;
58
- const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, () => {
59
+ const updated = body.replace(MANUAL_VALIDATION_STATUS_SECTION, (matched) => {
59
60
  if (inserted) return '';
60
61
  inserted = true;
61
- return `${section}\n\n`;
62
+ const trailingPlaceholder = TRAILING_CANONICAL_REPORT_PLACEHOLDER.exec(matched)?.[0];
63
+ return trailingPlaceholder
64
+ ? `${trailingPlaceholder.trim()}\n\n${section}\n\n`
65
+ : `${section}\n\n`;
62
66
  });
63
67
  return inserted ? `${updated.replace(/\s+$/u, '')}\n` : `${body.replace(/\s+$/u, '')}\n\n${section}\n`;
64
68
  }
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
3
4
 
4
5
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
5
6
  import {
@@ -8,15 +9,19 @@ import {
8
9
  syncPlatformComment
9
10
  } from '../platform/issue-comments.ts';
10
11
  import type { CommentKind } from '../platform/issue-comments.ts';
12
+ import { canonicalizeSummaryBody } from '../platform/comment-safety.ts';
13
+ import { platformResult } from '../platform/types.ts';
11
14
  import type { PlatformResult } from '../platform/types.ts';
12
15
  import { recoverTaskFromComments } from '../platform/task-recovery.ts';
13
16
  import { backfillCompletionComments } from '../platform/completion-backfill.ts';
14
17
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
18
+ import { resolveTaskRef } from '../task/resolve-ref.ts';
15
19
 
16
20
  const USAGE = `Usage: agent-infra-internal platform-comment list --issue <token> [--cwd <path>]
17
21
  agent-infra-internal platform-comment recover --issue <token> --task-id <TASK-id> --output <staging-task.md> [--cwd <path>]
18
22
  agent-infra-internal platform-comment owner <task-ref> [--cwd <path>]
19
23
  agent-infra-internal platform-comment backfill <task-ref> --agent <agent> [--cwd <path>]
24
+ agent-infra-internal platform-comment stage-summary <task-ref> --body-file <path|-> [--cwd <path>]
20
25
  agent-infra-internal platform-comment sync <task-ref> --kind <kind> --agent <agent> [--artifact <file>] [--body-file <path|->] [--status-label <label>] [--backfill] [--cwd <path>]
21
26
  `;
22
27
 
@@ -64,6 +69,7 @@ async function platformComment(args: string[] = []): Promise<void> {
64
69
  internalHandlerRoute('platform-comment', 'recover', operation),
65
70
  internalHandlerRoute('platform-comment', 'owner', operation),
66
71
  internalHandlerRoute('platform-comment', 'backfill', operation),
72
+ internalHandlerRoute('platform-comment', 'stage-summary', operation),
67
73
  internalHandlerRoute('platform-comment', 'sync', operation)
68
74
  ].some(Boolean)) { fail('a valid operation is required'); return; }
69
75
  const hasTaskRef = operation !== 'list' && operation !== 'recover';
@@ -144,6 +150,38 @@ async function platformComment(args: string[] = []): Promise<void> {
144
150
  finish(await backfillCompletionComments(taskRef!, { cwd, agent: normalizedAgent }));
145
151
  return;
146
152
  }
153
+ if (internalHandlerRoute('platform-comment', 'stage-summary', operation)) {
154
+ const unexpected = Object.keys(parsed.values).find((key) => !['cwd', 'bodyFile'].includes(key));
155
+ if (unexpected) { fail(`stage-summary does not accept '--${unexpected}'`); return; }
156
+ if (typeof parsed.values.bodyFile !== 'string') { fail('stage-summary requires --body-file'); return; }
157
+ let body: string;
158
+ try { body = readBodyFile(parsed.values.bodyFile, cwd); }
159
+ catch (error) { fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`); return; }
160
+ const canonical = canonicalizeSummaryBody(body);
161
+ if (!canonical.ok) { fail(`unable to stage summary: ${canonical.error.code}: ${canonical.error.message}`); return; }
162
+ body = canonical.value;
163
+ const resolved = resolveTaskRef(taskRef!, { repoRoot: cwd });
164
+ if (!resolved.ok || resolved.state !== 'active') { fail('stage-summary requires an active task'); return; }
165
+ const target = path.join(resolved.taskDir, '.delivery-summary.json');
166
+ const value = { taskId: resolved.taskId, body, sha256: createHash('sha256').update(body).digest('hex') };
167
+ const previous = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
168
+ const next = `${JSON.stringify(value)}\n`;
169
+ if (previous === next) {
170
+ finish(platformResult('no-op'));
171
+ return;
172
+ }
173
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
174
+ try {
175
+ fs.writeFileSync(temporary, next, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
176
+ fs.renameSync(temporary, target);
177
+ } catch (error) {
178
+ try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
179
+ fail(`unable to stage summary: ${error instanceof Error ? error.message : String(error)}`);
180
+ return;
181
+ }
182
+ finish(platformResult('applied', { operations: [{ name: 'delivery-summary', status: 'applied', reasonCode: null }] }));
183
+ return;
184
+ }
147
185
  if (!internalHandlerRoute('platform-comment', 'sync', operation)) { fail('operation is not registered'); return; }
148
186
  const kind = parsed.values.kind;
149
187
  const agent = parsed.values.agent;
@@ -1,11 +1,7 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import path from 'node:path';
4
1
  import {
5
2
  recoverSandboxControl,
6
3
  requestSandboxControl,
7
4
  requestSandboxTaskFinalization,
8
- requestSandboxTaskControl,
9
5
  requestSandboxTaskWorkflow,
10
6
  resolveVisibleActiveShortId,
11
7
  SandboxControlClientError
@@ -13,129 +9,9 @@ import {
13
9
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
14
10
  import { serveSandboxControl } from '../sandbox/control/server.ts';
15
11
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
16
- import {
17
- controllerProofFromContext,
18
- verifyCodexSandboxControllerContextWithWarnings
19
- } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
20
12
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
21
13
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
22
14
  import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.ts';
23
- import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
24
- import { routeOrchestration } from '../task/orchestration.ts';
25
- import { parseArtifactName } from '../task/artifact-name.ts';
26
- import type { LifecycleAuthorityRequestV1 } from '../task/control-authority.ts';
27
-
28
- function valueAfter(args: readonly string[], flag: string): string | undefined {
29
- for (let index = 0; index < args.length; index += 1) {
30
- if (args[index] === flag) return args[index + 1];
31
- if (args[index]?.startsWith(`${flag}=`)) return args[index]!.slice(flag.length + 1);
32
- }
33
- return undefined;
34
- }
35
-
36
- function withoutValue(args: readonly string[], flag: string): string[] {
37
- const result: string[] = [];
38
- for (let index = 0; index < args.length; index += 1) {
39
- if (args[index] === flag) { index += 1; continue; }
40
- if (args[index]?.startsWith(`${flag}=`)) continue;
41
- result.push(args[index]!);
42
- }
43
- return result;
44
- }
45
-
46
- function lifecycleIds(authorityRef: string, taskId: string): Readonly<{ operationId: string; lifecycleRequestId: string }> {
47
- const digest = (label: string) => createHash('sha256')
48
- .update(`${label}\0${taskId}\0${authorityRef}`, 'utf8')
49
- .digest('hex');
50
- return { operationId: digest('operation'), lifecycleRequestId: digest('lifecycle-request') };
51
- }
52
-
53
- function lifecycleAuthorityIdentity(
54
- authorityRef: string,
55
- taskId: string,
56
- requestId: string,
57
- phase: LifecycleAuthorityRequestV1['phase'],
58
- family: LifecycleAuthorityRequestV1['family'],
59
- artifact: string,
60
- round: number
61
- ): LifecycleAuthorityRequestV1 | undefined {
62
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
63
- if (!authorityRef || !contextPath) return undefined;
64
- const routed = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() });
65
- const buildIdentity = computeLifecycleBuildIdentity(process.cwd());
66
- const hookDefinitionHash = createHash('sha256')
67
- .update(readFileSync(path.join(process.cwd(), '.codex', 'hooks.json')))
68
- .digest('hex');
69
- const ids = lifecycleIds(authorityRef, taskId);
70
- return {
71
- version: 1,
72
- requestId,
73
- operationId: ids.operationId,
74
- phase,
75
- taskId,
76
- family,
77
- artifact,
78
- round,
79
- lifecycleRequestId: ids.lifecycleRequestId,
80
- authorityRef,
81
- expectedControlGeneration: routed.context.controlGeneration,
82
- expectedControllerInstanceDigest: routed.context.controllerInstanceDigest,
83
- expectedBuildIdentityDigest: createHash('sha256').update(JSON.stringify(buildIdentity)).digest('hex'),
84
- expectedHookDefinitionHash: hookDefinitionHash
85
- };
86
- }
87
-
88
- function lifecycleAuthoritySelector(args: readonly string[]): LifecycleAuthorityRequestV1 | undefined {
89
- if (!isCanonicalCodexPrepare(args)) return undefined;
90
- const authorityRef = valueAfter(args, '--capability-ref');
91
- const taskId = args[0];
92
- if (!authorityRef || !taskId) return undefined;
93
- const routed = routeOrchestration(taskId);
94
- if (!routed.next || !['analysis', 'plan', 'code'].includes(routed.next.stage)) return undefined;
95
- return lifecycleAuthorityIdentity(
96
- authorityRef,
97
- taskId,
98
- randomUUID(),
99
- 'orchestration.prepare',
100
- routed.next.stage === 'analysis' ? 'analysis' : routed.next.stage === 'plan' ? 'plan' : 'code',
101
- routed.next.artifact,
102
- routed.next.round
103
- );
104
- }
105
-
106
- function lifecycleWorkflowAuthoritySelector(
107
- originalArgs: readonly string[],
108
- workflow: ReturnType<typeof createTaskWorkflowRequest>
109
- ): LifecycleAuthorityRequestV1 | undefined {
110
- const authorityRef = valueAfter(originalArgs, '--capability-ref');
111
- if (!authorityRef) return undefined;
112
- if (workflow.operation === 'artifact-finalize-local') {
113
- const family = valueAfter(workflow.args, '--family');
114
- const artifact = valueAfter(workflow.args, '--artifact');
115
- const parsed = artifact ? parseArtifactName(artifact) : null;
116
- if ((family !== 'analysis' && family !== 'plan' && family !== 'code')
117
- || !parsed || parsed.family !== family) return undefined;
118
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id,
119
- 'artifact.finalize-local', family, artifact!, parsed.round);
120
- }
121
- if (workflow.operation !== 'event') return undefined;
122
- const completed = /^(analysis|plan|code)\.completed$/u.exec(workflow.args[1] ?? '');
123
- const artifact = valueAfter(workflow.args, '--artifact');
124
- const parsed = artifact ? parseArtifactName(artifact) : null;
125
- if (!completed || !parsed || parsed.family !== completed[1]) return undefined;
126
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id,
127
- 'task-event.completed', completed[1] as LifecycleAuthorityRequestV1['family'], artifact!, parsed.round);
128
- }
129
-
130
- function isCanonicalCodexPrepare(args: readonly string[]): boolean {
131
- if (args[1] !== 'prepare') return false;
132
- const clients: string[] = [];
133
- for (let index = 2; index < args.length; index += 1) {
134
- if (args[index] === '--client') clients.push(args[index + 1] ?? '');
135
- else if (args[index]?.startsWith('--client=')) clients.push(args[index]!.slice('--client='.length));
136
- }
137
- return clients.length === 1 && clients[0] === 'codex';
138
- }
139
15
 
140
16
  type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
141
17
 
@@ -274,7 +150,7 @@ async function sandboxControl(args: string[]): Promise<void> {
274
150
  }
275
151
  let workflow;
276
152
  try {
277
- const args = withoutValue(commandArgs.slice(1), '--capability-ref');
153
+ const args = commandArgs.slice(1);
278
154
  if (/^\d+$/.test(args[0] ?? '')) {
279
155
  const resolved = resolveVisibleActiveShortId(args[0]!);
280
156
  if (resolved !== taskId) throw new Error('SANDBOX_TASK_REF_MISMATCH');
@@ -289,11 +165,7 @@ async function sandboxControl(args: string[]): Promise<void> {
289
165
  }
290
166
  let response;
291
167
  try {
292
- const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
293
- response = requestSandboxTaskWorkflow({
294
- workflow,
295
- ...(authority ? { authority } : {})
296
- });
168
+ response = requestSandboxTaskWorkflow({ workflow });
297
169
  }
298
170
  catch (error) {
299
171
  if (!(error instanceof SandboxControlClientError)) throw error;
@@ -308,21 +180,7 @@ async function sandboxControl(args: string[]): Promise<void> {
308
180
  }
309
181
  let response;
310
182
  try {
311
- if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
312
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
313
- const proof = contextPath
314
- ? controllerProofFromContext(verifyCodexSandboxControllerContextWithWarnings(contextPath).context)
315
- : null;
316
- const authority = lifecycleAuthoritySelector(commandArgs);
317
- response = requestSandboxTaskControl({
318
- family,
319
- args: commandArgs,
320
- controllerProof: authority ? null : proof,
321
- ...(authority ? { authority } : {})
322
- });
323
- } else {
324
- response = requestSandboxControl({ family, args: commandArgs });
325
- }
183
+ response = requestSandboxControl({ family, args: commandArgs });
326
184
  } catch (error) {
327
185
  if (!(error instanceof SandboxControlClientError)) throw error;
328
186
  writeClientError(error);
@@ -1,7 +1,7 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.ts';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.ts';
3
3
 
4
- 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`;
4
+ 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`;
5
5
 
6
6
 
7
7
  function taskArtifact(args: string[] = []): void {
@@ -90,6 +90,7 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
90
90
  descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
91
91
  descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
92
92
  descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
93
+ descriptor('internal', 'platform-comment', 'stage-summary', 'task-bound', 'artifact-write'),
93
94
  descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
94
95
  descriptor('internal', 'platform-issue', 'inspect', 'task-bound', 'diagnostic'),
95
96
  descriptor('internal', 'platform-issue', 'create', 'task-bound', 'remote-write'),
@@ -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> <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';
8
+ 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';
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, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
30
+ const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
31
31
  const operation = args[1]!;
32
32
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
33
33
  if (dryRunConflict) { failUsage(dryRunConflict.message); return; }
@@ -38,8 +38,8 @@ async function taskReview(args: string[] = []): Promise<void> {
38
38
  try {
39
39
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
40
40
  let current = operation === 'preflight'
41
- ? preflightReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true })
42
- : finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
41
+ ? preflightReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
42
+ : finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
43
43
  if (operation === 'preflight') return current;
44
44
  if (current.status !== 'failed' || !overrideTicket) return current;
45
45
  if (!overrideTarget || !overrideScope) { failUsage('override ticket requires target and scope'); return current; }
@@ -49,7 +49,7 @@ async function taskReview(args: string[] = []): Promise<void> {
49
49
  target: overrideTarget, scope: overrideScope
50
50
  }, {
51
51
  effectExecutor: (capability) => {
52
- const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
52
+ const retried = finalizeReviewSummary({ taskRef: args[0]!, stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
53
53
  current = retried;
54
54
  return retried.status === 'failed' || retried.status === 'planned'
55
55
  ? { 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'}` }
@@ -140,11 +140,6 @@ function snapshotValidation(repoRoot: string, commit: string, options: ValidateO
140
140
  cleanupError = error;
141
141
  }
142
142
  }
143
- try {
144
- git(repoRoot, ['worktree', 'prune']);
145
- } catch (error) {
146
- cleanupError ??= error;
147
- }
148
143
  try {
149
144
  fs.rmSync(tempRoot, { recursive: true, force: true });
150
145
  } catch (error) {
@@ -189,6 +189,13 @@ function canonicalizeCommentBody(value: string): SanitizationResult<string> {
189
189
  return sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
190
190
  }
191
191
 
192
+ function canonicalizeSummaryBody(value: string): SanitizationResult<string> {
193
+ const sanitized = canonicalizeCommentBody(value);
194
+ return sanitized.ok
195
+ ? ok(`${sanitized.value.replace(/\n+$/, '')}\n`)
196
+ : sanitized;
197
+ }
198
+
192
199
  function renderSafeCodeFence(value: string, language = '', reservedMarkers: readonly RegExp[] = [CONTROL_MARKER_PATTERN]): string {
193
200
  const content = markerSafe(normalized(value), reservedMarkers);
194
201
  const longestRun = Math.max(0, ...(content.match(/`+/g) || []).map((run) => run.length));
@@ -249,6 +256,7 @@ export {
249
256
  fenceRanges,
250
257
  renderSafeCodeFence,
251
258
  canonicalizeCommentBody,
259
+ canonicalizeSummaryBody,
252
260
  sanitizeMarkdownDocument,
253
261
  splitDocumentPlaceholder
254
262
  };
@@ -331,13 +331,14 @@ function issueSnapshot(value: any, graph?: { type: any; values: Array<{ name: st
331
331
  };
332
332
  }
333
333
 
334
- function commentSnapshot(value: any): RemoteCommentSnapshot {
334
+ function commentSnapshot(value: any, createdSequence: number | null = null): RemoteCommentSnapshot {
335
335
  return {
336
336
  id: String(value.id),
337
337
  author: value.user?.login ? { id: String(value.user.login), name: String(value.user.login) } : null,
338
338
  body: String(value.body || ''),
339
339
  createdAt: String(value.created_at || ''),
340
340
  updatedAt: String(value.updated_at || value.created_at || '')
341
+ ,createdSequence
341
342
  };
342
343
  }
343
344
 
@@ -603,7 +604,15 @@ function createGitHubOperations(client: GitHubClient): Pick<PlatformProvider, 'i
603
604
  const response = client.json<any>(['api', '--paginate', '--slurp', `repos/${repository(context)}/issues/${number}/comments?per_page=100`], { cwd: context.workingDirectory });
604
605
  if (!response.ok) return response;
605
606
  const values = Array.isArray(response.value) ? response.value.flatMap((entry: any) => Array.isArray(entry) ? entry : [entry]) : [];
606
- return { ok: true, value: values.filter((entry: any) => entry && entry.id !== undefined).map(commentSnapshot) };
607
+ const comments = values.filter((entry: any) => entry && typeof entry === 'object');
608
+ let previous = 0;
609
+ const ordered = comments.length === values.length && comments.every((entry: any) => {
610
+ const id = Number(entry.id);
611
+ if (!Number.isSafeInteger(id) || id <= previous) return false;
612
+ previous = id;
613
+ return true;
614
+ });
615
+ return { ok: true, value: comments.map((comment: any) => commentSnapshot(comment, ordered ? Number(comment.id) : null)) };
607
616
  },
608
617
  async write({ context, parent, body, existingComment }) {
609
618
  const number = resourceIdentityNumber(parent);
@@ -27,13 +27,19 @@ import {
27
27
  import { projectTaskComment } from './task-comment-projection.ts';
28
28
  import {
29
29
  canonicalizeCommentBody,
30
+ canonicalizeSummaryBody,
30
31
  escapeHtmlText,
31
32
  fenceRanges,
32
33
  renderSafeCodeFence
33
34
  } from './comment-safety.ts';
34
35
  import type { FenceRange } from './comment-safety.ts';
35
36
 
36
- type RemoteComment = { id: number | string; body: string; user?: { login?: string } };
37
+ type RemoteComment = {
38
+ id: number | string;
39
+ body: string;
40
+ user?: { login?: string };
41
+ createdSequence?: number | null;
42
+ };
37
43
  type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
38
44
  type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
39
45
  type SyncOptions = {
@@ -45,6 +51,8 @@ type SyncOptions = {
45
51
  backfill?: boolean;
46
52
  client?: PlatformClient;
47
53
  runtimeVersion?: string;
54
+ summaryAuthorization?: { sha256: string };
55
+ verifyOnly?: boolean;
48
56
  };
49
57
 
50
58
  function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
@@ -457,6 +465,49 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
457
465
  });
458
466
  }
459
467
 
468
+ function taskManagedComment(comment: RemoteComment, taskId: string): boolean {
469
+ const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
470
+ return marker === MARKERS.task(taskId)
471
+ || marker === MARKERS.summary(taskId)
472
+ || marker.startsWith(`<!-- sync-issue:${taskId}:`);
473
+ }
474
+
475
+ function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boolean; known: boolean; summary: RemoteComment | null } {
476
+ const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
477
+ const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
478
+ if (summary.length !== 1) return { ok: false, known: false, summary: null };
479
+ if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
480
+ return { ok: false, known: false, summary: summary[0]! };
481
+ }
482
+ const sequence = summary[0]!.createdSequence!;
483
+ return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
484
+ }
485
+
486
+ function summaryDigest(comment: RemoteComment): string {
487
+ const body = normalizeCommentContent(comment.body)
488
+ .replace(/^<!-- sync-issue:[^\n]+:summary -->\n## [^\n]+\n\n> [^\n]+\n\n/u, '')
489
+ .replace(/^<details><summary>恢复元数据<\/summary>[\s\S]*?<\/details>\n\n/u, '')
490
+ .replace(/\n---\n\*[^\n]*\*$/u, '');
491
+ const canonical = canonicalizeSummaryBody(body);
492
+ return canonical.ok ? createHash('sha256').update(canonical.value).digest('hex') : '';
493
+ }
494
+
495
+ async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
496
+ return provider.comments?.list
497
+ ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
498
+ ? {
499
+ ok: true as const,
500
+ value: response.value.map((comment: any) => ({
501
+ id: providerCommentId(comment.id, provider),
502
+ body: comment.body,
503
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
504
+ createdSequence: comment.createdSequence
505
+ }))
506
+ }
507
+ : response)
508
+ : unsupportedProviderOperation(provider, 'comments.list');
509
+ }
510
+
460
511
  function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
461
512
  const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
462
513
  if (new Set(markers).size !== markers.length) return { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
@@ -527,18 +578,7 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
527
578
  const context = loaded.ok ? loaded.value.context : loaded.context;
528
579
  if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
529
580
  const issue = resourceIdentityNumber(issueIdentityFromTask);
530
- const listed = loaded.value.provider.comments?.list
531
- ? await loaded.value.provider.comments.list({
532
- context: providerOperationContext(loaded.value),
533
- parent: issueIdentityFromTask
534
- }).then((response) => response.ok
535
- ? { ok: true as const, value: response.value.map((comment) => ({
536
- id: providerCommentId(comment.id, loaded.value.provider),
537
- body: comment.body,
538
- user: comment.author?.name ? { login: comment.author.name } : undefined
539
- })) }
540
- : response)
541
- : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
581
+ const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
542
582
  if (!listed.ok) {
543
583
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
544
584
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
@@ -569,6 +609,26 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
569
609
  }
570
610
  }
571
611
 
612
+ if (options.verifyOnly) {
613
+ if (options.kind !== 'summary' || !options.summaryAuthorization) {
614
+ return platformResult('failed', {
615
+ ...contextFields(context), resource: { kind: 'issue', number: issue },
616
+ error: { code: 'SUMMARY_VERIFICATION_INVALID', message: 'summary verification requires durable authorization', retryable: false }
617
+ });
618
+ }
619
+ const position = summaryPosition(listed.value, resolved.taskId);
620
+ if (!position.ok || !position.summary || summaryDigest(position.summary) !== options.summaryAuthorization.sha256) {
621
+ return platformResult('failed', {
622
+ ...contextFields(context), resource: { kind: 'issue', number: issue },
623
+ error: { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary marker, body digest, or managed comment order is invalid', retryable: true }
624
+ });
625
+ }
626
+ return platformResult('no-op', {
627
+ ...contextFields(context), changed: false, resource: { kind: 'issue', number: issue },
628
+ comment: { kind: options.kind, marker: desired[0]!.marker, ids: [position.summary.id], parts: 1 }, error: null
629
+ });
630
+ }
631
+
572
632
 
573
633
  // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
574
634
  if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
@@ -646,6 +706,65 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
646
706
  }
647
707
  operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
648
708
  }
709
+ if (options.kind === 'summary') {
710
+ const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
711
+ if (!refreshed.ok) {
712
+ return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
713
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
714
+ });
715
+ }
716
+ const position = summaryPosition(refreshed.value, resolved.taskId);
717
+ if (!position.ok && !position.known) {
718
+ return platformResult('blocked', {
719
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
720
+ error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
721
+ });
722
+ }
723
+ if (!position.ok && position.summary) {
724
+ const owner = position.summary.user?.login;
725
+ if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
726
+ || !owner || owner !== context.platform.currentUser) {
727
+ return platformResult('failed', {
728
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
729
+ error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
730
+ });
731
+ }
732
+ const deleted = loaded.value.provider.comments?.delete
733
+ ? await loaded.value.provider.comments.delete({
734
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
735
+ comment: { kind: 'id', value: String(position.summary.id) },
736
+ mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
737
+ })
738
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
739
+ if (!deleted.ok) {
740
+ return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
741
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
742
+ });
743
+ }
744
+ const written = loaded.value.provider.comments?.write
745
+ ? await loaded.value.provider.comments.write({
746
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
747
+ mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
748
+ })
749
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
750
+ if (!written.ok) {
751
+ return platformResult(written.error.retryable ? 'blocked' : 'failed', {
752
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
753
+ });
754
+ }
755
+ ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
756
+ operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
757
+ const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
758
+ if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
759
+ const error = reconciled.ok
760
+ ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
761
+ : reconciled.error;
762
+ return platformResult(error.retryable ? 'blocked' : 'failed', {
763
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
764
+ });
765
+ }
766
+ }
767
+ }
649
768
  const changed = operations.some((operation) => operation.status === 'applied');
650
769
  const result = platformResult(changed ? 'applied' : 'no-op', {
651
770
  ...contextFields(context),
@@ -574,9 +574,8 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
574
574
  const failure = plan.operations.find((operation) => operation.status === 'failed');
575
575
  if (failure) 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 } });
576
576
  const planned = plan.operations.filter((operation) => operation.status === 'planned');
577
- const skipped = plan.operations.some((operation) => operation.status === 'skipped');
578
- if (options.dryRun) return result(planned.length ? 'planned' : skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
579
- if (planned.length === 0) return result(skipped ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
577
+ if (options.dryRun) return result(planned.length ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
578
+ if (planned.length === 0) return result('no-op', base.resolved.taskId, base.issueNumber, { issue: snapshot, operations: plan.operations, error: null });
580
579
  const payload = applyRestOperations(snapshot, planned) as Partial<{
581
580
  title: string; body: string; labels: string[]; assignees: string[];
582
581
  milestone: string | null; state: 'open' | 'closed'; fields: Record<string, string | number | null>;
@@ -611,7 +610,7 @@ async function syncPlatformIssue(taskRef: string, options: SyncOptions): Promise
611
610
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: 'Issue labels did not converge after update', retryable: true }
612
611
  });
613
612
  }
614
- return result(skipped ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, {
613
+ return result('applied', base.resolved.taskId, base.issueNumber, {
615
614
  changed: updated.value.changed, platform: base.context.platform, capabilities: base.context.capabilities,
616
615
  resource: { kind: 'issue', number: base.issueNumber, identity: base.issueIdentity }, issue: finalIssue,
617
616
  operations: plan.operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
@@ -84,6 +84,8 @@ type RemoteCommentSnapshot = {
84
84
  body: string;
85
85
  createdAt: string;
86
86
  updatedAt: string;
87
+ /** Strict creation order when the provider can prove it for this list snapshot. */
88
+ createdSequence: number | null;
87
89
  };
88
90
 
89
91
  type ChangeRequestSnapshot = {