@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,118 +1,9 @@
1
- import { createHash, randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskControl, requestSandboxTaskWorkflow, resolveVisibleActiveShortId, SandboxControlClientError } from '../sandbox/control/client.js';
1
+ import { recoverSandboxControl, requestSandboxControl, requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.js';
5
2
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
6
3
  import { serveSandboxControl } from '../sandbox/control/server.js';
7
4
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
8
- import { controllerProofFromContext, verifyCodexSandboxControllerContextWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
9
5
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
10
6
  import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.js';
11
- import { createTaskWorkflowRequest } from '../sandbox/control/task-workflow.js';
12
- import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
13
- import { routeOrchestration } from '../task/orchestration.js';
14
- import { parseArtifactName } from '../task/artifact-name.js';
15
- function valueAfter(args, flag) {
16
- for (let index = 0; index < args.length; index += 1) {
17
- if (args[index] === flag)
18
- return args[index + 1];
19
- if (args[index]?.startsWith(`${flag}=`))
20
- return args[index].slice(flag.length + 1);
21
- }
22
- return undefined;
23
- }
24
- function withoutValue(args, flag) {
25
- const result = [];
26
- for (let index = 0; index < args.length; index += 1) {
27
- if (args[index] === flag) {
28
- index += 1;
29
- continue;
30
- }
31
- if (args[index]?.startsWith(`${flag}=`))
32
- continue;
33
- result.push(args[index]);
34
- }
35
- return result;
36
- }
37
- function lifecycleIds(authorityRef, taskId) {
38
- const digest = (label) => createHash('sha256')
39
- .update(`${label}\0${taskId}\0${authorityRef}`, 'utf8')
40
- .digest('hex');
41
- return { operationId: digest('operation'), lifecycleRequestId: digest('lifecycle-request') };
42
- }
43
- function lifecycleAuthorityIdentity(authorityRef, taskId, requestId, phase, family, artifact, round) {
44
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
45
- if (!authorityRef || !contextPath)
46
- return undefined;
47
- const routed = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() });
48
- const buildIdentity = computeLifecycleBuildIdentity(process.cwd());
49
- const hookDefinitionHash = createHash('sha256')
50
- .update(readFileSync(path.join(process.cwd(), '.codex', 'hooks.json')))
51
- .digest('hex');
52
- const ids = lifecycleIds(authorityRef, taskId);
53
- return {
54
- version: 1,
55
- requestId,
56
- operationId: ids.operationId,
57
- phase,
58
- taskId,
59
- family,
60
- artifact,
61
- round,
62
- lifecycleRequestId: ids.lifecycleRequestId,
63
- authorityRef,
64
- expectedControlGeneration: routed.context.controlGeneration,
65
- expectedControllerInstanceDigest: routed.context.controllerInstanceDigest,
66
- expectedBuildIdentityDigest: createHash('sha256').update(JSON.stringify(buildIdentity)).digest('hex'),
67
- expectedHookDefinitionHash: hookDefinitionHash
68
- };
69
- }
70
- function lifecycleAuthoritySelector(args) {
71
- if (!isCanonicalCodexPrepare(args))
72
- return undefined;
73
- const authorityRef = valueAfter(args, '--capability-ref');
74
- const taskId = args[0];
75
- if (!authorityRef || !taskId)
76
- return undefined;
77
- const routed = routeOrchestration(taskId);
78
- if (!routed.next || !['analysis', 'plan', 'code'].includes(routed.next.stage))
79
- return undefined;
80
- return lifecycleAuthorityIdentity(authorityRef, taskId, randomUUID(), 'orchestration.prepare', routed.next.stage === 'analysis' ? 'analysis' : routed.next.stage === 'plan' ? 'plan' : 'code', routed.next.artifact, routed.next.round);
81
- }
82
- function lifecycleWorkflowAuthoritySelector(originalArgs, workflow) {
83
- const authorityRef = valueAfter(originalArgs, '--capability-ref');
84
- if (!authorityRef)
85
- return undefined;
86
- if (workflow.operation === 'artifact-finalize-local') {
87
- const family = valueAfter(workflow.args, '--family');
88
- const artifact = valueAfter(workflow.args, '--artifact');
89
- const parsed = artifact ? parseArtifactName(artifact) : null;
90
- if ((family !== 'analysis' && family !== 'plan' && family !== 'code')
91
- || !parsed || parsed.family !== family)
92
- return undefined;
93
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'artifact.finalize-local', family, artifact, parsed.round);
94
- }
95
- if (workflow.operation !== 'event')
96
- return undefined;
97
- const completed = /^(analysis|plan|code)\.completed$/u.exec(workflow.args[1] ?? '');
98
- const artifact = valueAfter(workflow.args, '--artifact');
99
- const parsed = artifact ? parseArtifactName(artifact) : null;
100
- if (!completed || !parsed || parsed.family !== completed[1])
101
- return undefined;
102
- return lifecycleAuthorityIdentity(authorityRef, workflow.taskId, workflow.id, 'task-event.completed', completed[1], artifact, parsed.round);
103
- }
104
- function isCanonicalCodexPrepare(args) {
105
- if (args[1] !== 'prepare')
106
- return false;
107
- const clients = [];
108
- for (let index = 2; index < args.length; index += 1) {
109
- if (args[index] === '--client')
110
- clients.push(args[index + 1] ?? '');
111
- else if (args[index]?.startsWith('--client='))
112
- clients.push(args[index].slice('--client='.length));
113
- }
114
- return clients.length === 1 && clients[0] === 'codex';
115
- }
116
7
  function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
117
8
  process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
118
9
  }
@@ -240,69 +131,9 @@ async function sandboxControl(args) {
240
131
  sandboxFinalizationClient(commandArgs);
241
132
  return;
242
133
  }
243
- if (family === 'task-workflow') {
244
- const command = commandArgs[0];
245
- const taskId = process.env.AGENT_INFRA_TASK_ID;
246
- const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
247
- if (!command || !taskId || !generation) {
248
- 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`);
249
- process.exitCode = 1;
250
- return;
251
- }
252
- let workflow;
253
- try {
254
- const args = withoutValue(commandArgs.slice(1), '--capability-ref');
255
- if (/^\d+$/.test(args[0] ?? '')) {
256
- const resolved = resolveVisibleActiveShortId(args[0]);
257
- if (resolved !== taskId)
258
- throw new Error('SANDBOX_TASK_REF_MISMATCH');
259
- args[0] = resolved;
260
- }
261
- workflow = createTaskWorkflowRequest(command, args, taskId, generation);
262
- }
263
- catch (error) {
264
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'SANDBOX_CONTROL_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } })}\n`);
265
- process.exitCode = 1;
266
- return;
267
- }
268
- let response;
269
- try {
270
- const authority = lifecycleWorkflowAuthoritySelector(commandArgs, workflow);
271
- response = requestSandboxTaskWorkflow({
272
- workflow,
273
- ...(authority ? { authority } : {})
274
- });
275
- }
276
- catch (error) {
277
- if (!(error instanceof SandboxControlClientError))
278
- throw error;
279
- writeClientError(error);
280
- process.exitCode = error.detail.retryable ? 75 : 1;
281
- return;
282
- }
283
- process.stdout.write(response.stdout);
284
- process.stderr.write(response.stderr);
285
- process.exitCode = response.phase === 'rejected' ? response.error?.retryable ? 75 : 1 : response.exitCode ?? 1;
286
- return;
287
- }
288
134
  let response;
289
135
  try {
290
- if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
291
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
292
- const proof = contextPath
293
- ? controllerProofFromContext(verifyCodexSandboxControllerContextWithWarnings(contextPath).context)
294
- : null;
295
- const authority = lifecycleAuthoritySelector(commandArgs);
296
- response = requestSandboxTaskControl({
297
- family,
298
- args: commandArgs,
299
- controllerProof: authority ? null : proof,
300
- ...(authority ? { authority } : {})
301
- });
302
- }
303
- else {
304
- response = requestSandboxControl({ family, args: commandArgs });
305
- }
136
+ response = requestSandboxControl({ family, args: commandArgs });
306
137
  }
307
138
  catch (error) {
308
139
  if (!(error instanceof SandboxControlClientError))
@@ -1,6 +1,6 @@
1
1
  import { parseArtifactCommand, executeArtifactCommand } from '../task/artifact-command.js';
2
2
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
3
- const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact> [--recovery-id <id>]\n\nInspect, initialize, preflight, or finalize a workflow artifact without changing task state.\n`;
3
+ const USAGE = `Usage: agent-infra-internal task-artifact <N | TASK-id> inspect --family <family>\n agent-infra-internal task-artifact <N | TASK-id> init --family <family> --artifact <artifact> [--locale <zh-CN|en>]\n agent-infra-internal task-artifact <N | TASK-id> preflight|finalize-local --family <analysis|plan|code> --artifact <artifact>\n\nInspect, initialize, preflight, or finalize a workflow artifact without changing task state.\n`;
4
4
  function taskArtifact(args = []) {
5
5
  if (!ensureInternalHandlerRoute('task-artifact', args))
6
6
  return;
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
2
2
  import { lifecycleIntentCatalog } from '../task/lifecycle.js';
3
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- 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`;
5
+ 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`;
6
6
  function usageFailure(message) {
7
7
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
8
8
  process.stderr.write(USAGE);
@@ -50,6 +50,7 @@ function internalTaskRoutes() {
50
50
  descriptor('internal', 'platform-comment', 'recover', 'non-task', 'recovery', 'none'),
51
51
  descriptor('internal', 'platform-comment', 'owner', 'task-bound', 'diagnostic'),
52
52
  descriptor('internal', 'platform-comment', 'backfill', 'task-bound', 'remote-write'),
53
+ descriptor('internal', 'platform-comment', 'stage-summary', 'task-bound', 'artifact-write'),
53
54
  descriptor('internal', 'platform-comment', 'sync', 'task-bound', 'remote-write'),
54
55
  descriptor('internal', 'platform-issue', 'inspect', 'task-bound', 'diagnostic'),
55
56
  descriptor('internal', 'platform-issue', 'create', 'task-bound', 'remote-write'),
@@ -228,6 +229,8 @@ export function resolveDelegatedTaskOperation(args) {
228
229
  const [operation, ...rest] = args;
229
230
  if (operation !== 'client' || !rest[0])
230
231
  return null;
232
+ if (rest[0] === 'task-orchestration')
233
+ return null;
231
234
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
232
235
  }
233
236
  const TASK_MARKER_KEYS = [
@@ -351,14 +354,16 @@ export function resolveSandboxControlTransport(env = process.env, options = {})
351
354
  if (!hasCompleteConfig) {
352
355
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
353
356
  }
354
- return { kind: 'broker-client', reasonCode: null };
357
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
355
358
  }
356
359
  if (!hasAnyMarker)
357
360
  return { kind: 'direct-host', reasonCode: null };
358
361
  if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
359
362
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
360
363
  }
361
- return { kind: 'broker-client', reasonCode: null };
364
+ return options.localWorkflow
365
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
366
+ : { kind: 'broker-client', reasonCode: null };
362
367
  }
363
368
  export function hasTaskBoundMarker(env = process.env) {
364
369
  return taskMarkerState(env) === 'task-bound';
@@ -4,7 +4,7 @@ import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task
4
4
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
5
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
6
6
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
7
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--recovery-id <id>] [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
7
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
8
8
  function failUsage(message) {
9
9
  process.stdout.write(`${JSON.stringify({
10
10
  status: 'failed',
@@ -30,7 +30,7 @@ async function taskReview(args = []) {
30
30
  failUsage(error instanceof Error ? error.message : String(error));
31
31
  return;
32
32
  }
33
- const { stage, artifact, recoveryId, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
33
+ const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
34
34
  const operation = args[1];
35
35
  const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
36
36
  if (dryRunConflict) {
@@ -47,8 +47,8 @@ async function taskReview(args = []) {
47
47
  try {
48
48
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
49
49
  let current = operation === 'preflight'
50
- ? preflightReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
- : finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true });
50
+ ? preflightReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
+ : finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
52
52
  if (operation === 'preflight')
53
53
  return current;
54
54
  if (current.status !== 'failed' || !overrideTicket)
@@ -63,7 +63,7 @@ async function taskReview(args = []) {
63
63
  target: overrideTarget, scope: overrideScope
64
64
  }, {
65
65
  effectExecutor: (capability) => {
66
- const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, recoveryId, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
66
+ const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
67
67
  current = retried;
68
68
  return retried.status === 'failed' || retried.status === 'planned'
69
69
  ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
@@ -135,12 +135,6 @@ function snapshotValidation(repoRoot, commit, options) {
135
135
  cleanupError = error;
136
136
  }
137
137
  }
138
- try {
139
- git(repoRoot, ['worktree', 'prune']);
140
- }
141
- catch (error) {
142
- cleanupError ??= error;
143
- }
144
138
  try {
145
139
  fs.rmSync(tempRoot, { recursive: true, force: true });
146
140
  }
@@ -171,6 +171,12 @@ function sanitizeMarkdownDocument(value, options = {}) {
171
171
  function canonicalizeCommentBody(value) {
172
172
  return sanitizeMarkdownDocument(value, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
173
173
  }
174
+ function canonicalizeSummaryBody(value) {
175
+ const sanitized = canonicalizeCommentBody(value);
176
+ return sanitized.ok
177
+ ? ok(`${sanitized.value.replace(/\n+$/, '')}\n`)
178
+ : sanitized;
179
+ }
174
180
  function renderSafeCodeFence(value, language = '', reservedMarkers = [CONTROL_MARKER_PATTERN]) {
175
181
  const content = markerSafe(normalized(value), reservedMarkers);
176
182
  const longestRun = Math.max(0, ...(content.match(/`+/g) || []).map((run) => run.length));
@@ -225,5 +231,5 @@ function splitDocumentPlaceholder(value, placeholder) {
225
231
  }
226
232
  return ok({ prefix: source.slice(0, position), suffix: source.slice(position + placeholder.length) });
227
233
  }
228
- export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
234
+ export { CONTROL_MARKER_PATTERN, escapeHtmlAttribute, escapeHtmlText, escapeMarkdownLiteral, fenceRanges, renderSafeCodeFence, canonicalizeCommentBody, canonicalizeSummaryBody, sanitizeMarkdownDocument, splitDocumentPlaceholder };
229
235
  //# sourceMappingURL=comment-safety.js.map
@@ -278,13 +278,14 @@ function issueSnapshot(value, graph) {
278
278
  displayUrl: value.url
279
279
  };
280
280
  }
281
- function commentSnapshot(value) {
281
+ function commentSnapshot(value, createdSequence = null) {
282
282
  return {
283
283
  id: String(value.id),
284
284
  author: value.user?.login ? { id: String(value.user.login), name: String(value.user.login) } : null,
285
285
  body: String(value.body || ''),
286
286
  createdAt: String(value.created_at || ''),
287
- updatedAt: String(value.updated_at || value.created_at || '')
287
+ updatedAt: String(value.updated_at || value.created_at || ''),
288
+ createdSequence
288
289
  };
289
290
  }
290
291
  function createReceipt(remoteId) {
@@ -555,7 +556,16 @@ function createGitHubOperations(client) {
555
556
  if (!response.ok)
556
557
  return response;
557
558
  const values = Array.isArray(response.value) ? response.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) : [];
558
- return { ok: true, value: values.filter((entry) => entry && entry.id !== undefined).map(commentSnapshot) };
559
+ const comments = values.filter((entry) => entry && typeof entry === 'object');
560
+ let previous = 0;
561
+ const ordered = comments.length === values.length && comments.every((entry) => {
562
+ const id = Number(entry.id);
563
+ if (!Number.isSafeInteger(id) || id <= previous)
564
+ return false;
565
+ previous = id;
566
+ return true;
567
+ });
568
+ return { ok: true, value: comments.map((comment) => commentSnapshot(comment, ordered ? Number(comment.id) : null)) };
559
569
  },
560
570
  async write({ context, parent, body, existingComment }) {
561
571
  const number = resourceIdentityNumber(parent);
@@ -29,8 +29,6 @@ const ARTIFACT_TITLES = {
29
29
  'review-plan': '技术方案审查',
30
30
  code: '实现报告',
31
31
  'review-code': '代码审查',
32
- 'manual-validation': '人工验证报告',
33
- 'validation-run': '验证运行证据',
34
32
  'pr-review': 'PR 审查报告'
35
33
  };
36
34
  function normalizeCommentContent(content) {
@@ -100,7 +98,7 @@ function taskCommentLanguage(repoRoot) {
100
98
  }
101
99
  function artifactIdentity(artifact) {
102
100
  const stem = path.basename(artifact, '.md');
103
- const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
101
+ const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
104
102
  if (!match)
105
103
  throw new Error(`unsupported artifact '${artifact}'`);
106
104
  const base = ARTIFACT_TITLES[match[1]];
@@ -373,6 +371,38 @@ function relatedComments(comments, prefix) {
373
371
  return first === base || (first.startsWith(chunkNamespace) && first.endsWith(' -->'));
374
372
  });
375
373
  }
374
+ function taskManagedComment(comment, taskId) {
375
+ const marker = normalizeCommentContent(comment.body).split('\n', 1)[0] || '';
376
+ return marker === MARKERS.task(taskId)
377
+ || marker === MARKERS.summary(taskId)
378
+ || marker.startsWith(`<!-- sync-issue:${taskId}:`);
379
+ }
380
+ function summaryPosition(comments, taskId) {
381
+ const managed = comments.filter((comment) => taskManagedComment(comment, taskId));
382
+ const summary = managed.filter((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] === MARKERS.summary(taskId));
383
+ if (summary.length !== 1)
384
+ return { ok: false, known: false, summary: null };
385
+ if (managed.some((comment) => !Number.isSafeInteger(comment.createdSequence) || Number(comment.createdSequence) < 1)) {
386
+ return { ok: false, known: false, summary: summary[0] };
387
+ }
388
+ const sequence = summary[0].createdSequence;
389
+ return { ok: managed.every((comment) => sequence >= comment.createdSequence), known: true, summary: summary[0] };
390
+ }
391
+ async function listedComments(provider, loaded, parent) {
392
+ return provider.comments?.list
393
+ ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response) => response.ok
394
+ ? {
395
+ ok: true,
396
+ value: response.value.map((comment) => ({
397
+ id: providerCommentId(comment.id, provider),
398
+ body: comment.body,
399
+ user: comment.author?.name ? { login: comment.author.name } : undefined,
400
+ createdSequence: comment.createdSequence
401
+ }))
402
+ }
403
+ : response)
404
+ : unsupportedProviderOperation(provider, 'comments.list');
405
+ }
376
406
  function validateRelatedMarkerSet(comments, prefix) {
377
407
  const markers = comments.map((comment) => normalizeCommentContent(comment.body).split('\n', 1)[0] || '');
378
408
  if (new Set(markers).size !== markers.length)
@@ -442,18 +472,7 @@ async function syncPlatformComment(taskRef, options) {
442
472
  if (!hasResolvedPlatformContext(context) || !loaded.ok)
443
473
  return context;
444
474
  const issue = resourceIdentityNumber(issueIdentityFromTask);
445
- const listed = loaded.value.provider.comments?.list
446
- ? await loaded.value.provider.comments.list({
447
- context: providerOperationContext(loaded.value),
448
- parent: issueIdentityFromTask
449
- }).then((response) => response.ok
450
- ? { ok: true, value: response.value.map((comment) => ({
451
- id: providerCommentId(comment.id, loaded.value.provider),
452
- body: comment.body,
453
- user: comment.author?.name ? { login: comment.author.name } : undefined
454
- })) }
455
- : response)
456
- : unsupportedProviderOperation(loaded.value.provider, 'comments.list');
475
+ const listed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
457
476
  if (!listed.ok) {
458
477
  return platformResult(listed.error.retryable ? 'blocked' : 'failed', {
459
478
  ...contextFields(context), resource: { kind: 'issue', number: issue }, error: listed.error
@@ -555,6 +574,65 @@ async function syncPlatformComment(taskRef, options) {
555
574
  }
556
575
  operations.push({ name: `comment:${comment.id}`, status: 'applied', reasonCode: 'STALE_CHUNK_DELETED' });
557
576
  }
577
+ if (options.kind === 'summary') {
578
+ const refreshed = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
579
+ if (!refreshed.ok) {
580
+ return platformResult(refreshed.error.retryable ? 'blocked' : 'failed', {
581
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: refreshed.error
582
+ });
583
+ }
584
+ const position = summaryPosition(refreshed.value, resolved.taskId);
585
+ if (!position.ok && !position.known) {
586
+ return platformResult('blocked', {
587
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
588
+ error: { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment ordering cannot be proven from the provider snapshot', retryable: true }
589
+ });
590
+ }
591
+ if (!position.ok && position.summary) {
592
+ const owner = position.summary.user?.login;
593
+ if (!options.summaryAuthorization || options.summaryAuthorization.sha256 !== createHash('sha256').update(options.body ?? '').digest('hex')
594
+ || !owner || owner !== context.platform.currentUser) {
595
+ return platformResult('failed', {
596
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations,
597
+ error: { code: 'SUMMARY_REPOSITION_UNAUTHORIZED', message: 'summary reposition requires current finalization authorization and an owned durable summary', retryable: false }
598
+ });
599
+ }
600
+ const deleted = loaded.value.provider.comments?.delete
601
+ ? await loaded.value.provider.comments.delete({
602
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask,
603
+ comment: { kind: 'id', value: String(position.summary.id) },
604
+ mutation: { idempotencyKey: `summary-reposition-delete:${resolved.taskId}:${position.summary.id}` }
605
+ })
606
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.delete');
607
+ if (!deleted.ok) {
608
+ return platformResult(deleted.error.retryable ? 'blocked' : 'failed', {
609
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: deleted.error
610
+ });
611
+ }
612
+ const written = loaded.value.provider.comments?.write
613
+ ? await loaded.value.provider.comments.write({
614
+ context: providerOperationContext(loaded.value), parent: issueIdentityFromTask, body: desired[0].body,
615
+ mutation: { idempotencyKey: `summary-reposition-write:${resolved.taskId}` }
616
+ })
617
+ : unsupportedProviderOperation(loaded.value.provider, 'comments.write');
618
+ if (!written.ok) {
619
+ return platformResult(written.error.retryable ? 'blocked' : 'failed', {
620
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error: written.error
621
+ });
622
+ }
623
+ ids.splice(0, ids.length, providerCommentId(written.value.remoteId, loaded.value.provider));
624
+ operations.push({ name: `comment:${desired[0].marker}`, status: 'applied', reasonCode: 'SUMMARY_REPOSITIONED' });
625
+ const reconciled = await listedComments(loaded.value.provider, loaded.value, issueIdentityFromTask);
626
+ if (!reconciled.ok || !summaryPosition(reconciled.value, resolved.taskId).ok) {
627
+ const error = reconciled.ok
628
+ ? { code: 'SUMMARY_POSITION_UNVERIFIED', message: 'Summary comment is not provably last among task-managed comments', retryable: true }
629
+ : reconciled.error;
630
+ return platformResult(error.retryable ? 'blocked' : 'failed', {
631
+ ...contextFields(context), resource: { kind: 'issue', number: issue }, operations, error
632
+ });
633
+ }
634
+ }
635
+ }
558
636
  const changed = operations.some((operation) => operation.status === 'applied');
559
637
  const result = platformResult(changed ? 'applied' : 'no-op', {
560
638
  ...contextFields(context),
@@ -13,7 +13,7 @@ import { platformResult } from './types.js';
13
13
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
14
14
  import { resourceIdentityEquals } from './resource-identity.js';
15
15
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
16
- import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
16
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
17
17
  import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
18
18
  function summaryMarker(taskId) {
19
19
  return `<!-- sync-pr:${taskId}:summary -->`;
@@ -515,6 +515,7 @@ async function syncPullRequestSummary(taskRef, options) {
515
515
  if (!initial.ok)
516
516
  return fail(initial.status, context, initial.error);
517
517
  const manual = options.manualValidation;
518
+ let manualEvidenceDigest = null;
518
519
  const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
519
520
  if (hasFinalManualValidation && (!manual || manual.phase !== 'final'))
520
521
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
@@ -538,6 +539,7 @@ async function syncPullRequestSummary(taskRef, options) {
538
539
  if (!completion.ok)
539
540
  return fail('failed', context, platformError(completion.error), prNumber);
540
541
  const { receipt, transaction } = completion.value;
542
+ manualEvidenceDigest = receipt.evidenceDigest;
541
543
  if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended)
542
544
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
543
545
  if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt))
@@ -581,7 +583,10 @@ async function syncPullRequestSummary(taskRef, options) {
581
583
  const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
582
584
  if (!replaced.ok)
583
585
  return fail('failed', context, platformError(replaced.error));
584
- const desired = buildPullRequestSummary(resolved.taskId, replaced.value, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
586
+ const renderedBody = manual?.phase === 'final'
587
+ ? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
588
+ : replaced.value;
589
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
585
590
  if (!isSafeSummaryEnvelope(desired, resolved.taskId))
586
591
  return fail('failed', context, {
587
592
  code: 'PR_SUMMARY_RENDER_INVALID',
@@ -87,6 +87,8 @@ type RemoteCommentSnapshot = {
87
87
  body: string;
88
88
  createdAt: string;
89
89
  updatedAt: string;
90
+ /** Strict creation order when the provider can prove it for this list snapshot. */
91
+ createdSequence: number | null;
90
92
  };
91
93
  type ChangeRequestSnapshot = {
92
94
  id: string;
@@ -189,13 +189,18 @@ function issueType(value, label) {
189
189
  }
190
190
  function validateComment(value) {
191
191
  const item = record(value, 'comment');
192
- exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt'], 'comment');
192
+ exactKeys(item, ['id', 'author', 'body', 'createdAt', 'updatedAt', 'createdSequence'], 'comment');
193
193
  return {
194
194
  id: stringValue(item.id, 'comment.id'),
195
195
  author: author(item.author, 'comment.author'),
196
196
  body: stringValue(item.body, 'comment.body', true),
197
197
  createdAt: utcTimestamp(item.createdAt, 'comment.createdAt'),
198
- updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt')
198
+ updatedAt: utcTimestamp(item.updatedAt, 'comment.updatedAt'),
199
+ createdSequence: item.createdSequence === null ? null : (() => {
200
+ if (!Number.isSafeInteger(item.createdSequence) || Number(item.createdSequence) < 1)
201
+ throw new Error('comment.createdSequence must be a positive safe integer or null');
202
+ return Number(item.createdSequence);
203
+ })()
199
204
  };
200
205
  }
201
206
  function validateChangeRequest(value, expectedKind, requireIdentity = false) {
@@ -11,7 +11,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.js";
11
11
  import { readPrDeliveryFact } from "../task/pr-delivery-fact.js";
12
12
  import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.js";
13
13
  import { taskIssueIdentity } from "./task-identities.js";
14
- import { renderTaskCommentResult } from "./issue-comments.js";
14
+ import { MARKERS, renderTaskCommentResult } from "./issue-comments.js";
15
15
  import { CONTROL_MARKER_PATTERN, canonicalizeCommentBody, renderSafeCodeFence, sanitizeMarkdownDocument } from "./comment-safety.js";
16
16
  const CHECK_TYPE = "platform-sync";
17
17
  const VERSION_LINE_REGEX = /^[0-9]+\.[0-9]+\.x$/;
@@ -258,7 +258,9 @@ async function fetchRemoteData(context, shared) {
258
258
  }
259
259
  return {
260
260
  issue,
261
- comments: facts.value.comments.map((comment) => ({ id: comment.id, body: comment.body })),
261
+ comments: facts.value.comments.map((comment) => ({
262
+ id: comment.id, body: comment.body, createdSequence: comment.createdSequence
263
+ })),
262
264
  prComments,
263
265
  prLabels: changeRequest?.labels || null,
264
266
  issueType: issueSnapshot
@@ -320,6 +322,21 @@ function checkCommentMarker(context, remoteData, shared) {
320
322
  }
321
323
  const comment = findCommentByMarker(remoteData.comments, context.marker);
322
324
  if (comment) {
325
+ if (context.marker === MARKERS.summary(context.task.metadata.id)) {
326
+ const managed = remoteData.comments.filter((candidate) => {
327
+ const first = String(candidate.body || '').replace(/\r\n/g, '\n').split('\n', 1)[0] || '';
328
+ return first === MARKERS.task(context.task.metadata.id)
329
+ || first === MARKERS.summary(context.task.metadata.id)
330
+ || first.startsWith(`<!-- sync-issue:${context.task.metadata.id}:`);
331
+ });
332
+ if (managed.some((candidate) => !Number.isSafeInteger(candidate.createdSequence) || candidate.createdSequence < 1)) {
333
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment ordering is not provable`, "check_failed");
334
+ }
335
+ const summarySequence = comment.createdSequence;
336
+ if (!managed.every((candidate) => summarySequence >= candidate.createdSequence)) {
337
+ return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment is not last among task-managed comments`, "check_failed");
338
+ }
339
+ }
323
340
  return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
324
341
  }
325
342
  return shared.failResult(CHECK_TYPE, `Expected comment marker '${context.marker}' not found on Issue #${context.issueNumber}`, "check_failed");