@fitlab-ai/agent-infra 0.11.1 → 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 (88) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/platform-comment.js +59 -0
  3. package/dist/lib/internal/sandbox-control.js +4 -131
  4. package/dist/lib/internal/task-artifact.js +1 -1
  5. package/dist/lib/internal/task-operation-registry.js +1 -0
  6. package/dist/lib/internal/task-review.js +5 -5
  7. package/dist/lib/internal/task-validate.js +0 -6
  8. package/dist/lib/platform/comment-safety.js +7 -1
  9. package/dist/lib/platform/github-provider.js +13 -3
  10. package/dist/lib/platform/issue-comments.js +120 -13
  11. package/dist/lib/platform/provider-contract.d.ts +2 -0
  12. package/dist/lib/platform/provider-validation.js +7 -2
  13. package/dist/lib/platform/verification-sync.js +19 -2
  14. package/dist/lib/sandbox/control/executor.js +0 -38
  15. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  16. package/dist/lib/sandbox/control/task-view.js +3 -1
  17. package/dist/lib/sandbox/control/workflow-executor.js +28 -43
  18. package/dist/lib/task/artifact-command.js +6 -6
  19. package/dist/lib/task/codex-orchestration.js +10 -208
  20. package/dist/lib/task/commit-operation.js +3 -18
  21. package/dist/lib/task/control-authority.js +1 -8
  22. package/dist/lib/task/delegation-receipts.js +14 -76
  23. package/dist/lib/task/events.js +89 -237
  24. package/dist/lib/task/finalization-state.js +2 -1
  25. package/dist/lib/task/finalization.js +273 -49
  26. package/dist/lib/task/lifecycle-execution.js +2 -11
  27. package/dist/lib/task/local-artifact-finalization.js +25 -242
  28. package/dist/lib/task/orchestration.js +0 -18
  29. package/dist/lib/task/review-command.js +0 -1
  30. package/dist/lib/task/review-finalization.js +46 -211
  31. package/dist/lib/task/workflow-command.js +4 -1
  32. package/dist/lib/task/workflow-dispatch.js +55 -3
  33. package/dist/lifecycle-build-manifest.json +3 -3
  34. package/lib/internal/cli-route-inventory.ts +1 -1
  35. package/lib/internal/platform-comment.ts +38 -0
  36. package/lib/internal/sandbox-control.ts +3 -145
  37. package/lib/internal/task-artifact.ts +1 -1
  38. package/lib/internal/task-operation-registry.ts +1 -0
  39. package/lib/internal/task-review.ts +5 -5
  40. package/lib/internal/task-validate.ts +0 -5
  41. package/lib/platform/comment-safety.ts +8 -0
  42. package/lib/platform/github-provider.ts +11 -2
  43. package/lib/platform/issue-comments.ts +132 -13
  44. package/lib/platform/provider-contract.ts +2 -0
  45. package/lib/platform/provider-validation.ts +6 -2
  46. package/lib/platform/verification-sync.ts +19 -2
  47. package/lib/sandbox/control/executor.ts +0 -44
  48. package/lib/sandbox/control/finalization-response.ts +10 -2
  49. package/lib/sandbox/control/task-view.ts +3 -1
  50. package/lib/sandbox/control/workflow-executor.ts +23 -50
  51. package/lib/task/artifact-command.ts +6 -7
  52. package/lib/task/codex-orchestration.ts +8 -241
  53. package/lib/task/commit-operation.ts +3 -23
  54. package/lib/task/control-authority.ts +1 -8
  55. package/lib/task/delegation-receipts.ts +12 -79
  56. package/lib/task/events.ts +106 -255
  57. package/lib/task/finalization-state.ts +2 -1
  58. package/lib/task/finalization.ts +277 -57
  59. package/lib/task/lifecycle-execution.ts +10 -21
  60. package/lib/task/local-artifact-finalization.ts +23 -281
  61. package/lib/task/orchestration.ts +0 -34
  62. package/lib/task/review-command.ts +0 -1
  63. package/lib/task/review-finalization.ts +45 -193
  64. package/lib/task/workflow-command.ts +4 -1
  65. package/lib/task/workflow-dispatch.ts +46 -3
  66. package/package.json +2 -2
  67. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  68. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  69. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  70. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  71. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  72. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  73. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  74. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  75. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  76. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  77. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  78. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  79. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  80. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  81. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  82. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  83. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  84. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  85. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  86. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  87. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  88. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -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),
@@ -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 = {
@@ -218,13 +218,17 @@ function issueType(value: unknown, label: string): NonNullable<IssueSnapshot['is
218
218
 
219
219
  function validateComment(value: unknown): RemoteCommentSnapshot {
220
220
  const item = record(value, 'comment');
221
- exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt'], 'comment');
221
+ exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt', 'createdSequence'], 'comment');
222
222
  return {
223
223
  id: stringValue(item.id, 'comment.id'),
224
224
  author: author(item.author, 'comment.author'),
225
225
  body: stringValue(item.body, 'comment.body', true),
226
226
  createdAt: utcTimestamp(item.createdAt, 'comment.createdAt'),
227
- updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt')
227
+ updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt'),
228
+ createdSequence: item.createdSequence === null ? null : (() => {
229
+ if (!Number.isSafeInteger(item.createdSequence) || Number(item.createdSequence) < 1) throw new Error('comment.createdSequence must be a positive safe integer or null');
230
+ return Number(item.createdSequence);
231
+ })()
228
232
  };
229
233
  }
230
234
 
@@ -13,7 +13,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
13
13
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
14
14
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
15
15
  import { taskIssueIdentity } from "./task-identities.ts";
16
- import { renderTaskCommentResult } from "./issue-comments.ts";
16
+ import { MARKERS, renderTaskCommentResult } from "./issue-comments.ts";
17
17
  import {
18
18
  CONTROL_MARKER_PATTERN,
19
19
  canonicalizeCommentBody,
@@ -279,7 +279,9 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
279
279
  }
280
280
  return {
281
281
  issue,
282
- comments: facts.value.comments.map((comment: { id: string; body: string }) => ({ id: comment.id, body: comment.body })),
282
+ comments: facts.value.comments.map((comment: { id: string; body: string; createdSequence: number | null }) => ({
283
+ id: comment.id, body: comment.body, createdSequence: comment.createdSequence
284
+ })),
283
285
  prComments,
284
286
  prLabels: changeRequest?.labels || null,
285
287
  issueType: issueSnapshot
@@ -361,6 +363,21 @@ function checkCommentMarker(context: any, remoteData: any, shared: VerificationS
361
363
 
362
364
  const comment = findCommentByMarker(remoteData.comments, context.marker);
363
365
  if (comment) {
366
+ if (context.marker === MARKERS.summary(context.task.metadata.id)) {
367
+ const managed = remoteData.comments.filter((candidate: any) => {
368
+ const first = String(candidate.body || '').replace(/\r\n/g, '\n').split('\n', 1)[0] || '';
369
+ return first === MARKERS.task(context.task.metadata.id)
370
+ || first === MARKERS.summary(context.task.metadata.id)
371
+ || first.startsWith(`<!-- sync-issue:${context.task.metadata.id}:`);
372
+ });
373
+ if (managed.some((candidate: any) => !Number.isSafeInteger(candidate.createdSequence) || candidate.createdSequence < 1)) {
374
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment ordering is not provable`, "check_failed");
375
+ }
376
+ const summarySequence = comment.createdSequence;
377
+ if (!managed.every((candidate: any) => summarySequence >= candidate.createdSequence)) {
378
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment is not last among task-managed comments`, "check_failed");
379
+ }
380
+ }
364
381
  return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
365
382
  }
366
383
 
@@ -441,49 +441,6 @@ async function executeRequestInner(
441
441
  ? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
442
442
  : orchestrationFailure(code, message);
443
443
  }
444
- let controllerBinding: Readonly<{
445
- instanceDigest: string;
446
- controlGeneration: string;
447
- }> | null = null;
448
- if (request.family === 'task-orchestration') {
449
- const codexPrepare = operation.family === 'task-orchestration'
450
- && operation.intent === 'prepare'
451
- && operation.input.client === 'codex';
452
- if (codexPrepare) {
453
- if (!request.controllerProof) {
454
- const authority = options.lifecycleRecoveryAttestation;
455
- if (!authority || authority.phase !== 'orchestration.prepare') {
456
- return orchestrationFailure(
457
- 'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
458
- 'Codex prepare requires a current controller lease proof'
459
- );
460
- }
461
- controllerBinding = {
462
- instanceDigest: authority.controllerInstanceDigest,
463
- controlGeneration: authority.controlGeneration
464
- };
465
- } else {
466
- try {
467
- controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
468
- manifest,
469
- manifestPath,
470
- proof: request.controllerProof,
471
- buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
472
- });
473
- } catch (error) {
474
- const code = error instanceof CodexControllerRegistrationError
475
- ? error.code
476
- : 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
477
- return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
478
- }
479
- }
480
- } else if (request.controllerProof !== null) {
481
- return orchestrationFailure(
482
- 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID',
483
- 'Controller proof is only accepted for canonical Codex prepare'
484
- );
485
- }
486
- }
487
444
  const diagnosticLog: OrchestrationDiagnosticLogger = (event, fields): void => {
488
445
  appendExecutorAudit(manifest, event.startsWith('orchestration-') ? event : `orchestration-${event}`, {
489
446
  requestId: request.id,
@@ -501,7 +458,6 @@ async function executeRequestInner(
501
458
  manifestPath,
502
459
  requestId: request.id,
503
460
  diagnosticLog,
504
- ...(controllerBinding ? { controllerBinding } : {}),
505
461
  lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
506
462
  });
507
463
  const format = (value: unknown): SandboxControlExecutionResult => {