@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
@@ -125,7 +125,9 @@ export async function prepareSandboxControlExecution(params) {
125
125
  owner: gateOwner,
126
126
  requestId: params.request.id,
127
127
  operationId: authority?.operationId ?? params.request.id,
128
- phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
128
+ phase: authority?.phase ?? (params.request.family === 'task-workflow'
129
+ && params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
130
+ : params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
129
131
  authority
130
132
  });
131
133
  },
@@ -163,7 +165,7 @@ function waitForGate(nonce, timeoutMs = 2_000) {
163
165
  return;
164
166
  }
165
167
  if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
166
- || !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase)
168
+ || !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase)
167
169
  || (value.authority !== null && value.authority !== undefined && (() => {
168
170
  try {
169
171
  validateLifecycleRecoveryAttestation(value.authority);
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { readStableFile, SecureFileError, writeAtomicFile } from '../../host-control/secure-fs.js';
4
+ import { readStableFile, SecureFileError, writeAtomicFile } from '../../task/secure-fs.js';
5
5
  import { TASK_WORKFLOW_COMMANDS, TASK_WORKFLOW_OPERATIONS } from '../../task/workflow-command.js';
6
6
  export function validateTaskWorkflowRequest(value) {
7
7
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -1,10 +1,11 @@
1
1
  import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.js';
2
2
  import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.js';
3
3
  import { parseReviewCommand } from '../../task/review-command.js';
4
- import { prepareLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.js';
5
- import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.js';
4
+ import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.js';
5
+ import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance, preflightReviewSummary } from '../../task/review-finalization.js';
6
+ import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.js';
6
7
  import { withTaskExecutionLock } from '../../task/task-execution-lock.js';
7
- import { dispatchHostControlCommand } from '../../host-control/command.js';
8
+ import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.js';
8
9
  import { assertSandboxTaskSource } from '../workspace-view.js';
9
10
  import { appendDiagnosticAudit } from './audit.js';
10
11
  import { readTaskArtifact } from './task-workflow.js';
@@ -14,9 +15,32 @@ function executionResult(result) {
14
15
  return { exitCode: result.status === 'failed' || result.status === 'refused' ? 1 : 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
15
16
  }
16
17
  /** The broker already authorized this host executor; domain operations own validation. */
17
- export async function executeTaskWorkflow(manifest, request, lifecycleRecoveryAttestation = null) {
18
- let publicationStarted = false;
18
+ export async function executeTaskWorkflow(manifest, request, lifecycleRecoveryAttestation = null, options = {}) {
19
+ let publicationCommitted = false;
20
+ let recoveryObservation = null;
21
+ const observeRecoveryPublication = () => {
22
+ if (!recoveryObservation || recoveryObservation.wasPassed)
23
+ return;
24
+ const intent = readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, recoveryObservation.family, recoveryObservation.artifact);
25
+ publicationCommitted ||= intent?.state === 'preflight-ready'
26
+ || intent?.state === 'preflight-commit-started'
27
+ || intent?.state === 'preflight-passed'
28
+ || intent?.state === 'passed'
29
+ || intent?.state === 'commit-started';
30
+ };
31
+ let faultTriggered = false;
32
+ const fault = (window) => {
33
+ if (faultTriggered || options.faultWindow !== window)
34
+ return;
35
+ faultTriggered = true;
36
+ throw new Error(`TASK_WORKFLOW_FAULT_INJECTED:${window}`);
37
+ };
38
+ const response = (result) => {
39
+ fault('before-result-return');
40
+ return executionResult(result);
41
+ };
19
42
  try {
43
+ fault('before-call');
20
44
  const taskDir = assertSandboxTaskSource(manifest.repoRoot, request.taskId);
21
45
  const [command] = TASK_WORKFLOW_COMMANDS[request.operation];
22
46
  if (request.operation === 'event') {
@@ -24,66 +48,110 @@ export async function executeTaskWorkflow(manifest, request, lifecycleRecoveryAt
24
48
  const boundEventRequest = lifecycleRecoveryAttestation && eventRequest.requestId === undefined
25
49
  ? { ...eventRequest, requestId: lifecycleRecoveryAttestation.lifecycleRequestId }
26
50
  : eventRequest;
51
+ fault('before-domain-write');
27
52
  const result = applyTaskEvent(boundEventRequest, {
28
53
  repoRoot: manifest.repoRoot,
29
54
  lifecycleRecoveryAttestation,
30
55
  deferLifecycleRecoveryConsumption: true
31
56
  });
32
- return executionResult(result);
57
+ publicationCommitted = result.changed;
58
+ fault('after-atomic-rename');
59
+ return response(result);
33
60
  }
34
61
  if (command !== 'task-artifact' && command !== 'task-review') {
35
- publicationStarted = true;
36
- // Inherit this executor's process group so its existing recovery owns the worker too.
37
- const result = await dispatchHostControlCommand({
38
- operation: command, payload: { workingDirectory: manifest.repoRoot, args: request.args }
39
- });
40
- return result;
62
+ // The broker owns this short-lived executor; no global service or worker
63
+ // credential participates in the command's authority or recovery path.
64
+ fault('before-domain-write');
65
+ const result = dispatchWorkflowCommand(manifest.repoRoot, request.operation, request.args);
66
+ publicationCommitted = result.changed === true;
67
+ fault('after-atomic-rename');
68
+ return response(result);
41
69
  }
42
70
  const input = command === 'task-artifact' ? parseArtifactCommand(request.args) : parseReviewCommand(request.args);
43
71
  if (command === 'task-artifact' && 'operation' in input && input.operation === 'inspect') {
44
- return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot }));
72
+ fault('before-domain-write');
73
+ const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot });
74
+ publicationCommitted = result.changed === true;
75
+ fault('after-atomic-rename');
76
+ return response(result);
45
77
  }
46
78
  if ('operation' in input && input.operation === 'init') {
47
- return executionResult(executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir }));
79
+ fault('before-domain-write');
80
+ const result = executeArtifactCommand(input, { repoRoot: manifest.repoRoot, artifactDir: taskDir });
81
+ publicationCommitted = result.changed === true;
82
+ fault('after-atomic-rename');
83
+ return response(result);
48
84
  }
49
85
  return await withTaskExecutionLock(manifest.repoRoot, request.taskId, `sandbox-control.${request.operation}`, async () => {
50
86
  const artifact = await readTaskArtifact(taskDir, { artifact: input.artifact });
51
87
  const content = artifact.bytes.toString('utf8');
88
+ fault('before-domain-write');
52
89
  let result;
53
90
  if ('operation' in input) {
54
91
  const { family } = input;
55
92
  if (family !== 'analysis' && family !== 'plan' && family !== 'code')
56
93
  throw new Error('ARTIFACT_IDENTITY_INVALID');
57
94
  const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot, recoveryId: input.recoveryId, lockAlreadyHeld: true };
95
+ if (input.operation === 'preflight') {
96
+ // Preflight only seals a verified generation. It must not publish the
97
+ // formal artifact, set publicationStarted, or append finalizer audit.
98
+ const result = preflightLocalArtifact(local);
99
+ recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
100
+ observeRecoveryPublication();
101
+ return response(result);
102
+ }
103
+ recoveryObservation = {
104
+ family,
105
+ artifact: input.artifact,
106
+ wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
107
+ };
58
108
  const prepared = prepareLocalArtifact(local, content, lifecycleRecoveryAttestation ?? undefined);
109
+ observeRecoveryPublication();
110
+ const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
59
111
  if (prepared.result.status === 'failed')
60
- return executionResult(commitLocalArtifactProvenance(prepared));
61
- publicationStarted = true;
62
- result = commitLocalArtifactProvenance(prepared);
112
+ return executionResult(commitLocalArtifactProvenance(prepared, commitOptions));
113
+ result = commitLocalArtifactProvenance(prepared, commitOptions);
63
114
  }
64
115
  else {
65
116
  if (input.overrideTicket)
66
117
  throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
118
+ const family = `review-${input.stage}`;
119
+ if (request.operation === 'review-preflight') {
120
+ const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
121
+ recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
122
+ observeRecoveryPublication();
123
+ return response(result);
124
+ }
125
+ recoveryObservation = {
126
+ family,
127
+ artifact: input.artifact,
128
+ wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
129
+ };
67
130
  const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true, startRecovery: true });
131
+ observeRecoveryPublication();
68
132
  if (input.dryRun || prepared.result.status === 'planned')
69
133
  return executionResult(prepared.result);
134
+ const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
70
135
  if (prepared.result.status === 'failed')
71
- return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot));
72
- publicationStarted = true;
73
- result = commitReviewSummaryProvenance(prepared, manifest.repoRoot);
136
+ return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions));
137
+ result = commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions);
74
138
  }
139
+ observeRecoveryPublication();
140
+ publicationCommitted ||= result.changed === true;
75
141
  if (result.status !== 'failed')
76
142
  appendDiagnosticAudit(manifest, 'task-workflow-artifact-finalized', {
77
143
  requestId: request.id, sandboxTaskId: request.taskId, workflowOperation: request.operation,
78
144
  artifact: input.artifact, sha256: result.artifactSha256, semanticDigest: result.semanticDigest
79
145
  });
80
- return executionResult({ ...result, changed: true });
146
+ fault('after-atomic-rename');
147
+ return response(result);
81
148
  });
82
149
  }
83
150
  catch (error) {
151
+ observeRecoveryPublication();
84
152
  const message = error instanceof Error ? error.message : String(error);
85
153
  const code = error?.code ?? /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_WORKFLOW_REQUEST_INVALID';
86
- return executionResult({ status: 'failed', changed: publicationStarted ? null : false, error: { code, message } });
154
+ return executionResult({ status: 'failed', changed: publicationCommitted ? null : false, error: { code, message } });
87
155
  }
88
156
  }
89
157
  //# sourceMappingURL=workflow-executor.js.map
@@ -97,7 +97,7 @@ export function getProcessStartTime(pid, platform = process.platform) {
97
97
  return null;
98
98
  }
99
99
  }
100
- export function getProcessIdentityState(identity, platform = process.platform) {
100
+ export function getProcessIdentityState(identity, platform = process.platform, runtime = {}) {
101
101
  if (!Number.isInteger(identity.pid) || identity.pid <= 0 || !Number.isSafeInteger(identity.startTime)) {
102
102
  return 'dead';
103
103
  }
@@ -114,8 +114,9 @@ export function getProcessIdentityState(identity, platform = process.platform) {
114
114
  return error.code === 'ENOENT' ? 'dead' : 'unknown';
115
115
  }
116
116
  }
117
+ const probePid = runtime.probePid ?? ((pid) => process.kill(pid, 0));
117
118
  try {
118
- process.kill(identity.pid, 0);
119
+ probePid(identity.pid);
119
120
  }
120
121
  catch (error) {
121
122
  const code = error.code;
@@ -128,23 +129,34 @@ export function getProcessIdentityState(identity, platform = process.platform) {
128
129
  if (query === null)
129
130
  return 'unknown';
130
131
  try {
131
- const output = execFileSync(query.command, query.args, {
132
+ const output = (runtime.execFileSync ?? execFileSync)(query.command, query.args, {
132
133
  encoding: 'utf8',
133
134
  env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
134
135
  }).trim();
135
136
  if (output.length === 0)
136
- return 'unknown';
137
+ return recheckDarwinProcessExit(identity.pid, platform, probePid);
137
138
  const startTime = platform === 'win32'
138
139
  ? new Date(output).getTime()
139
140
  : platform === 'darwin'
140
141
  ? parseDarwinStartTime(output)
141
142
  : null;
142
143
  if (startTime === null || Number.isNaN(startTime))
143
- return 'unknown';
144
+ return recheckDarwinProcessExit(identity.pid, platform, probePid);
144
145
  return startTime === identity.startTime ? 'alive' : 'dead';
145
146
  }
146
147
  catch {
148
+ return recheckDarwinProcessExit(identity.pid, platform, probePid);
149
+ }
150
+ }
151
+ function recheckDarwinProcessExit(pid, platform, probePid) {
152
+ if (platform !== 'darwin')
147
153
  return 'unknown';
154
+ try {
155
+ probePid(pid);
156
+ return 'unknown';
157
+ }
158
+ catch (error) {
159
+ return error.code === 'ESRCH' ? 'dead' : 'unknown';
148
160
  }
149
161
  }
150
162
  function parsePidRecord(raw) {
@@ -3,18 +3,19 @@ import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecyc
3
3
  import { parseArtifactName } from './artifact-name.js';
4
4
  import { resolveTaskRef } from './resolve-ref.js';
5
5
  import { getArtifactSchema } from './artifact-schema.js';
6
- import { finalizeLocalArtifact } from './local-artifact-finalization.js';
6
+ import { finalizeLocalArtifact, preflightLocalArtifact } from './local-artifact-finalization.js';
7
7
  import { initializeArtifactSkeleton } from './artifact-operations.js';
8
8
  /** One option contract for the CLI and trusted projection executor. */
9
9
  export function parseArtifactCommand(args) {
10
10
  const [taskRef, operation] = args;
11
- if (!taskRef || !['inspect', 'init', 'finalize-local'].includes(operation ?? '')) {
11
+ if (!taskRef || !['inspect', 'init', 'preflight', 'finalize-local'].includes(operation ?? '')) {
12
12
  throw new Error('task ref and a supported artifact operation are required');
13
13
  }
14
14
  const fields = {};
15
15
  const allowed = {
16
16
  inspect: ['--family'],
17
17
  init: ['--family', '--artifact', '--locale'],
18
+ 'preflight': ['--family', '--artifact', '--recovery-id'],
18
19
  'finalize-local': ['--family', '--artifact', '--recovery-id']
19
20
  }[operation];
20
21
  for (let index = 2; index < args.length; index += 1) {
@@ -72,8 +73,10 @@ export function executeArtifactCommand(command, options = {}) {
72
73
  return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
73
74
  }
74
75
  if (family !== 'analysis' && family !== 'plan' && family !== 'code') {
75
- return fail('ARTIFACT_PAYLOAD_INVALID', "finalize-local only supports 'analysis', 'plan', and 'code'");
76
+ return fail('ARTIFACT_PAYLOAD_INVALID', "preflight and finalize-local only support 'analysis', 'plan', and 'code'");
76
77
  }
78
+ if (operation === 'preflight')
79
+ return { ...preflightLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
77
80
  return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
78
81
  }
79
82
  //# sourceMappingURL=artifact-command.js.map
@@ -150,72 +150,35 @@ function inspectArtifactDirectory(taskDir, family, identity = {}) {
150
150
  };
151
151
  }
152
152
  function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
153
- let content;
154
- try {
155
- content = fs.readFileSync(review.path, 'utf8');
156
- }
157
- catch (error) {
158
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
159
- return null;
160
- }
161
- const parsed = parseReviewedInputReference(content, expectedFamily);
162
- if (!parsed) {
163
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
164
- return null;
165
- }
166
- const abs = path.join(taskDir, parsed.name);
167
153
  try {
154
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
155
+ const receipt = receiptForOutput(taskContent, review.name);
156
+ const expectedEvent = reviewEventName(review.family);
157
+ if (!receipt || receipt.event !== expectedEvent)
158
+ throw new Error(`receipt for ${review.name} is missing or invalid`);
159
+ const input = parseArtifactName(receipt.input);
160
+ if (!input || input.family !== expectedFamily)
161
+ throw new Error(`receipt input '${receipt.input}' is not a canonical ${expectedFamily} artifact`);
162
+ const abs = path.join(taskDir, input.name);
168
163
  const stat = fs.lstatSync(abs);
169
164
  if (!stat.isFile() || stat.isSymbolicLink())
170
165
  throw new Error('referenced input is not a regular file');
171
- const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
172
166
  const invalidation = parseInvalidationDocument(taskContent);
173
167
  if (!invalidation.ok)
174
168
  throw new Error(invalidation.message);
175
- if (isArtifactInvalidated(invalidation.document, expectedFamily, parsed.name)) {
176
- throw new Error(`${parsed.name} is invalidated`);
177
- }
178
- const receipt = receiptForOutput(taskContent, review.name);
179
- if (!receipt)
180
- throw new Error(`receipt for ${review.name} is missing`);
181
- const expectedEvent = reviewEventName(review.family);
182
- if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
183
- throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
184
- }
169
+ if (isArtifactInvalidated(invalidation.document, expectedFamily, input.name))
170
+ throw new Error(`${input.name} is invalidated`);
185
171
  const actualSha256 = sha256File(abs);
186
172
  if (actualSha256 !== receipt.inputSha256) {
187
- throw new Error(`${parsed.name} content digest does not match receipt`);
173
+ throw new Error(`${input.name} content digest does not match receipt`);
188
174
  }
189
- return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
175
+ return { ...input, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
190
176
  }
191
177
  catch (error) {
192
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name}: ${String(error)}`));
178
+ diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
193
179
  return null;
194
180
  }
195
181
  }
196
- function parseReviewedInputReference(content, expectedFamily) {
197
- const lines = content.split(/\r?\n/);
198
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
199
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
200
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
201
- return candidates
202
- .map((name) => parseArtifactName(name))
203
- .find((item) => item?.family === expectedFamily) ?? null;
204
- }
205
- function parseCodePlanInputReference(content) {
206
- const lines = content.split(/\r?\n/);
207
- const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
208
- if (sectionStart < 0)
209
- return null;
210
- const bodyLines = lines.slice(sectionStart + 1);
211
- const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
212
- const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
213
- const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
214
- if (!match)
215
- return null;
216
- const parsed = parseArtifactName(match[1]);
217
- return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
218
- }
219
182
  function reviewEventName(family) {
220
183
  if (family === 'review-analysis')
221
184
  return 'review-analysis.completed';
@@ -509,5 +472,5 @@ function buildArtifactLinkSection(content, artifact) {
509
472
  body = lines.join('\n').replace(/^\n+|\n+$/g, '');
510
473
  return { aliases, heading, body };
511
474
  }
512
- export { artifactFamilyCatalog, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
475
+ export { artifactFamilyCatalog, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
513
476
  //# sourceMappingURL=artifact-lifecycle.js.map
@@ -1,11 +1,12 @@
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.js';
4
+ import { readStableFileSync } from './secure-fs.js';
5
5
  import { parseArtifactName } from './artifact-name.js';
6
6
  import { scanVisibleMarkdown } from './markdown.js';
7
7
  import { getArtifactSchema, renderArtifactSkeleton } from './artifact-schema.js';
8
8
  import { withTaskExecutionLock } from './task-execution-lock.js';
9
+ import { buildQualificationAudit, expectedQualificationRelations, parseTaskQualification, renderQualificationAudit } from './qualification-audit.js';
9
10
  function sha256Content(content) {
10
11
  return createHash('sha256').update(content, 'utf8').digest('hex');
11
12
  }
@@ -193,6 +194,27 @@ function artifactRound(family, artifact) {
193
194
  const identity = parseArtifactName(artifact);
194
195
  return identity?.family === family ? identity.round : null;
195
196
  }
197
+ function qualificationAuditForInit(request) {
198
+ let taskContent;
199
+ try {
200
+ taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
201
+ }
202
+ catch (error) {
203
+ return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
204
+ }
205
+ const qualification = parseTaskQualification(taskContent);
206
+ if (!qualification.ok)
207
+ return qualification;
208
+ if (!qualification.qualification.present)
209
+ return { ok: true, content: '' };
210
+ const relations = expectedQualificationRelations(taskContent, request.family);
211
+ if (!relations.ok)
212
+ return relations;
213
+ const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
214
+ if (!audit.ok)
215
+ return audit;
216
+ return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
217
+ }
196
218
  function initializeArtifactSkeleton(request) {
197
219
  const schema = getArtifactSchema(request.family);
198
220
  if (!schema)
@@ -216,12 +238,16 @@ function initializeArtifactSkeleton(request) {
216
238
  if (error.code !== 'ENOENT')
217
239
  return resultFailure('ARTIFACT_INIT_TARGET_INVALID', String(error));
218
240
  }
219
- const content = renderArtifactSkeleton({
241
+ const skeleton = renderArtifactSkeleton({
220
242
  taskId: request.taskId,
221
243
  family: request.family,
222
244
  artifact: request.artifact,
223
245
  ...(request.locale ? { locale: request.locale } : {})
224
246
  });
247
+ const audit = qualificationAuditForInit(request);
248
+ if (!audit.ok)
249
+ return resultFailure(audit.code, audit.message);
250
+ const content = `${skeleton.trimEnd()}${audit.content}`;
225
251
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
226
252
  try {
227
253
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });