@fitlab-ai/agent-infra 0.10.0 → 0.11.1

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 (154) hide show
  1. package/README.md +2 -6
  2. package/README.zh-CN.md +2 -6
  3. package/bin/internal-cli.sh +0 -13
  4. package/dist/bin/internal-cli.js +11 -49
  5. package/dist/lib/internal/cli-route-inventory.js +3 -4
  6. package/dist/lib/internal/manual-validation.js +6 -2
  7. package/dist/lib/internal/platform-comment.js +60 -2
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-finalization.js +2 -2
  10. package/dist/lib/internal/task-lifecycle.js +2 -2
  11. package/dist/lib/internal/task-operation-registry.js +22 -12
  12. package/dist/lib/internal/task-orchestration.js +2 -2
  13. package/dist/lib/internal/task-review.js +10 -5
  14. package/dist/lib/platform/activity-recovery.js +159 -0
  15. package/dist/lib/platform/issue-comments.js +84 -32
  16. package/dist/lib/platform/issues.js +3 -4
  17. package/dist/lib/platform/task-comment-projection.js +70 -0
  18. package/dist/lib/platform/task-recovery.js +55 -0
  19. package/dist/lib/platform/verification-sync.js +184 -162
  20. package/dist/lib/sandbox/control/executor.js +4 -2
  21. package/dist/lib/sandbox/control/task-workflow.js +1 -1
  22. package/dist/lib/sandbox/control/workflow-executor.js +90 -22
  23. package/dist/lib/server/process-state.js +17 -5
  24. package/dist/lib/task/artifact-command.js +6 -3
  25. package/dist/lib/task/artifact-lifecycle.js +15 -52
  26. package/dist/lib/task/artifact-operations.js +28 -2
  27. package/dist/lib/task/artifact-recovery.js +114 -68
  28. package/dist/lib/task/artifact-repair-intent.js +25 -8
  29. package/dist/lib/task/capabilities.js +0 -2
  30. package/dist/lib/task/control-authority.js +1 -1
  31. package/dist/lib/task/events.js +66 -49
  32. package/dist/lib/task/gate-policy.js +94 -0
  33. package/dist/lib/task/intermediate-cleanup.js +28 -3
  34. package/dist/lib/task/local-artifact-finalization.js +85 -18
  35. package/dist/lib/task/orchestration.js +8 -6
  36. package/dist/lib/task/review-artifacts.js +12 -6
  37. package/dist/lib/task/review-command.js +2 -2
  38. package/dist/lib/task/review-finalization.js +198 -11
  39. package/dist/lib/{host-control → task}/secure-fs.js +8 -19
  40. package/dist/lib/task/verification-engine.js +28 -17
  41. package/dist/lib/task/verification.js +25 -9
  42. package/dist/lib/task/workflow-command.js +2 -0
  43. package/dist/lib/task/workflow-dispatch.js +161 -0
  44. package/dist/lifecycle-build-manifest.json +3 -3
  45. package/lib/internal/cli-route-inventory.ts +3 -4
  46. package/lib/internal/manual-validation.ts +6 -2
  47. package/lib/internal/platform-comment.ts +53 -2
  48. package/lib/internal/task-artifact.ts +1 -1
  49. package/lib/internal/task-finalization.ts +1 -1
  50. package/lib/internal/task-lifecycle.ts +1 -1
  51. package/lib/internal/task-operation-registry.ts +22 -12
  52. package/lib/internal/task-orchestration.ts +1 -1
  53. package/lib/internal/task-review.ts +9 -5
  54. package/lib/platform/activity-recovery.ts +179 -0
  55. package/lib/platform/issue-comments.ts +98 -31
  56. package/lib/platform/issues.ts +3 -4
  57. package/lib/platform/task-comment-projection.ts +79 -0
  58. package/lib/platform/task-recovery.ts +54 -0
  59. package/lib/platform/verification-sync.ts +203 -188
  60. package/lib/sandbox/control/executor.ts +4 -2
  61. package/lib/sandbox/control/protocol.ts +1 -1
  62. package/lib/sandbox/control/task-workflow.ts +1 -1
  63. package/lib/sandbox/control/workflow-executor.ts +113 -22
  64. package/lib/server/process-state.ts +26 -5
  65. package/lib/task/artifact-command.ts +6 -4
  66. package/lib/task/artifact-lifecycle.ts +12 -49
  67. package/lib/task/artifact-operations.ts +28 -2
  68. package/lib/task/artifact-recovery.ts +120 -67
  69. package/lib/task/artifact-repair-intent.ts +38 -11
  70. package/lib/task/capabilities.ts +0 -1
  71. package/lib/task/control-authority.ts +1 -1
  72. package/lib/task/events.ts +55 -38
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/intermediate-cleanup.ts +18 -2
  75. package/lib/task/local-artifact-finalization.ts +91 -15
  76. package/lib/task/orchestration.ts +12 -7
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +1 -1
  79. package/lib/task/review-finalization.ts +198 -9
  80. package/lib/{host-control → task}/secure-fs.ts +10 -32
  81. package/lib/task/verification-engine.ts +24 -14
  82. package/lib/task/verification-types.ts +7 -0
  83. package/lib/task/verification.ts +25 -9
  84. package/lib/task/workflow-command.ts +2 -0
  85. package/lib/task/workflow-dispatch.ts +171 -0
  86. package/package.json +1 -1
  87. package/templates/.agents/rules/issue-sync.en.md +11 -1
  88. package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
  89. package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
  90. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
  91. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  94. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  95. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  96. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  97. package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
  98. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
  99. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  100. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  101. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  102. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  104. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  105. package/templates/.agents/skills/commit/config/verify.json +1 -5
  106. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  108. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  109. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  110. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  111. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  112. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  113. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
  114. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
  115. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  116. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  117. package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
  118. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
  119. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  120. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  121. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  122. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  123. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  124. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  125. package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
  126. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
  127. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  128. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  129. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  130. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  131. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  134. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  135. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  136. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  137. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  138. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  139. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  140. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  141. package/dist/lib/host-control/audit.js +0 -19
  142. package/dist/lib/host-control/client.js +0 -142
  143. package/dist/lib/host-control/command.js +0 -49
  144. package/dist/lib/host-control/path.js +0 -160
  145. package/dist/lib/host-control/server.js +0 -159
  146. package/dist/lib/host-control/service.js +0 -112
  147. package/dist/lib/internal/host-control.js +0 -28
  148. package/lib/host-control/audit.ts +0 -22
  149. package/lib/host-control/client.ts +0 -172
  150. package/lib/host-control/command.ts +0 -60
  151. package/lib/host-control/path.ts +0 -167
  152. package/lib/host-control/server.ts +0 -180
  153. package/lib/host-control/service.ts +0 -133
  154. package/lib/internal/host-control.ts +0 -28
@@ -188,7 +188,9 @@ export async function prepareSandboxControlExecution(params: {
188
188
  owner: gateOwner,
189
189
  requestId: params.request.id,
190
190
  operationId: authority?.operationId ?? params.request.id,
191
- phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
191
+ phase: authority?.phase ?? (params.request.family === 'task-workflow'
192
+ && params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
193
+ : params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
192
194
  authority
193
195
  } satisfies SandboxControlExecutorGateV2);
194
196
  },
@@ -235,7 +237,7 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
235
237
  return;
236
238
  }
237
239
  if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
238
- || !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
240
+ || !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
239
241
  || (value.authority !== null && value.authority !== undefined && (() => {
240
242
  try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
241
243
  })())) {
@@ -62,7 +62,7 @@ export type SandboxControlExecutorGateV2 = Readonly<{
62
62
  owner: SandboxControlBrokerOwner;
63
63
  requestId: string;
64
64
  operationId: string;
65
- phase: 'orchestration.prepare' | 'artifact.finalize-local' | 'task-event.completed';
65
+ phase: 'orchestration.prepare' | 'artifact.preflight' | 'artifact.finalize-local' | 'task-event.completed';
66
66
  authority: LifecycleRecoveryAttestationV1 | null;
67
67
  }>;
68
68
  type RequestBase = Readonly<{
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
- import { readStableFile, SecureFileError, writeAtomicFile } from '../../host-control/secure-fs.ts';
5
+ import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.ts';
6
6
 
7
7
  import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS, type TaskWorkflowOperation, type WorkflowCommand } from '../../task/workflow-command.ts';
8
8
 
@@ -2,10 +2,15 @@ import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.ts';
2
2
 
3
3
  import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.ts';
4
4
  import { parseReviewCommand } from '../../task/review-command.ts';
5
- import { prepareLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
6
- import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.ts';
5
+ import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
6
+ import {
7
+ prepareReviewSummaryCandidate,
8
+ commitReviewSummaryProvenance,
9
+ preflightReviewSummary
10
+ } from '../../task/review-finalization.ts';
11
+ import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.ts';
7
12
  import { withTaskExecutionLock } from '../../task/task-execution-lock.ts';
8
- import { dispatchHostControlCommand } from '../../host-control/command.ts';
13
+ import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.ts';
9
14
  import { assertSandboxTaskSource } from '../workspace-view.ts';
10
15
  import { appendDiagnosticAudit } from './audit.ts';
11
16
  import {
@@ -17,6 +22,17 @@ import type { LifecycleRecoveryAttestationV1 } from '../../task/control-authorit
17
22
  import { applyTaskEvent } from '../../task/events.ts';
18
23
  import { parseTaskEventRequest } from '../../internal/task-event.ts';
19
24
 
25
+ export type TaskWorkflowFaultWindow =
26
+ | 'before-call'
27
+ | 'before-domain-write'
28
+ | 'after-atomic-rename'
29
+ | 'before-result-return';
30
+
31
+ export type TaskWorkflowExecutionOptions = Readonly<{
32
+ /** Deterministic test seam; production callers leave this unset. */
33
+ faultWindow?: TaskWorkflowFaultWindow;
34
+ }>;
35
+
20
36
  function executionResult(result: Record<string, unknown>): SandboxControlExecutionResult {
21
37
  return { exitCode: result.status === 'failed' || result.status === 'refused' ? 1 : 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
22
38
  }
@@ -25,10 +41,41 @@ function executionResult(result: Record<string, unknown>): SandboxControlExecuti
25
41
  export async function executeTaskWorkflow(
26
42
  manifest: SandboxControlManifest,
27
43
  request: TaskWorkflowRequest,
28
- lifecycleRecoveryAttestation: LifecycleRecoveryAttestationV1 | null = null
44
+ lifecycleRecoveryAttestation: LifecycleRecoveryAttestationV1 | null = null,
45
+ options: TaskWorkflowExecutionOptions = {}
29
46
  ): Promise<SandboxControlExecutionResult> {
30
- let publicationStarted = false;
47
+ let publicationCommitted = false;
48
+ let recoveryObservation: Readonly<{
49
+ family: 'analysis' | 'plan' | 'code' | 'review-analysis' | 'review-plan' | 'review-code';
50
+ artifact: string;
51
+ wasPassed: boolean;
52
+ }> | null = null;
53
+ const observeRecoveryPublication = (): void => {
54
+ if (!recoveryObservation || recoveryObservation.wasPassed) return;
55
+ const intent = readArtifactRecoveryIntent(
56
+ manifest.repoRoot,
57
+ request.taskId,
58
+ recoveryObservation.family,
59
+ recoveryObservation.artifact
60
+ );
61
+ publicationCommitted ||= intent?.state === 'preflight-ready'
62
+ || intent?.state === 'preflight-commit-started'
63
+ || intent?.state === 'preflight-passed'
64
+ || intent?.state === 'passed'
65
+ || intent?.state === 'commit-started';
66
+ };
67
+ let faultTriggered = false;
68
+ const fault = (window: TaskWorkflowFaultWindow): void => {
69
+ if (faultTriggered || options.faultWindow !== window) return;
70
+ faultTriggered = true;
71
+ throw new Error(`TASK_WORKFLOW_FAULT_INJECTED:${window}`);
72
+ };
73
+ const response = (result: Record<string, unknown>): SandboxControlExecutionResult => {
74
+ fault('before-result-return');
75
+ return executionResult(result);
76
+ };
31
77
  try {
78
+ fault('before-call');
32
79
  const taskDir = assertSandboxTaskSource(manifest.repoRoot, request.taskId);
33
80
  const [command] = TASK_WORKFLOW_COMMANDS[request.operation];
34
81
  if (request.operation === 'event') {
@@ -36,57 +83,101 @@ export async function executeTaskWorkflow(
36
83
  const boundEventRequest = lifecycleRecoveryAttestation && eventRequest.requestId === undefined
37
84
  ? { ...eventRequest, requestId: lifecycleRecoveryAttestation.lifecycleRequestId }
38
85
  : eventRequest;
86
+ fault('before-domain-write');
39
87
  const result = applyTaskEvent(boundEventRequest, {
40
88
  repoRoot: manifest.repoRoot,
41
89
  lifecycleRecoveryAttestation,
42
90
  deferLifecycleRecoveryConsumption: true
43
91
  });
44
- return executionResult(result);
92
+ publicationCommitted = result.changed;
93
+ fault('after-atomic-rename');
94
+ return response(result);
45
95
  }
46
96
  if (command !== 'task-artifact' && command !== 'task-review') {
47
- publicationStarted = true;
48
- // Inherit this executor's process group so its existing recovery owns the worker too.
49
- const result = await dispatchHostControlCommand({
50
- operation: command, payload: { workingDirectory: manifest.repoRoot, args: request.args }
51
- });
52
- return result;
97
+ // The broker owns this short-lived executor; no global service or worker
98
+ // credential participates in the command's authority or recovery path.
99
+ fault('before-domain-write');
100
+ const result = dispatchWorkflowCommand(manifest.repoRoot, request.operation, request.args);
101
+ publicationCommitted = result.changed === true;
102
+ fault('after-atomic-rename');
103
+ return response(result);
53
104
  }
54
105
  const input = command === 'task-artifact' ? parseArtifactCommand(request.args) : parseReviewCommand(request.args);
55
106
  if (command === 'task-artifact' && 'operation' in input && input.operation === 'inspect') {
56
- return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot }));
107
+ fault('before-domain-write');
108
+ const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot });
109
+ publicationCommitted = result.changed === true;
110
+ fault('after-atomic-rename');
111
+ return response(result);
57
112
  }
58
113
  if ('operation' in input && input.operation === 'init') {
59
- return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir }));
114
+ fault('before-domain-write');
115
+ const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir });
116
+ publicationCommitted = result.changed === true;
117
+ fault('after-atomic-rename');
118
+ return response(result);
60
119
  }
61
120
  return await withTaskExecutionLock(manifest.repoRoot, request.taskId, `sandbox-control.${request.operation}`, async () => {
62
121
  const artifact = await readTaskArtifact(taskDir, { artifact: input.artifact });
63
122
  const content = artifact.bytes.toString('utf8');
123
+ fault('before-domain-write');
64
124
  let result: Record<string, unknown>;
65
125
  if ('operation' in input) {
66
126
  const { family } = input;
67
127
  if (family !== 'analysis' && family !== 'plan' && family !== 'code') throw new Error('ARTIFACT_IDENTITY_INVALID');
68
128
  const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot, recoveryId: input.recoveryId, lockAlreadyHeld: true } as const;
129
+ if (input.operation === 'preflight') {
130
+ // Preflight only seals a verified generation. It must not publish the
131
+ // formal artifact, set publicationStarted, or append finalizer audit.
132
+ const result = preflightLocalArtifact(local);
133
+ recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
134
+ observeRecoveryPublication();
135
+ return response(result);
136
+ }
137
+ recoveryObservation = {
138
+ family,
139
+ artifact: input.artifact,
140
+ wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
141
+ };
69
142
  const prepared = prepareLocalArtifact(local, content, lifecycleRecoveryAttestation ?? undefined);
70
- if (prepared.result.status === 'failed') return executionResult(commitLocalArtifactProvenance(prepared));
71
- publicationStarted = true;
72
- result = commitLocalArtifactProvenance(prepared);
143
+ observeRecoveryPublication();
144
+ const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
145
+ if (prepared.result.status === 'failed') return executionResult(commitLocalArtifactProvenance(prepared, commitOptions));
146
+ result = commitLocalArtifactProvenance(prepared, commitOptions);
73
147
  } else {
74
148
  if (input.overrideTicket) throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
149
+ const family = `review-${input.stage}` as 'review-analysis' | 'review-plan' | 'review-code';
150
+ if (request.operation === 'review-preflight') {
151
+ const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
152
+ recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
153
+ observeRecoveryPublication();
154
+ return response(result);
155
+ }
156
+ recoveryObservation = {
157
+ family,
158
+ artifact: input.artifact,
159
+ wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
160
+ };
75
161
  const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true, startRecovery: true });
162
+ observeRecoveryPublication();
76
163
  if (input.dryRun || prepared.result.status === 'planned') return executionResult(prepared.result);
77
- if (prepared.result.status === 'failed') return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot));
78
- publicationStarted = true;
79
- result = commitReviewSummaryProvenance(prepared, manifest.repoRoot);
164
+ const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
165
+ if (prepared.result.status === 'failed') return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions));
166
+ result = commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions);
80
167
  }
168
+ observeRecoveryPublication();
169
+ publicationCommitted ||= result.changed === true;
81
170
  if (result.status !== 'failed') appendDiagnosticAudit(manifest, 'task-workflow-artifact-finalized', {
82
171
  requestId: request.id, sandboxTaskId: request.taskId, workflowOperation: request.operation,
83
172
  artifact: input.artifact, sha256: result.artifactSha256 as string, semanticDigest: result.semanticDigest as string
84
173
  });
85
- return executionResult({ ...result, changed: true });
174
+ fault('after-atomic-rename');
175
+ return response(result);
86
176
  });
87
177
  } catch (error) {
178
+ observeRecoveryPublication();
88
179
  const message = error instanceof Error ? error.message : String(error);
89
180
  const code = (error as { code?: string })?.code ?? /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_WORKFLOW_REQUEST_INVALID';
90
- return executionResult({ status: 'failed', changed: publicationStarted ? null : false, error: { code, message } });
181
+ return executionResult({ status: 'failed', changed: publicationCommitted ? null : false, error: { code, message } });
91
182
  }
92
183
  }
@@ -12,6 +12,11 @@ export type ProcessIdentity = Readonly<{
12
12
  export type ProcessIdentityState = 'alive' | 'dead' | 'unknown';
13
13
  export type ProcessIdentityProbe = (identity: ProcessIdentity) => ProcessIdentityState;
14
14
 
15
+ type ProcessIdentityRuntime = Readonly<{
16
+ probePid?: (pid: number) => void;
17
+ execFileSync?: typeof execFileSync;
18
+ }>;
19
+
15
20
  export type PidRecord = ProcessIdentity & {
16
21
  version: 2;
17
22
  };
@@ -132,7 +137,8 @@ export function getProcessStartTime(
132
137
 
133
138
  export function getProcessIdentityState(
134
139
  identity: ProcessIdentity,
135
- platform: NodeJS.Platform = process.platform
140
+ platform: NodeJS.Platform = process.platform,
141
+ runtime: ProcessIdentityRuntime = {}
136
142
  ): ProcessIdentityState {
137
143
  if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
138
144
  return 'dead';
@@ -147,8 +153,9 @@ export function getProcessIdentityState(
147
153
  return (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'dead' : 'unknown';
148
154
  }
149
155
  }
156
+ const probePid = runtime.probePid ?? ((pid: number) => process.kill(pid, 0));
150
157
  try {
151
- process.kill(identity.pid, 0);
158
+ probePid(identity.pid);
152
159
  } catch (error) {
153
160
  const code = (error as NodeJS.ErrnoException).code;
154
161
  if (code === 'ESRCH') return 'dead';
@@ -157,20 +164,34 @@ export function getProcessIdentityState(
157
164
  const query = buildProcessStartTimeQuery(identity.pid, platform);
158
165
  if (query === null) return 'unknown';
159
166
  try {
160
- const output = execFileSync(query.command, query.args, {
167
+ const output = (runtime.execFileSync ?? execFileSync)(query.command, query.args, {
161
168
  encoding: 'utf8',
162
169
  env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
163
170
  }).trim();
164
- if (output.length === 0) return 'unknown';
171
+ if (output.length === 0) return recheckDarwinProcessExit(identity.pid, platform, probePid);
165
172
  const startTime = platform === 'win32'
166
173
  ? new Date(output).getTime()
167
174
  : platform === 'darwin'
168
175
  ? parseDarwinStartTime(output)
169
176
  : null;
170
- if (startTime === null || Number.isNaN(startTime)) return 'unknown';
177
+ if (startTime === null || Number.isNaN(startTime)) return recheckDarwinProcessExit(identity.pid, platform, probePid);
171
178
  return startTime === identity.startTime ? 'alive' : 'dead';
172
179
  } catch {
180
+ return recheckDarwinProcessExit(identity.pid, platform, probePid);
181
+ }
182
+ }
183
+
184
+ function recheckDarwinProcessExit(
185
+ pid: number,
186
+ platform: NodeJS.Platform,
187
+ probePid: (pid: number) => void
188
+ ): ProcessIdentityState {
189
+ if (platform !== 'darwin') return 'unknown';
190
+ try {
191
+ probePid(pid);
173
192
  return 'unknown';
193
+ } catch (error) {
194
+ return (error as NodeJS.ErrnoException).code === 'ESRCH' ? 'dead' : 'unknown';
174
195
  }
175
196
  }
176
197
 
@@ -4,12 +4,12 @@ import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecyc
4
4
  import { parseArtifactName } from './artifact-name.ts';
5
5
  import { resolveTaskRef } from './resolve-ref.ts';
6
6
  import { getArtifactSchema } from './artifact-schema.ts';
7
- import { finalizeLocalArtifact } from './local-artifact-finalization.ts';
7
+ import { finalizeLocalArtifact, preflightLocalArtifact } from './local-artifact-finalization.ts';
8
8
  import { initializeArtifactSkeleton } from './artifact-operations.ts';
9
9
 
10
10
  export type ArtifactCommand = Readonly<{
11
11
  taskRef: string;
12
- operation: 'inspect' | 'init' | 'finalize-local';
12
+ operation: 'inspect' | 'init' | 'preflight' | 'finalize-local';
13
13
  family: string;
14
14
  artifact: string;
15
15
  locale?: 'zh-CN' | 'en';
@@ -19,13 +19,14 @@ export type ArtifactCommand = Readonly<{
19
19
  /** One option contract for the CLI and trusted projection executor. */
20
20
  export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
21
21
  const [taskRef, operation] = args;
22
- if (!taskRef || !['inspect', 'init', 'finalize-local'].includes(operation ?? '')) {
22
+ if (!taskRef || !['inspect', 'init', 'preflight', 'finalize-local'].includes(operation ?? '')) {
23
23
  throw new Error('task ref and a supported artifact operation are required');
24
24
  }
25
25
  const fields: Record<string, string> = {};
26
26
  const allowed = {
27
27
  inspect: ['--family'],
28
28
  init: ['--family', '--artifact', '--locale'],
29
+ 'preflight': ['--family', '--artifact', '--recovery-id'],
29
30
  'finalize-local': ['--family', '--artifact', '--recovery-id']
30
31
  }[operation as ArtifactCommand['operation']];
31
32
  for (let index = 2; index < args.length; index += 1) {
@@ -79,7 +80,8 @@ export function executeArtifactCommand(
79
80
  return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
80
81
  }
81
82
  if (family !== 'analysis' && family !== 'plan' && family !== 'code') {
82
- return fail('ARTIFACT_PAYLOAD_INVALID', "finalize-local only supports 'analysis', 'plan', and 'code'");
83
+ return fail('ARTIFACT_PAYLOAD_INVALID', "preflight and finalize-local only support 'analysis', 'plan', and 'code'");
83
84
  }
85
+ if (operation === 'preflight') return { ...preflightLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
84
86
  return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
85
87
  }
@@ -221,67 +221,30 @@ function resolveReviewedInput(
221
221
  expectedFamily: ArtifactFamily,
222
222
  diagnostics: ArtifactDiagnostic[]
223
223
  ): ArtifactIdentity | null {
224
- let content: string;
225
- try { content = fs.readFileSync(review.path, 'utf8'); }
226
- catch (error) {
227
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
228
- return null;
229
- }
230
- const parsed = parseReviewedInputReference(content, expectedFamily);
231
- if (!parsed) {
232
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
233
- return null;
234
- }
235
- const abs = path.join(taskDir, parsed.name);
236
224
  try {
225
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
226
+ const receipt = receiptForOutput(taskContent, review.name);
227
+ const expectedEvent = reviewEventName(review.family);
228
+ if (!receipt || receipt.event !== expectedEvent) throw new Error(`receipt for ${review.name} is missing or invalid`);
229
+ const input = parseArtifactName(receipt.input);
230
+ if (!input || input.family !== expectedFamily) throw new Error(`receipt input '${receipt.input}' is not a canonical ${expectedFamily} artifact`);
231
+ const abs = path.join(taskDir, input.name);
237
232
  const stat = fs.lstatSync(abs);
238
233
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('referenced input is not a regular file');
239
- const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
240
234
  const invalidation = parseInvalidationDocument(taskContent);
241
235
  if (!invalidation.ok) throw new Error(invalidation.message);
242
- if (isArtifactInvalidated(invalidation.document, expectedFamily, parsed.name)) {
243
- throw new Error(`${parsed.name} is invalidated`);
244
- }
245
- const receipt = receiptForOutput(taskContent, review.name);
246
- if (!receipt) throw new Error(`receipt for ${review.name} is missing`);
247
- const expectedEvent = reviewEventName(review.family);
248
- if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
249
- throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
250
- }
236
+ if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name)) throw new Error(`${input.name} is invalidated`);
251
237
  const actualSha256 = sha256File(abs);
252
238
  if (actualSha256 !== receipt.inputSha256) {
253
- throw new Error(`${parsed.name} content digest does not match receipt`);
239
+ throw new Error(`${input.name} content digest does not match receipt`);
254
240
  }
255
- return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
241
+ return { ...input, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
256
242
  } catch (error) {
257
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name}: ${String(error)}`));
243
+ diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
258
244
  return null;
259
245
  }
260
246
  }
261
247
 
262
- function parseReviewedInputReference(content: string, expectedFamily: ArtifactFamily): { family: ArtifactFamily; round: number; name: string } | null {
263
- const lines = content.split(/\r?\n/);
264
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
265
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
266
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]!);
267
- return candidates
268
- .map((name) => parseArtifactName(name))
269
- .find((item) => item?.family === expectedFamily) ?? null;
270
- }
271
-
272
- function parseCodePlanInputReference(content: string): { family: 'plan'; round: number; name: string } | null {
273
- const lines = content.split(/\r?\n/);
274
- const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
275
- if (sectionStart < 0) return null;
276
- const bodyLines = lines.slice(sectionStart + 1);
277
- const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
278
- const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
279
- const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
280
- if (!match) return null;
281
- const parsed = parseArtifactName(match[1]!);
282
- return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
283
- }
284
-
285
248
  function reviewEventName(family: ArtifactFamily): 'review-analysis.completed' | 'review-plan.completed' | 'review-code.completed' {
286
249
  if (family === 'review-analysis') return 'review-analysis.completed';
287
250
  if (family === 'review-plan') return 'review-plan.completed';
@@ -557,7 +520,7 @@ function buildArtifactLinkSection(content: string, artifact: ArtifactIdentity):
557
520
  export {
558
521
  artifactFamilyCatalog, familySpec,
559
522
  inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext,
560
- parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName,
523
+ resolveCodePlanInput, reviewEventName,
561
524
  assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection
562
525
  };
563
526
  export type {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
- import { readStableFileSync } from '../host-control/secure-fs.ts';
4
+ import { readStableFileSync } from './secure-fs.ts';
5
5
  import { parseArtifactName } from './artifact-name.ts';
6
6
 
7
7
  import { scanVisibleMarkdown, type VisibleMarkdown, type VisibleHeading } from './markdown.ts';
@@ -10,6 +10,12 @@ import {
10
10
  renderArtifactSkeleton
11
11
  } from './artifact-schema.ts';
12
12
  import { withTaskExecutionLock } from './task-execution-lock.ts';
13
+ import {
14
+ buildQualificationAudit,
15
+ expectedQualificationRelations,
16
+ parseTaskQualification,
17
+ renderQualificationAudit
18
+ } from './qualification-audit.ts';
13
19
  import type {
14
20
  ArtifactSchema,
15
21
  ArtifactSchemaFamily,
@@ -288,6 +294,23 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
288
294
  return identity?.family === family ? identity.round : null;
289
295
  }
290
296
 
297
+ function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
298
+ let taskContent: string;
299
+ try {
300
+ taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
301
+ } catch (error) {
302
+ return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
303
+ }
304
+ const qualification = parseTaskQualification(taskContent);
305
+ if (!qualification.ok) return qualification;
306
+ if (!qualification.qualification.present) return { ok: true, content: '' };
307
+ const relations = expectedQualificationRelations(taskContent, request.family);
308
+ if (!relations.ok) return relations;
309
+ const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
310
+ if (!audit.ok) return audit;
311
+ return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
312
+ }
313
+
291
314
  function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
292
315
  const schema = getArtifactSchema(request.family);
293
316
  if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
@@ -304,12 +327,15 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
304
327
  } catch (error) {
305
328
  if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
306
329
  }
307
- const content = renderArtifactSkeleton({
330
+ const skeleton = renderArtifactSkeleton({
308
331
  taskId: request.taskId,
309
332
  family: request.family,
310
333
  artifact: request.artifact,
311
334
  ...(request.locale ? { locale: request.locale } : {})
312
335
  });
336
+ const audit = qualificationAuditForInit(request);
337
+ if (!audit.ok) return resultFailure(audit.code, audit.message);
338
+ const content = `${skeleton.trimEnd()}${audit.content}`;
313
339
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
314
340
  try {
315
341
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });