@fitlab-ai/agent-infra 0.11.1 → 0.11.3

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 (131) hide show
  1. package/dist/bin/internal-cli.js +7 -2
  2. package/dist/lib/agent-clients/adapter.js +3 -0
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  6. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  7. package/dist/lib/internal/cli-route-inventory.js +1 -1
  8. package/dist/lib/internal/manual-validation.js +22 -27
  9. package/dist/lib/internal/platform-comment.js +59 -0
  10. package/dist/lib/internal/sandbox-control.js +2 -171
  11. package/dist/lib/internal/task-artifact.js +1 -1
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +7 -2
  14. package/dist/lib/internal/task-review.js +5 -5
  15. package/dist/lib/internal/task-validate.js +0 -6
  16. package/dist/lib/platform/comment-safety.js +7 -1
  17. package/dist/lib/platform/github-provider.js +13 -3
  18. package/dist/lib/platform/issue-comments.js +93 -15
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/platform/provider-contract.d.ts +2 -0
  21. package/dist/lib/platform/provider-validation.js +7 -2
  22. package/dist/lib/platform/verification-sync.js +19 -2
  23. package/dist/lib/sandbox/control/client.js +3 -44
  24. package/dist/lib/sandbox/control/executor.js +14 -117
  25. package/dist/lib/sandbox/control/finalization-response.js +8 -2
  26. package/dist/lib/sandbox/control/protocol.js +5 -64
  27. package/dist/lib/sandbox/control/server.js +29 -206
  28. package/dist/lib/sandbox/control/state.js +1 -7
  29. package/dist/lib/sandbox/control/task-view.js +2 -1
  30. package/dist/lib/task/artifact-command.js +6 -6
  31. package/dist/lib/task/codex-orchestration.js +10 -208
  32. package/dist/lib/task/commit-operation.js +3 -18
  33. package/dist/lib/task/control-authority.js +35 -44
  34. package/dist/lib/task/control-recovery.js +3 -42
  35. package/dist/lib/task/delegation-receipts.js +16 -78
  36. package/dist/lib/task/events.js +89 -237
  37. package/dist/lib/task/finalization-artifacts.js +1 -3
  38. package/dist/lib/task/finalization-state.js +2 -1
  39. package/dist/lib/task/finalization.js +208 -57
  40. package/dist/lib/task/lifecycle-execution.js +2 -11
  41. package/dist/lib/task/lifecycle.js +1 -1
  42. package/dist/lib/task/local-artifact-finalization.js +25 -242
  43. package/dist/lib/task/manual-validation-receipt.js +8 -3
  44. package/dist/lib/task/orchestration.js +12 -24
  45. package/dist/lib/task/review-command.js +0 -1
  46. package/dist/lib/task/review-finalization.js +46 -211
  47. package/dist/lifecycle-build-manifest.json +3 -3
  48. package/lib/agent-clients/adapter.ts +46 -0
  49. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  51. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  52. package/lib/agent-clients/adapters/codex.ts +10 -0
  53. package/lib/internal/cli-route-inventory.ts +1 -1
  54. package/lib/internal/manual-validation.ts +22 -26
  55. package/lib/internal/platform-comment.ts +38 -0
  56. package/lib/internal/sandbox-control.ts +1 -185
  57. package/lib/internal/task-artifact.ts +1 -1
  58. package/lib/internal/task-lifecycle.ts +2 -2
  59. package/lib/internal/task-operation-registry.ts +8 -4
  60. package/lib/internal/task-review.ts +5 -5
  61. package/lib/internal/task-validate.ts +0 -5
  62. package/lib/platform/comment-safety.ts +8 -0
  63. package/lib/platform/github-provider.ts +11 -2
  64. package/lib/platform/issue-comments.ts +102 -17
  65. package/lib/platform/pr-summary.ts +10 -2
  66. package/lib/platform/provider-contract.ts +2 -0
  67. package/lib/platform/provider-validation.ts +6 -2
  68. package/lib/platform/verification-sync.ts +19 -2
  69. package/lib/sandbox/control/client.ts +5 -68
  70. package/lib/sandbox/control/executor.ts +14 -125
  71. package/lib/sandbox/control/finalization-response.ts +8 -2
  72. package/lib/sandbox/control/protocol.ts +7 -83
  73. package/lib/sandbox/control/server.ts +30 -220
  74. package/lib/sandbox/control/state.ts +1 -9
  75. package/lib/sandbox/control/task-view.ts +2 -1
  76. package/lib/task/artifact-command.ts +6 -7
  77. package/lib/task/codex-orchestration.ts +8 -241
  78. package/lib/task/commit-operation.ts +3 -23
  79. package/lib/task/control-authority.ts +39 -55
  80. package/lib/task/control-recovery.ts +3 -43
  81. package/lib/task/delegation-receipts.ts +14 -81
  82. package/lib/task/events.ts +106 -255
  83. package/lib/task/finalization-artifacts.ts +1 -3
  84. package/lib/task/finalization-state.ts +2 -1
  85. package/lib/task/finalization.ts +221 -65
  86. package/lib/task/lifecycle-execution.ts +10 -21
  87. package/lib/task/lifecycle.ts +2 -2
  88. package/lib/task/local-artifact-finalization.ts +23 -281
  89. package/lib/task/manual-validation-receipt.ts +8 -2
  90. package/lib/task/orchestration.ts +30 -53
  91. package/lib/task/review-command.ts +0 -1
  92. package/lib/task/review-finalization.ts +45 -193
  93. package/package.json +2 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
  95. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
  96. package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
  97. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  101. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  102. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  105. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  106. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  109. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  110. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  111. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  112. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  113. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  114. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  115. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  116. package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
  117. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
  118. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  119. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  120. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  121. package/dist/lib/sandbox/control/workflow-executor.js +0 -157
  122. package/dist/lib/task/lifecycle-recovery.js +0 -549
  123. package/dist/lib/task/recovery-warning.js +0 -19
  124. package/dist/lib/task/workflow-command.js +0 -18
  125. package/dist/lib/task/workflow-dispatch.js +0 -161
  126. package/lib/sandbox/control/task-workflow.ts +0 -95
  127. package/lib/sandbox/control/workflow-executor.ts +0 -183
  128. package/lib/task/lifecycle-recovery.ts +0 -691
  129. package/lib/task/recovery-warning.ts +0 -21
  130. package/lib/task/workflow-command.ts +0 -19
  131. package/lib/task/workflow-dispatch.ts +0 -171
@@ -1,141 +1,14 @@
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
- requestSandboxTaskWorkflow,
10
- resolveVisibleActiveShortId,
11
5
  SandboxControlClientError
12
6
  } from '../sandbox/control/client.ts';
13
7
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
14
8
  import { serveSandboxControl } from '../sandbox/control/server.ts';
15
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
16
- import {
17
- controllerProofFromContext,
18
- verifyCodexSandboxControllerContextWithWarnings
19
- } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
20
10
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
21
11
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
22
- 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
12
 
140
13
  type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
141
14
 
@@ -263,66 +136,9 @@ async function sandboxControl(args: string[]): Promise<void> {
263
136
  sandboxFinalizationClient(commandArgs);
264
137
  return;
265
138
  }
266
- if (family === 'task-workflow') {
267
- const command = commandArgs[0] as Parameters<typeof createTaskWorkflowRequest>[0] | undefined;
268
- const taskId = process.env.AGENT_INFRA_TASK_ID;
269
- const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
270
- if (!command || !taskId || !generation) {
271
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: 'task-workflow requires a bound task and generation' } })}\n`);
272
- process.exitCode = 1;
273
- return;
274
- }
275
- let workflow;
276
- try {
277
- const args = withoutValue(commandArgs.slice(1), '--capability-ref');
278
- if (/^\d+$/.test(args[0] ?? '')) {
279
- const resolved = resolveVisibleActiveShortId(args[0]!);
280
- if (resolved !== taskId) throw new Error('SANDBOX_TASK_REF_MISMATCH');
281
- args[0] = resolved;
282
- }
283
- workflow = createTaskWorkflowRequest(command, args, taskId, generation);
284
- }
285
- catch (error) {
286
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
287
- process.exitCode = 1;
288
- return;
289
- }
290
- let response;
291
- try {
292
- const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
293
- response = requestSandboxTaskWorkflow({
294
- workflow,
295
- ...(authority ? { authority } : {})
296
- });
297
- }
298
- catch (error) {
299
- if (!(error instanceof SandboxControlClientError)) throw error;
300
- writeClientError(error);
301
- process.exitCode = error.detail.retryable ? 75 : 1;
302
- return;
303
- }
304
- process.stdout.write(response.stdout);
305
- process.stderr.write(response.stderr);
306
- process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
307
- return;
308
- }
309
139
  let response;
310
140
  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
- }
141
+ response = requestSandboxControl({ family, args: commandArgs });
326
142
  } catch (error) {
327
143
  if (!(error instanceof SandboxControlClientError)) throw error;
328
144
  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 {
@@ -7,10 +7,10 @@ import {
7
7
  import { lifecycleIntentCatalog } from '../task/lifecycle.ts';
8
8
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
9
9
  import type { TaskLifecycleResult } from '../task/lifecycle.ts';
10
- import type { LifecycleRecoveryResult } from '../task/lifecycle-recovery.ts';
10
+ import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
11
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
12
12
 
13
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --stage <stage> --round <round> --artifact <artifact> --reason <reason>\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
13
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
14
14
 
15
15
  function usageFailure(message: string): void {
16
16
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
@@ -22,7 +22,7 @@ export type TaskOperationEffect = TaskViewAccessEffect;
22
22
  export type TaskRefSource = 'argv' | 'environment' | 'none' | 'delegated' | 'input';
23
23
 
24
24
  export type SandboxControlTransportDecision = Readonly<{
25
- kind: 'direct-host' | 'broker-client' | 'fail-closed';
25
+ kind: 'direct-host' | 'sandbox-local' | 'broker-client' | 'fail-closed';
26
26
  reasonCode: string | null;
27
27
  }>;
28
28
 
@@ -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'),
@@ -276,6 +277,7 @@ export function resolveTaskOperation(
276
277
  export function resolveDelegatedTaskOperation(args: readonly string[]): TaskOperationDescriptor | null {
277
278
  const [operation, ...rest] = args;
278
279
  if (operation !== 'client' || !rest[0]) return null;
280
+ if (rest[0] === 'task-orchestration') return null;
279
281
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
280
282
  }
281
283
 
@@ -339,7 +341,7 @@ function taskMarkerState(env: NodeJS.ProcessEnv): TaskMarkerState {
339
341
 
340
342
  export function resolveSandboxControlTransport(
341
343
  env: NodeJS.ProcessEnv = process.env,
342
- options: Readonly<{ statusMountPath?: string }> = {}
344
+ options: Readonly<{ statusMountPath?: string; localWorkflow?: boolean }> = {}
343
345
  ): SandboxControlTransportDecision {
344
346
  const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
345
347
  || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
@@ -406,13 +408,15 @@ export function resolveSandboxControlTransport(
406
408
  if (!hasCompleteConfig) {
407
409
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
408
410
  }
409
- return { kind: 'broker-client', reasonCode: null };
411
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
410
412
  }
411
413
  if (!hasAnyMarker) return { kind: 'direct-host', reasonCode: null };
412
414
  if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
413
415
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
414
416
  }
415
- return { kind: 'broker-client', reasonCode: null };
417
+ return options.localWorkflow
418
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
419
+ : { kind: 'broker-client', reasonCode: null };
416
420
  }
417
421
 
418
422
  export function hasTaskBoundMarker(env: NodeJS.ProcessEnv = process.env): boolean {
@@ -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);
@@ -33,7 +33,12 @@ import {
33
33
  } from './comment-safety.ts';
34
34
  import type { FenceRange } from './comment-safety.ts';
35
35
 
36
- type RemoteComment = { id: number | string; body: string; user?: { login?: string } };
36
+ type RemoteComment = {
37
+ id: number | string;
38
+ body: string;
39
+ user?: { login?: string };
40
+ createdSequence?: number | null;
41
+ };
37
42
  type RenderedChunk = { marker: string; body: string; content: string; part: number; total: number };
38
43
  type CommentKind = 'task' | 'artifact' | 'summary' | 'cancel';
39
44
  type SyncOptions = {
@@ -45,6 +50,7 @@ type SyncOptions = {
45
50
  backfill?: boolean;
46
51
  client?: PlatformClient;
47
52
  runtimeVersion?: string;
53
+ summaryAuthorization?: { sha256: string };
48
54
  };
49
55
 
50
56
  function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
@@ -68,8 +74,6 @@ const ARTIFACT_TITLES: Record<string, string> = {
68
74
  'review-plan': '技术方案审查',
69
75
  code: '实现报告',
70
76
  'review-code': '代码审查',
71
- 'manual-validation': '人工验证报告',
72
- 'validation-run': '验证运行证据',
73
77
  'pr-review': 'PR 审查报告'
74
78
  };
75
79
 
@@ -146,7 +150,7 @@ function taskCommentLanguage(repoRoot: string): string {
146
150
 
147
151
  function artifactIdentity(artifact: string): { stem: string; title: string } {
148
152
  const stem = path.basename(artifact, '.md');
149
- const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
153
+ const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
150
154
  if (!match) throw new Error(`unsupported artifact '${artifact}'`);
151
155
  const base = ARTIFACT_TITLES[match[1]!]!;
152
156
  const round = match[2] ? Number(match[2]) : 1;
@@ -457,6 +461,40 @@ function relatedComments(comments: RemoteComment[], prefix: string): RemoteComme
457
461
  });
458
462
  }
459
463
 
464
+ function taskManagedComment(comment: RemoteComment, taskId: string): boolean {
465
+ const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
466
+ return marker === MARKERS.task(taskId)
467
+ || marker === MARKERS.summary(taskId)
468
+ || marker.startsWith(`<!-- sync-issue:${taskId}:`);
469
+ }
470
+
471
+ function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boolean; known: boolean; summary: RemoteComment | null } {
472
+ const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
473
+ const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
474
+ if (summary.length !== 1) return { ok: false, known: false, summary: null };
475
+ if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
476
+ return { ok: false, known: false, summary: summary[0]! };
477
+ }
478
+ const sequence = summary[0]!.createdSequence!;
479
+ return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
480
+ }
481
+
482
+ async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
483
+ return provider.comments?.list
484
+ ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
485
+ ? {
486
+ ok: true as const,
487
+ value: response.value.map((comment: any) => ({
488
+ id: providerCommentId(comment.id, provider),
489
+ body: comment.body,
490
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
491
+ createdSequence: comment.createdSequence
492
+ }))
493
+ }
494
+ : response)
495
+ : unsupportedProviderOperation(provider, 'comments.list');
496
+ }
497
+
460
498
  function validateRelatedMarkerSet(comments: RemoteComment[], prefix: string): { ok: boolean; code: string | null } {
461
499
  const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
462
500
  if (new Set(markers).size !== markers.length) return { ok: false, code: 'COMMENT_MARKER_CONFLICT' };
@@ -527,18 +565,7 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
527
565
  const context = loaded.ok ? loaded.value.context : loaded.context;
528
566
  if (!hasResolvedPlatformContext(context) || !loaded.ok) return context;
529
567
  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');
568
+ const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
542
569
  if (!listed.ok) {
543
570
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
544
571
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
@@ -569,7 +596,6 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
569
596
  }
570
597
  }
571
598
 
572
-
573
599
  // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
574
600
  if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
575
601
  const operations = existing.map((comment): PlatformOperation => ({
@@ -646,6 +672,65 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
646
672
  }
647
673
  operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
648
674
  }
675
+ if (options.kind === 'summary') {
676
+ const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
677
+ if (!refreshed.ok) {
678
+ return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
679
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
680
+ });
681
+ }
682
+ const position = summaryPosition(refreshed.value, resolved.taskId);
683
+ if (!position.ok && !position.known) {
684
+ return platformResult('blocked', {
685
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
686
+ error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
687
+ });
688
+ }
689
+ if (!position.ok && position.summary) {
690
+ const owner = position.summary.user?.login;
691
+ if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
692
+ || !owner || owner !== context.platform.currentUser) {
693
+ return platformResult('failed', {
694
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
695
+ error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
696
+ });
697
+ }
698
+ const deleted = loaded.value.provider.comments?.delete
699
+ ? await loaded.value.provider.comments.delete({
700
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
701
+ comment: { kind: 'id', value: String(position.summary.id) },
702
+ mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
703
+ })
704
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
705
+ if (!deleted.ok) {
706
+ return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
707
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
708
+ });
709
+ }
710
+ const written = loaded.value.provider.comments?.write
711
+ ? await loaded.value.provider.comments.write({
712
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0]!.body,
713
+ mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
714
+ })
715
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
716
+ if (!written.ok) {
717
+ return platformResult(written.error.retryable ? 'blocked' : 'failed', {
718
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
719
+ });
720
+ }
721
+ ids.splice(0, ids.length, providerCommentId((written.value as { remoteId: string }).remoteId, loaded.value.provider));
722
+ operations.push({ name: `comment:${desired[0]!.marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
723
+ const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
724
+ if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
725
+ const error = reconciled.ok
726
+ ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
727
+ : reconciled.error;
728
+ return platformResult(error.retryable ? 'blocked' : 'failed', {
729
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
730
+ });
731
+ }
732
+ }
733
+ }
649
734
  const changed = operations.some((operation) => operation.status === 'applied');
650
735
  const result = platformResult(changed ? 'applied' : 'no-op', {
651
736
  ...contextFields(context),
@@ -35,7 +35,7 @@ import type {
35
35
  MechanicalChangeReport,
36
36
  PrChangeReport
37
37
  } from './pr-change-report.ts';
38
- import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
38
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
39
39
  import { readManualValidationCompletion } from '../task/manual-validation-completion.ts';
40
40
 
41
41
  type SummaryComment = { id: number | string; body: string };
@@ -582,6 +582,7 @@ async function syncPullRequestSummary(
582
582
  const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
583
583
  if (!initial.ok) return fail(initial.status, context, initial.error);
584
584
  const manual = options.manualValidation;
585
+ let manualEvidenceDigest: string | null = null;
585
586
  const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
586
587
  if (hasFinalManualValidation && (!manual || manual.phase !== 'final')) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
587
588
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
@@ -599,6 +600,7 @@ async function syncPullRequestSummary(
599
600
  });
600
601
  if (!completion.ok) return fail('failed', context, platformError(completion.error), prNumber);
601
602
  const { receipt, transaction } = completion.value;
603
+ manualEvidenceDigest = receipt.evidenceDigest;
602
604
  if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
603
605
  if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', message: 'final summary body does not match the canonical receipt projection', retryable: false }, prNumber);
604
606
  }
@@ -635,7 +637,13 @@ async function syncPullRequestSummary(
635
637
  }
636
638
  const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
637
639
  if (!replaced.ok) return fail('failed', context, platformError(replaced.error));
638
- const desired = buildPullRequestSummary(resolved.taskId, replaced.value, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
640
+ const renderedBody = manual?.phase === 'final'
641
+ ? replaced.value.replace(
642
+ MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
643
+ `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
644
+ )
645
+ : replaced.value;
646
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
639
647
  if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
640
648
  code: 'PR_SUMMARY_RENDER_INVALID',
641
649
  message: 'Summary contains an invalid or duplicated reserved control marker',
@@ -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