@fitlab-ai/agent-infra 0.11.2 → 0.11.4

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 (162) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +7 -2
  4. package/dist/lib/agent-clients/adapter.js +3 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
  8. package/dist/lib/agent-clients/adapters/codex.js +10 -0
  9. package/dist/lib/internal/cli-route-inventory.js +1 -1
  10. package/dist/lib/internal/manual-validation.js +22 -27
  11. package/dist/lib/internal/sandbox-control.js +1 -43
  12. package/dist/lib/internal/task-artifact.js +1 -1
  13. package/dist/lib/internal/task-finalization.js +41 -1
  14. package/dist/lib/internal/task-ledger.js +13 -5
  15. package/dist/lib/internal/task-lifecycle.js +1 -1
  16. package/dist/lib/internal/task-operation-registry.js +7 -2
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/issue-comments.js +2 -31
  19. package/dist/lib/platform/pr-summary.js +7 -2
  20. package/dist/lib/sandbox/control/client.js +3 -44
  21. package/dist/lib/sandbox/control/executor.js +14 -79
  22. package/dist/lib/sandbox/control/protocol.js +5 -64
  23. package/dist/lib/sandbox/control/server.js +49 -343
  24. package/dist/lib/sandbox/control/state.js +1 -7
  25. package/dist/lib/sandbox/control/task-view.js +1 -2
  26. package/dist/lib/sandbox/task-cleanup.js +13 -20
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +1 -26
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +1 -0
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +37 -38
  36. package/dist/lib/task/control-recovery.js +3 -42
  37. package/dist/lib/task/decision-intents.js +12 -0
  38. package/dist/lib/task/delegation-receipts.js +2 -2
  39. package/dist/lib/task/delivery.js +1 -43
  40. package/dist/lib/task/events.js +80 -64
  41. package/dist/lib/task/finalization-artifacts.js +1 -3
  42. package/dist/lib/task/finalization-state.js +1 -1
  43. package/dist/lib/task/finalization.js +319 -234
  44. package/dist/lib/task/ledger-intents.js +157 -15
  45. package/dist/lib/task/lifecycle-path.js +75 -0
  46. package/dist/lib/task/lifecycle.js +11 -1
  47. package/dist/lib/task/local-artifact-finalization.js +1 -20
  48. package/dist/lib/task/manual-validation-receipt.js +8 -3
  49. package/dist/lib/task/orchestration.js +29 -13
  50. package/dist/lib/task/review-finalization.js +3 -16
  51. package/dist/lib/task/rework-intent.js +70 -4
  52. package/dist/lib/task/verification-engine.js +20 -30
  53. package/dist/lib/task/verification.js +1 -0
  54. package/dist/lib/task/workflow-warning-intents.js +21 -6
  55. package/dist/lib/task/workspace-snapshot.js +7 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/agent-clients/adapter.ts +46 -0
  58. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
  60. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
  61. package/lib/agent-clients/adapters/codex.ts +10 -0
  62. package/lib/internal/cli-route-inventory.ts +1 -1
  63. package/lib/internal/manual-validation.ts +22 -26
  64. package/lib/internal/sandbox-control.ts +0 -42
  65. package/lib/internal/task-artifact.ts +1 -1
  66. package/lib/internal/task-finalization.ts +40 -1
  67. package/lib/internal/task-ledger.ts +12 -5
  68. package/lib/internal/task-lifecycle.ts +2 -2
  69. package/lib/internal/task-operation-registry.ts +8 -4
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/issue-comments.ts +1 -35
  72. package/lib/platform/pr-summary.ts +10 -2
  73. package/lib/sandbox/control/client.ts +5 -68
  74. package/lib/sandbox/control/executor.ts +14 -81
  75. package/lib/sandbox/control/protocol.ts +7 -83
  76. package/lib/sandbox/control/server.ts +47 -362
  77. package/lib/sandbox/control/state.ts +1 -9
  78. package/lib/sandbox/control/task-view.ts +1 -2
  79. package/lib/sandbox/task-cleanup.ts +12 -20
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +1 -26
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +1 -0
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +41 -48
  89. package/lib/task/control-recovery.ts +3 -43
  90. package/lib/task/decision-intents.ts +9 -0
  91. package/lib/task/delegation-receipts.ts +2 -2
  92. package/lib/task/delivery.ts +1 -40
  93. package/lib/task/events.ts +82 -64
  94. package/lib/task/finalization-artifacts.ts +1 -3
  95. package/lib/task/finalization-state.ts +1 -1
  96. package/lib/task/finalization.ts +310 -216
  97. package/lib/task/ledger-intents.ts +155 -15
  98. package/lib/task/lifecycle-path.ts +98 -0
  99. package/lib/task/lifecycle.ts +15 -2
  100. package/lib/task/local-artifact-finalization.ts +2 -25
  101. package/lib/task/manual-validation-receipt.ts +8 -2
  102. package/lib/task/orchestration.ts +56 -27
  103. package/lib/task/review-finalization.ts +3 -16
  104. package/lib/task/rework-intent.ts +75 -5
  105. package/lib/task/verification-engine.ts +19 -30
  106. package/lib/task/verification.ts +2 -1
  107. package/lib/task/workflow-warning-intents.ts +18 -6
  108. package/lib/task/workspace-snapshot.ts +3 -2
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
  111. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
  112. package/templates/.agents/rules/review-handshake.en.md +4 -4
  113. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  114. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
  115. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
  116. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  117. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  118. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  119. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  120. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  121. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  122. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  123. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  124. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  125. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
  128. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  129. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  130. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  131. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  132. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  135. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  136. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
  137. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
  138. package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
  139. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
  140. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  141. package/templates/.agents/templates/task.en.md +2 -2
  142. package/templates/.agents/templates/task.zh-CN.md +2 -2
  143. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  144. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  145. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  146. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  147. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  148. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  149. package/dist/lib/sandbox/control/finalization-response.js +0 -35
  150. package/dist/lib/sandbox/control/task-workflow.js +0 -74
  151. package/dist/lib/sandbox/control/workflow-executor.js +0 -142
  152. package/dist/lib/task/lifecycle-recovery.js +0 -549
  153. package/dist/lib/task/recovery-warning.js +0 -19
  154. package/dist/lib/task/workflow-command.js +0 -21
  155. package/dist/lib/task/workflow-dispatch.js +0 -213
  156. package/lib/sandbox/control/finalization-response.ts +0 -37
  157. package/lib/sandbox/control/task-workflow.ts +0 -95
  158. package/lib/sandbox/control/workflow-executor.ts +0 -156
  159. package/lib/task/lifecycle-recovery.ts +0 -691
  160. package/lib/task/recovery-warning.ts +0 -21
  161. package/lib/task/workflow-command.ts +0 -22
  162. package/lib/task/workflow-dispatch.ts +0 -214
@@ -3,8 +3,11 @@ import {
3
3
  dispatchTaskControlOperation,
4
4
  parseTaskControlOperation
5
5
  } from '../task/control-authority.ts';
6
- import { applyTaskFinalization } from '../task/finalization.ts';
6
+ import { applyTaskFinalization, prepareTaskFinalization } from '../task/finalization.ts';
7
7
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
8
+ import { verifyTaskEvent } from '../task/verification.ts';
9
+ import { resolveSandboxControlTransport } from './task-operation-registry.ts';
10
+ import { requestSandboxTaskFinalization, SandboxControlClientError } from '../sandbox/control/client.ts';
8
11
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
9
12
 
10
13
  const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
@@ -73,10 +76,46 @@ async function taskFinalization(args: string[] = []): Promise<void> {
73
76
  ...operation,
74
77
  request: { ...operation.request, taskRef: resolved.taskId }
75
78
  };
79
+ const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
80
+ if (transport.kind === 'sandbox-local') {
81
+ const prepared = await prepareTaskFinalization(boundOperation.request, {
82
+ repoRoot,
83
+ preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options)
84
+ });
85
+ if (prepared.status !== 'prepared') {
86
+ process.stdout.write(envelope(prepared.status, prepared.changed, true, prepared, prepared.error));
87
+ process.exitCode = exitCode(prepared.status);
88
+ return;
89
+ }
90
+ try {
91
+ const response = requestSandboxTaskFinalization({ agent: boundOperation.request.agent });
92
+ process.stdout.write(response.stdout);
93
+ process.stderr.write(response.stderr);
94
+ process.exitCode = response.exitCode ?? 1;
95
+ return;
96
+ } catch (error) {
97
+ const detail = error instanceof SandboxControlClientError
98
+ ? error.detail
99
+ : { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
100
+ process.stdout.write(envelope(detail.retryable ? 'blocked' : 'failed', false, false, null, detail));
101
+ process.exitCode = detail.retryable ? 2 : 1;
102
+ return;
103
+ }
104
+ }
76
105
  const result = await dispatchTaskControlOperation(
77
106
  createDirectHostExecutionContext({ repoRoot }),
78
107
  boundOperation
79
108
  );
109
+ if (result.status === 'prepared') {
110
+ const detail = {
111
+ code: 'TASK_FINALIZATION_DIRECT_COMMIT_REQUIRED',
112
+ message: 'direct finalization prepared external work without committing the lifecycle transition',
113
+ retryable: false
114
+ };
115
+ process.stdout.write(envelope('failed', result.changed, true, result, detail));
116
+ process.exitCode = 1;
117
+ return;
118
+ }
80
119
  process.stdout.write(envelope(result.status, result.changed, true, result, result.error));
81
120
  process.exitCode = exitCode(result.status);
82
121
  }
@@ -8,17 +8,18 @@ import { resolveTaskRef } from '../task/resolve-ref.ts';
8
8
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
9
9
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
10
10
 
11
- const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, rework-intent-upsert, stage-status\n`;
11
+ const USAGE = `Usage: agent-infra-internal task-ledger <task-ref> <intent> [intent flags] [--dry-run]\n\nIntents: finding-upsert, finding-respond, finding-review, decision-next-id, decision-upsert, rework-intent-upsert, rework-intent-rebuild, stage-status\n`;
12
12
  const FLAGS: Record<string, string> = {
13
13
  '--stage': 'stage', '--review-artifact': 'reviewArtifact', '--ordinal': 'ordinal',
14
14
  '--severity': 'severity', '--evidence': 'evidence', '--id': 'id', '--round': 'round',
15
15
  '--status': 'status', '--artifact': 'artifact', '--needs-implementation': 'needsImplementation',
16
16
  '--intent-id': 'intentId', '--finding-id': 'findingId', '--source-artifact': 'sourceArtifact',
17
- '--source-sha256': 'sourceSha256', '--target': 'target',
17
+ '--source-sha256': 'sourceSha256', '--classification': 'classification',
18
18
  '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
19
19
  };
20
20
  const NUMERIC = new Set(['ordinal', 'round']);
21
21
  const BOOLEAN = new Set(['needsImplementation']);
22
+ const REWORK_CLASSIFICATIONS = new Set(['scope-or-requirement', 'design', 'implementation', 'human-decision', 'insufficient-evidence']);
22
23
 
23
24
  function usageFailure(message: string): void {
24
25
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LEDGER_PAYLOAD_INVALID', message } })}\n`);
@@ -38,6 +39,7 @@ async function taskLedger(args: string[] = []): Promise<void> {
38
39
  internalHandlerRoute('task-ledger', 'decision-next-id', kind),
39
40
  internalHandlerRoute('task-ledger', 'decision-upsert', kind),
40
41
  internalHandlerRoute('task-ledger', 'rework-intent-upsert', kind),
42
+ internalHandlerRoute('task-ledger', 'rework-intent-rebuild', kind),
41
43
  internalHandlerRoute('task-ledger', 'stage-status', kind)
42
44
  ].some(Boolean)) {
43
45
  usageFailure(`unknown ledger intent '${kind}'`); return;
@@ -67,14 +69,16 @@ async function taskLedger(args: string[] = []): Promise<void> {
67
69
  'finding-review': ['id', 'status', 'evidence'],
68
70
  'decision-next-id': [],
69
71
  'decision-upsert': ['id', 'stage', 'artifact'],
70
- 'rework-intent-upsert': ['intentId', 'findingId', 'sourceArtifact', 'sourceSha256', 'target'],
72
+ 'rework-intent-upsert': ['intentId', 'findingId', 'sourceArtifact', 'sourceSha256', 'classification'],
73
+ 'rework-intent-rebuild': [],
71
74
  'stage-status': ['stage']
72
75
  };
73
76
  const optional: Record<string, string[]> = {
74
77
  'finding-review': ['needsImplementation'],
75
- 'decision-upsert': ['needsImplementation']
78
+ 'decision-upsert': ['needsImplementation'],
79
+ 'rework-intent-rebuild': ['findingId', 'sourceArtifact', 'sourceSha256', 'classification']
76
80
  };
77
- const overrideFields = kind === 'stage-status' || kind === 'decision-next-id' || kind === 'rework-intent-upsert'
81
+ const overrideFields = kind === 'stage-status' || kind === 'decision-next-id' || kind === 'rework-intent-upsert' || kind === 'rework-intent-rebuild'
78
82
  ? []
79
83
  : ['overrideTicket', 'overrideTarget', 'overrideScope'];
80
84
  const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind]!, ...(optional[kind] ?? []), ...overrideFields]);
@@ -84,6 +88,9 @@ async function taskLedger(args: string[] = []): Promise<void> {
84
88
  if (missing) { usageFailure(`${kind} requires '${missing}'`); return; }
85
89
  const dryRunConflict = overrideDryRunConflict(values);
86
90
  if (dryRunConflict) { usageFailure(dryRunConflict.message); return; }
91
+ if (values.classification !== undefined && !REWORK_CLASSIFICATIONS.has(String(values.classification))) {
92
+ usageFailure(`invalid rework classification '${values.classification}'`); return;
93
+ }
87
94
  if (kind === 'stage-status') {
88
95
  if (!isReviewStage(String(values.stage))) { usageFailure(`stage-status requires a valid review stage`); return; }
89
96
  const resolved = resolveTaskRef(taskRef);
@@ -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
 
@@ -125,6 +125,7 @@ function internalTaskRoutes(): TaskOperationDescriptor[] {
125
125
  descriptor('internal', 'task-ledger', 'finding-review', 'task-bound', 'progress'),
126
126
  descriptor('internal', 'task-ledger', 'decision-upsert', 'task-bound', 'progress'),
127
127
  descriptor('internal', 'task-ledger', 'rework-intent-upsert', 'task-bound', 'progress'),
128
+ descriptor('internal', 'task-ledger', 'rework-intent-rebuild', 'task-bound', 'progress'),
128
129
  descriptor('internal', 'task-warning', 'list', 'task-bound', 'diagnostic'),
129
130
  descriptor('internal', 'task-warning', 'add', 'task-bound', 'progress'),
130
131
  descriptor('internal', 'task-warning', 'set-status', 'task-bound', 'progress'),
@@ -277,6 +278,7 @@ export function resolveTaskOperation(
277
278
  export function resolveDelegatedTaskOperation(args: readonly string[]): TaskOperationDescriptor | null {
278
279
  const [operation, ...rest] = args;
279
280
  if (operation !== 'client' || !rest[0]) return null;
281
+ if (rest[0] === 'task-orchestration') return null;
280
282
  return resolveTaskOperation('internal', rest[0], rest.slice(1));
281
283
  }
282
284
 
@@ -340,7 +342,7 @@ function taskMarkerState(env: NodeJS.ProcessEnv): TaskMarkerState {
340
342
 
341
343
  export function resolveSandboxControlTransport(
342
344
  env: NodeJS.ProcessEnv = process.env,
343
- options: Readonly<{ statusMountPath?: string }> = {}
345
+ options: Readonly<{ statusMountPath?: string; localWorkflow?: boolean }> = {}
344
346
  ): SandboxControlTransportDecision {
345
347
  const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
346
348
  || Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
@@ -407,13 +409,15 @@ export function resolveSandboxControlTransport(
407
409
  if (!hasCompleteConfig) {
408
410
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
409
411
  }
410
- return { kind: 'broker-client', reasonCode: null };
412
+ return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
411
413
  }
412
414
  if (!hasAnyMarker) return { kind: 'direct-host', reasonCode: null };
413
415
  if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
414
416
  return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
415
417
  }
416
- return { kind: 'broker-client', reasonCode: null };
418
+ return options.localWorkflow
419
+ ? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
420
+ : { kind: 'broker-client', reasonCode: null };
417
421
  }
418
422
 
419
423
  export function hasTaskBoundMarker(env: NodeJS.ProcessEnv = process.env): boolean {
@@ -19,6 +19,24 @@ type CompletionBackfillResult = PlatformResult & {
19
19
  artifacts: Array<{ artifact: string; status: PlatformResult['status'] }>;
20
20
  warnings: Array<{ id: string; status: string }>;
21
21
  };
22
+ type CompletionBackfillEligibility = Readonly<{
23
+ status: 'ready' | 'failed';
24
+ eligible: boolean;
25
+ error: { code: string; message: string; retryable: boolean } | null;
26
+ }>;
27
+ type BackfillArtifactReference = Readonly<
28
+ | { status: 'ignored'; artifact: null; error: null }
29
+ | { status: 'ready'; artifact: string; error: null }
30
+ | { status: 'failed'; artifact: null; error: { code: string; message: string; retryable: false } }
31
+ >;
32
+ type BackfillWarningMatch = Readonly<{
33
+ warning: ReturnType<typeof getOpenWorkflowWarnings>[number];
34
+ artifact: string;
35
+ }>;
36
+ type BackfillWarningMatches = Readonly<
37
+ | { status: 'ready'; matches: BackfillWarningMatch[]; error: null }
38
+ | { status: 'failed'; matches: []; error: { code: string; message: string; retryable: false } }
39
+ >;
22
40
 
23
41
  function result(
24
42
  base: PlatformResult,
@@ -28,18 +46,65 @@ function result(
28
46
  return { ...base, artifacts, warnings };
29
47
  }
30
48
 
31
- function excludedPrReview(taskDir: string, message: string): string | null {
32
- const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])(pr-review(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
49
+ function referencedBackfillArtifact(taskDir: string, message: string): BackfillArtifactReference {
50
+ const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])([a-z][a-z-]*(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
33
51
  .map((match) => match[1]!);
34
- if (names.length !== 1) return null;
52
+ if (names.length !== 1) return { status: 'ignored', artifact: null, error: null };
35
53
  const parsed = parseArtifactName(names[0]!);
36
- if (!parsed || parsed.family !== 'pr-review') return null;
54
+ if (!parsed || (parsed.family !== 'pr-review' && !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family as typeof COMPLETION_BACKFILL_FAMILIES[number]))) {
55
+ return { status: 'ignored', artifact: null, error: null };
56
+ }
37
57
  const artifactPath = path.join(taskDir, parsed.name);
38
58
  try {
39
59
  const stat = fs.lstatSync(artifactPath);
40
- return stat.isFile() && !stat.isSymbolicLink() ? parsed.name : null;
41
- } catch {
42
- return null;
60
+ if (stat.isSymbolicLink()) return {
61
+ status: 'failed', artifact: null,
62
+ error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `SYMBOLIC_LINK: symbolic links are not workflow artifacts: ${parsed.name}`, retryable: false }
63
+ };
64
+ if (!stat.isFile()) return {
65
+ status: 'failed', artifact: null,
66
+ error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `NON_REGULAR_FILE: artifact is not a regular file: ${parsed.name}`, retryable: false }
67
+ };
68
+ fs.accessSync(artifactPath, fs.constants.R_OK);
69
+ return { status: 'ready', artifact: parsed.name, error: null };
70
+ } catch (error) {
71
+ return {
72
+ status: 'failed', artifact: null,
73
+ error: { code: 'ARTIFACT_READ_FAILED', message: `Unable to inspect referenced artifact '${parsed.name}': ${error instanceof Error ? error.message : String(error)}`, retryable: false }
74
+ };
75
+ }
76
+ }
77
+
78
+ function matchingBackfillWarnings(taskDir: string, content: string): BackfillWarningMatches {
79
+ const matches: BackfillWarningMatch[] = [];
80
+ for (const warning of getOpenWorkflowWarnings(content)) {
81
+ if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact') continue;
82
+ const reference = referencedBackfillArtifact(taskDir, warning.message);
83
+ if (reference.status === 'failed') return { status: 'failed', matches: [], error: reference.error };
84
+ if (reference.status === 'ready') matches.push({ warning, artifact: reference.artifact });
85
+ }
86
+ return { status: 'ready', matches, error: null };
87
+ }
88
+
89
+ function inspectCompletionBackfillEligibility(
90
+ taskRef: string,
91
+ options: Pick<CompletionBackfillOptions, 'cwd'> = {}
92
+ ): CompletionBackfillEligibility {
93
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
94
+ if (!resolved.ok) return {
95
+ status: 'failed', eligible: false,
96
+ error: { code: resolved.code, message: resolved.message, retryable: false }
97
+ };
98
+ try {
99
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
100
+ const matching = matchingBackfillWarnings(resolved.taskDir, content);
101
+ if (matching.status === 'failed') return { status: 'failed', eligible: false, error: matching.error };
102
+ return { status: 'ready', eligible: matching.matches.length > 0, error: null };
103
+ } catch (error) {
104
+ return {
105
+ status: 'failed', eligible: false,
106
+ error: { code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
107
+ };
43
108
  }
44
109
  }
45
110
 
@@ -60,6 +125,8 @@ async function backfillCompletionComments(
60
125
  }
61
126
  }), [], []);
62
127
  const initialContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
128
+ const matching = matchingBackfillWarnings(resolved.taskDir, initialContent);
129
+ if (matching.status === 'failed') return result(platformResult('failed', { error: matching.error }), [], []);
63
130
  const issueIdentity = taskIssueIdentity(parseTaskFrontmatter(initialContent));
64
131
  if (!issueIdentity) return result(platformResult('no-op', {
65
132
  error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no valid platform issue identity', retryable: false }
@@ -92,17 +159,12 @@ async function backfillCompletionComments(
92
159
  }), artifacts, []);
93
160
  }
94
161
 
95
- const matching = getOpenWorkflowWarnings(initialContent).flatMap((warning) => {
96
- if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact') return [];
97
- const excluded = excludedPrReview(resolved.taskDir, warning.message);
98
- return excluded ? [{ warning, excluded }] : [];
99
- });
100
162
  const warnings: CompletionBackfillResult['warnings'] = [];
101
163
  let metadata: ReturnType<typeof captureTaskWriteMetadata> | null = null;
102
- for (const { warning, excluded } of matching) {
164
+ for (const { warning, artifact } of matching.matches) {
103
165
  metadata ??= captureTaskWriteMetadata();
104
166
  const resolution = [
105
- `excluded=${excluded}`,
167
+ `artifact=${artifact}`,
106
168
  `completionFamilies=${COMPLETION_BACKFILL_FAMILIES.join(',')}`,
107
169
  `synchronized=${inventory.artifacts.map((artifact) => artifact.name).join(',') || 'none'}`,
108
170
  `completedAt=${metadata.timestamp}`
@@ -131,5 +193,5 @@ async function backfillCompletionComments(
131
193
  }), artifacts, warnings);
132
194
  }
133
195
 
134
- export { backfillCompletionComments };
135
- export type { CompletionBackfillOptions, CompletionBackfillResult };
196
+ export { backfillCompletionComments, inspectCompletionBackfillEligibility };
197
+ export type { CompletionBackfillEligibility, CompletionBackfillOptions, CompletionBackfillResult };
@@ -27,7 +27,6 @@ import {
27
27
  import { projectTaskComment } from './task-comment-projection.ts';
28
28
  import {
29
29
  canonicalizeCommentBody,
30
- canonicalizeSummaryBody,
31
30
  escapeHtmlText,
32
31
  fenceRanges,
33
32
  renderSafeCodeFence
@@ -52,7 +51,6 @@ type SyncOptions = {
52
51
  client?: PlatformClient;
53
52
  runtimeVersion?: string;
54
53
  summaryAuthorization?: { sha256: string };
55
- verifyOnly?: boolean;
56
54
  };
57
55
 
58
56
  function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
@@ -76,8 +74,6 @@ const ARTIFACT_TITLES: Record<string, string> = {
76
74
  'review-plan': '技术方案审查',
77
75
  code: '实现报告',
78
76
  'review-code': '代码审查',
79
- 'manual-validation': '人工验证报告',
80
- 'validation-run': '验证运行证据',
81
77
  'pr-review': 'PR 审查报告'
82
78
  };
83
79
 
@@ -154,7 +150,7 @@ function taskCommentLanguage(repoRoot: string): string {
154
150
 
155
151
  function artifactIdentity(artifact: string): { stem: string; title: string } {
156
152
  const stem = path.basename(artifact, '.md');
157
- 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+))?$/);
158
154
  if (!match) throw new Error(`unsupported artifact '${artifact}'`);
159
155
  const base = ARTIFACT_TITLES[match[1]!]!;
160
156
  const round = match[2] ? Number(match[2]) : 1;
@@ -483,15 +479,6 @@ function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boole
483
479
  return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
484
480
  }
485
481
 
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
482
  async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
496
483
  return provider.comments?.list
497
484
  ? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
@@ -609,27 +596,6 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
609
596
  }
610
597
  }
611
598
 
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
-
632
-
633
599
  // Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
634
600
  if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
635
601
  const operations = existing.map((comment): PlatformOperation => ({
@@ -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',
@@ -11,10 +11,8 @@ import {
11
11
  type SandboxControlRequest,
12
12
  type SandboxControlResponse,
13
13
  type SandboxTaskCreateRequest,
14
- type SandboxTaskCommandRequest,
15
14
  type SandboxTaskFinalizationRequest,
16
- type SandboxCodexControllerRequest,
17
- type SandboxTaskWorkflowRequest
15
+ type SandboxCodexControllerRequest
18
16
  } from './protocol.ts';
19
17
  import type {
20
18
  CodexControllerLeaseProofV1,
@@ -26,8 +24,6 @@ import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts'
26
24
  import type { TaskCreateCandidateV1 } from '../../task/create.ts';
27
25
  import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
28
26
  import { readSandboxControlIdentitySentinel } from './identity-sentinel.ts';
29
- import type { TaskWorkflowRequest } from './task-workflow.ts';
30
- import type { LifecycleAuthorityRequestV1 } from '../../task/control-authority.ts';
31
27
  import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.ts';
32
28
 
33
29
  const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
@@ -119,8 +115,6 @@ function preflight(
119
115
 
120
116
  function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
121
117
  if (request.family === 'task-lifecycle' || request.family === 'task-finalization') return 'progress';
122
- if (request.family === 'task-orchestration') return request.args[1] === 'status' ? 'diagnostic' : 'progress';
123
- if (request.family === 'task-workflow') return request.workflow.operation === 'artifact-inspect' || request.workflow.operation === 'decision-next-id' ? 'diagnostic' : 'progress';
124
118
  return null;
125
119
  }
126
120
 
@@ -194,9 +188,7 @@ export function resolveVisibleActiveShortId(ref: string): string | null {
194
188
  // may be a documented short id. Compare canonical ids so a bound sandbox keeps
195
189
  // refusing other tasks without rejecting its own task's short id.
196
190
  function canonicalRequestTask(request: SandboxControlRequest): CanonicalRequestTask {
197
- const ref = request.family === 'task-workflow'
198
- ? request.workflow.taskId
199
- : 'args' in request ? request.args[0] ?? null : null;
191
+ const ref = 'args' in request ? request.args[0] ?? null : null;
200
192
  if (!ref) return { kind: 'none' };
201
193
  if (CONTROL_TASK_ID_RE.test(ref)) return { kind: 'task', taskId: ref };
202
194
  if (!CONTROL_SHORT_ID_RE.test(ref)) return { kind: 'none' };
@@ -223,7 +215,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
223
215
  || (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
224
216
  clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
225
217
  }
226
- if (identity.mode === 'branch-only' && (request.family === 'task-finalization' || request.family === 'task-workflow')) {
218
+ if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
227
219
  clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
228
220
  }
229
221
  }
@@ -384,7 +376,7 @@ export function requestSandboxControl(params: Readonly<{
384
376
  token?: string; generation?: string; timeoutMs?: number;
385
377
  }>): SandboxControlResponse {
386
378
  if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
387
- if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization' || params.family === 'task-workflow') {
379
+ if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
388
380
  clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
389
381
  }
390
382
  const auth = authority(params);
@@ -392,41 +384,13 @@ export function requestSandboxControl(params: Readonly<{
392
384
  const request: SandboxControlRequest = {
393
385
  version: 3, id: randomUUID(), ...auth, issuedAt,
394
386
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
395
- family: params.family as 'task-lifecycle' | 'task-orchestration', args: params.args,
387
+ family: params.family as 'task-lifecycle', args: params.args,
396
388
  controllerProcess: null,
397
389
  controllerProof: null
398
390
  };
399
391
  return exchangeSandboxControl(request, params);
400
392
  }
401
393
 
402
- export function requestSandboxTaskControl(params: Readonly<{
403
- family: 'task-lifecycle' | 'task-orchestration';
404
- args: string[];
405
- controllerProof: CodexControllerLeaseProofV1 | null;
406
- authority?: LifecycleAuthorityRequestV1;
407
- channelDir?: string;
408
- statusDir?: string;
409
- token?: string;
410
- generation?: string;
411
- timeoutMs?: number;
412
- }>): SandboxControlResponse {
413
- const auth = authority(params);
414
- const issuedAt = Date.now();
415
- const request: SandboxTaskCommandRequest = {
416
- version: 3,
417
- id: params.authority?.requestId ?? randomUUID(),
418
- ...auth,
419
- issuedAt,
420
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
421
- family: params.family,
422
- args: params.args,
423
- controllerProcess: null,
424
- controllerProof: params.authority ? null : params.controllerProof,
425
- ...(params.authority ? { authority: params.authority } : {})
426
- };
427
- return exchangeSandboxControl(request, params);
428
- }
429
-
430
394
  export function requestSandboxTaskFinalization(params: Readonly<{
431
395
  agent: string;
432
396
  channelDir?: string;
@@ -455,33 +419,6 @@ export function requestSandboxTaskFinalization(params: Readonly<{
455
419
  return exchangeSandboxControl(request, params);
456
420
  }
457
421
 
458
- export function requestSandboxTaskWorkflow(params: Readonly<{
459
- workflow: TaskWorkflowRequest;
460
- channelDir?: string;
461
- statusDir?: string;
462
- token?: string;
463
- generation?: string;
464
- timeoutMs?: number;
465
- authority?: LifecycleAuthorityRequestV1;
466
- }>): SandboxControlResponse {
467
- const auth = authority(params);
468
- const issuedAt = Date.now();
469
- const request: SandboxTaskWorkflowRequest = {
470
- version: 3,
471
- id: params.workflow.id,
472
- ...auth,
473
- issuedAt,
474
- expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
475
- family: 'task-workflow',
476
- args: [],
477
- controllerProcess: null,
478
- controllerProof: null,
479
- workflow: params.workflow,
480
- ...(params.authority ? { authority: params.authority } : {})
481
- };
482
- return exchangeSandboxControl(request, params);
483
- }
484
-
485
422
  type CodexControllerClosed = Readonly<{
486
423
  version: 1;
487
424
  status: 'closed';