@fitlab-ai/agent-infra 0.11.0 → 0.11.2

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 (146) hide show
  1. package/dist/lib/internal/cli-route-inventory.js +1 -1
  2. package/dist/lib/internal/manual-validation.js +6 -2
  3. package/dist/lib/internal/platform-comment.js +59 -0
  4. package/dist/lib/internal/sandbox-control.js +4 -131
  5. package/dist/lib/internal/task-artifact.js +1 -1
  6. package/dist/lib/internal/task-operation-registry.js +1 -0
  7. package/dist/lib/internal/task-review.js +5 -5
  8. package/dist/lib/internal/task-validate.js +0 -6
  9. package/dist/lib/platform/comment-safety.js +7 -1
  10. package/dist/lib/platform/github-provider.js +13 -3
  11. package/dist/lib/platform/issue-comments.js +120 -13
  12. package/dist/lib/platform/issues.js +3 -4
  13. package/dist/lib/platform/provider-contract.d.ts +2 -0
  14. package/dist/lib/platform/provider-validation.js +7 -2
  15. package/dist/lib/platform/verification-sync.js +161 -156
  16. package/dist/lib/sandbox/control/executor.js +0 -38
  17. package/dist/lib/sandbox/control/finalization-response.js +10 -2
  18. package/dist/lib/sandbox/control/task-view.js +3 -1
  19. package/dist/lib/sandbox/control/workflow-executor.js +32 -41
  20. package/dist/lib/task/artifact-command.js +6 -6
  21. package/dist/lib/task/artifact-lifecycle.js +15 -52
  22. package/dist/lib/task/artifact-operations.js +27 -1
  23. package/dist/lib/task/capabilities.js +0 -2
  24. package/dist/lib/task/codex-orchestration.js +10 -208
  25. package/dist/lib/task/commit-operation.js +3 -18
  26. package/dist/lib/task/control-authority.js +1 -8
  27. package/dist/lib/task/delegation-receipts.js +14 -76
  28. package/dist/lib/task/events.js +147 -278
  29. package/dist/lib/task/finalization-state.js +2 -1
  30. package/dist/lib/task/finalization.js +273 -49
  31. package/dist/lib/task/gate-policy.js +94 -0
  32. package/dist/lib/task/lifecycle-execution.js +2 -11
  33. package/dist/lib/task/local-artifact-finalization.js +31 -245
  34. package/dist/lib/task/orchestration.js +8 -24
  35. package/dist/lib/task/review-artifacts.js +12 -6
  36. package/dist/lib/task/review-command.js +0 -1
  37. package/dist/lib/task/review-finalization.js +47 -218
  38. package/dist/lib/task/verification-engine.js +28 -17
  39. package/dist/lib/task/verification.js +25 -9
  40. package/dist/lib/task/workflow-command.js +4 -1
  41. package/dist/lib/task/workflow-dispatch.js +55 -3
  42. package/dist/lifecycle-build-manifest.json +3 -3
  43. package/lib/internal/cli-route-inventory.ts +1 -1
  44. package/lib/internal/manual-validation.ts +6 -2
  45. package/lib/internal/platform-comment.ts +38 -0
  46. package/lib/internal/sandbox-control.ts +3 -145
  47. package/lib/internal/task-artifact.ts +1 -1
  48. package/lib/internal/task-operation-registry.ts +1 -0
  49. package/lib/internal/task-review.ts +5 -5
  50. package/lib/internal/task-validate.ts +0 -5
  51. package/lib/platform/comment-safety.ts +8 -0
  52. package/lib/platform/github-provider.ts +11 -2
  53. package/lib/platform/issue-comments.ts +132 -13
  54. package/lib/platform/issues.ts +3 -4
  55. package/lib/platform/provider-contract.ts +2 -0
  56. package/lib/platform/provider-validation.ts +6 -2
  57. package/lib/platform/verification-sync.ts +178 -179
  58. package/lib/sandbox/control/executor.ts +0 -44
  59. package/lib/sandbox/control/finalization-response.ts +10 -2
  60. package/lib/sandbox/control/task-view.ts +3 -1
  61. package/lib/sandbox/control/workflow-executor.ts +31 -48
  62. package/lib/task/artifact-command.ts +6 -7
  63. package/lib/task/artifact-lifecycle.ts +12 -49
  64. package/lib/task/artifact-operations.ts +27 -1
  65. package/lib/task/capabilities.ts +0 -1
  66. package/lib/task/codex-orchestration.ts +8 -241
  67. package/lib/task/commit-operation.ts +3 -23
  68. package/lib/task/control-authority.ts +1 -8
  69. package/lib/task/delegation-receipts.ts +12 -79
  70. package/lib/task/events.ts +155 -287
  71. package/lib/task/finalization-state.ts +2 -1
  72. package/lib/task/finalization.ts +277 -57
  73. package/lib/task/gate-policy.ts +133 -0
  74. package/lib/task/lifecycle-execution.ts +10 -21
  75. package/lib/task/local-artifact-finalization.ts +29 -285
  76. package/lib/task/orchestration.ts +12 -41
  77. package/lib/task/review-artifacts.ts +8 -2
  78. package/lib/task/review-command.ts +0 -1
  79. package/lib/task/review-finalization.ts +46 -200
  80. package/lib/task/verification-engine.ts +24 -14
  81. package/lib/task/verification-types.ts +7 -0
  82. package/lib/task/verification.ts +25 -9
  83. package/lib/task/workflow-command.ts +4 -1
  84. package/lib/task/workflow-dispatch.ts +46 -3
  85. package/package.json +2 -2
  86. package/templates/.agents/rules/issue-sync.en.md +2 -0
  87. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -8
  88. package/templates/.agents/rules/lifecycle-orchestration.en.md +7 -7
  89. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +7 -7
  90. package/templates/.agents/rules/local-artifact-repair.en.md +14 -52
  91. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -52
  92. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  93. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  94. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
  95. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
  96. package/templates/.agents/skills/block-task/config/verify.json +1 -3
  97. package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
  98. package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
  99. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  100. package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
  101. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
  102. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  103. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  104. package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
  105. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
  106. package/templates/.agents/skills/commit/config/verify.json +1 -5
  107. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
  108. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
  109. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  110. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  111. package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
  112. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
  113. package/templates/.agents/skills/create-pr/config/verify.json +1 -10
  114. package/templates/.agents/skills/create-task/config/verify.json +1 -4
  115. package/templates/.agents/skills/import-issue/config/verify.json +1 -2
  116. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  117. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  118. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
  119. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
  120. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -4
  121. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  122. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  123. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
  124. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
  125. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
  126. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
  127. package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
  128. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
  129. package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
  130. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
  131. package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
  132. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
  133. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  134. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  135. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
  136. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
  137. package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
  138. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
  139. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
  140. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
  141. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
  142. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
  143. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  144. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  145. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  146. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
@@ -441,49 +441,6 @@ async function executeRequestInner(
441
441
  ? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
442
442
  : orchestrationFailure(code, message);
443
443
  }
444
- let controllerBinding: Readonly<{
445
- instanceDigest: string;
446
- controlGeneration: string;
447
- }> | null = null;
448
- if (request.family === 'task-orchestration') {
449
- const codexPrepare = operation.family === 'task-orchestration'
450
- && operation.intent === 'prepare'
451
- && operation.input.client === 'codex';
452
- if (codexPrepare) {
453
- if (!request.controllerProof) {
454
- const authority = options.lifecycleRecoveryAttestation;
455
- if (!authority || authority.phase !== 'orchestration.prepare') {
456
- return orchestrationFailure(
457
- 'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
458
- 'Codex prepare requires a current controller lease proof'
459
- );
460
- }
461
- controllerBinding = {
462
- instanceDigest: authority.controllerInstanceDigest,
463
- controlGeneration: authority.controlGeneration
464
- };
465
- } else {
466
- try {
467
- controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
468
- manifest,
469
- manifestPath,
470
- proof: request.controllerProof,
471
- buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
472
- });
473
- } catch (error) {
474
- const code = error instanceof CodexControllerRegistrationError
475
- ? error.code
476
- : 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
477
- return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
478
- }
479
- }
480
- } else if (request.controllerProof !== null) {
481
- return orchestrationFailure(
482
- 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID',
483
- 'Controller proof is only accepted for canonical Codex prepare'
484
- );
485
- }
486
- }
487
444
  const diagnosticLog: OrchestrationDiagnosticLogger = (event, fields): void => {
488
445
  appendExecutorAudit(manifest, event.startsWith('orchestration-') ? event : `orchestration-${event}`, {
489
446
  requestId: request.id,
@@ -501,7 +458,6 @@ async function executeRequestInner(
501
458
  manifestPath,
502
459
  requestId: request.id,
503
460
  diagnosticLog,
504
- ...(controllerBinding ? { controllerBinding } : {}),
505
461
  lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
506
462
  });
507
463
  const format = (value: unknown): SandboxControlExecutionResult => {
@@ -4,9 +4,15 @@ import type { SandboxControlResponse } from './protocol.ts';
4
4
  export function finalizationTerminalResponse(taskId: string, requestId: string, receipt: TaskFinalizationReceipt): SandboxControlResponse {
5
5
  const pendingSteps = [
6
6
  receipt.taskComment === 'pending' ? 'task-comment' : null,
7
- receipt.verification === 'pending' ? 'verification' : null
7
+ receipt.verification === 'pending' ? 'verification' : null,
8
+ receipt.summary === 'pending' ? 'summary' : null,
9
+ receipt.postSummaryVerification === 'pending' ? 'post-summary-verification' : null
8
10
  ].filter((step): step is string => step !== null);
9
- const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
11
+ const completedSteps = [
12
+ 'lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment',
13
+ receipt.verification === 'pending' ? null : 'verification', receipt.summary === 'pending' ? null : 'summary',
14
+ receipt.postSummaryVerification === 'pending' ? null : 'post-summary-verification'
15
+ ]
10
16
  .filter((step): step is string => step !== null);
11
17
  const warnings = receipt.warnings
12
18
  .filter((warning) => warning.status === 'open')
@@ -16,6 +22,8 @@ export function finalizationTerminalResponse(taskId: string, requestId: string,
16
22
  lifecycle: { status: 'no-op', changed: false, error: null },
17
23
  taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
18
24
  verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
25
+ summary: receipt.summary === 'pending' ? null : { status: 'no-op', changed: false, error: null },
26
+ postSummaryVerification: receipt.postSummaryVerification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
19
27
  completedSteps, pendingSteps,
20
28
  result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
21
29
  ? 'completed_with_warnings' : 'completed',
@@ -56,9 +56,11 @@ function receiptIdentity(value: unknown, generation: string, requestId?: string)
56
56
  const binding = receipt.controlBinding;
57
57
  if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return null;
58
58
  const controlBinding = binding as Record<string, unknown>;
59
- if (receipt.version !== 2 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
59
+ if (receipt.version !== 3 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
60
60
  || !Number.isSafeInteger(receipt.revision) || (receipt.revision as number) < 0
61
61
  || receipt.lifecycle !== 'done'
62
+ || !['pending', 'done', 'skipped'].includes(String(receipt.summary))
63
+ || !['pending', 'done', 'skipped'].includes(String(receipt.postSummaryVerification))
62
64
  || controlBinding.generation !== generation
63
65
  || typeof controlBinding.requestId !== 'string'
64
66
  || !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)
@@ -1,10 +1,16 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
1
5
  import { TASK_WORKFLOW_COMMANDS } from '../../task/workflow-command.ts';
2
6
 
3
7
  import { parseArtifactCommand, executeArtifactCommand } from '../../task/artifact-command.ts';
4
8
  import { parseReviewCommand } from '../../task/review-command.ts';
5
9
  import { prepareLocalArtifact, preflightLocalArtifact, commitLocalArtifactProvenance } from '../../task/local-artifact-finalization.ts';
6
- import { prepareReviewSummaryCandidate, commitReviewSummaryProvenance } from '../../task/review-finalization.ts';
7
- import { readArtifactRecoveryIntent } from '../../task/artifact-repair-intent.ts';
10
+ import {
11
+ prepareReviewSummaryCandidate,
12
+ preflightReviewSummary
13
+ } from '../../task/review-finalization.ts';
8
14
  import { withTaskExecutionLock } from '../../task/task-execution-lock.ts';
9
15
  import { dispatchWorkflowCommand } from '../../task/workflow-dispatch.ts';
10
16
  import { assertSandboxTaskSource } from '../workspace-view.ts';
@@ -33,6 +39,18 @@ function executionResult(result: Record<string, unknown>): SandboxControlExecuti
33
39
  return { exitCode: result.status === 'failed' || result.status === 'refused' ? 1 : 0, stdout: `${JSON.stringify(result)}\n`, stderr: '' };
34
40
  }
35
41
 
42
+ function replaceCurrentArtifactAtomically(file: string, content: string): void {
43
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${randomUUID()}.tmp`);
44
+ try {
45
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
46
+ fs.renameSync(temporary, file);
47
+ } finally {
48
+ try { fs.unlinkSync(temporary); } catch (error) {
49
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
50
+ }
51
+ }
52
+ }
53
+
36
54
  /** The broker already authorized this host executor; domain operations own validation. */
37
55
  export async function executeTaskWorkflow(
38
56
  manifest: SandboxControlManifest,
@@ -41,25 +59,6 @@ export async function executeTaskWorkflow(
41
59
  options: TaskWorkflowExecutionOptions = {}
42
60
  ): Promise<SandboxControlExecutionResult> {
43
61
  let publicationCommitted = false;
44
- let recoveryObservation: Readonly<{
45
- family: 'analysis' | 'plan' | 'code' | 'review-analysis' | 'review-plan' | 'review-code';
46
- artifact: string;
47
- wasPassed: boolean;
48
- }> | null = null;
49
- const observeRecoveryPublication = (): void => {
50
- if (!recoveryObservation || recoveryObservation.wasPassed) return;
51
- const intent = readArtifactRecoveryIntent(
52
- manifest.repoRoot,
53
- request.taskId,
54
- recoveryObservation.family,
55
- recoveryObservation.artifact
56
- );
57
- publicationCommitted ||= intent?.state === 'preflight-ready'
58
- || intent?.state === 'preflight-commit-started'
59
- || intent?.state === 'preflight-passed'
60
- || intent?.state === 'passed'
61
- || intent?.state === 'commit-started';
62
- };
63
62
  let faultTriggered = false;
64
63
  const fault = (window: TaskWorkflowFaultWindow): void => {
65
64
  if (faultTriggered || options.faultWindow !== window) return;
@@ -121,41 +120,26 @@ export async function executeTaskWorkflow(
121
120
  if ('operation' in input) {
122
121
  const { family } = input;
123
122
  if (family !== 'analysis' && family !== 'plan' && family !== 'code') throw new Error('ARTIFACT_IDENTITY_INVALID');
124
- const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot, recoveryId: input.recoveryId, lockAlreadyHeld: true } as const;
123
+ const local = { taskRef: request.taskId, family, artifact: input.artifact, repoRoot: manifest.repoRoot, lockAlreadyHeld: true } as const;
125
124
  if (input.operation === 'preflight') {
126
- // Preflight only seals a verified generation. It must not publish the
127
- // formal artifact, set publicationStarted, or append finalizer audit.
128
125
  const result = preflightLocalArtifact(local);
129
- recoveryObservation = { family, artifact: input.artifact, wasPassed: false };
130
- observeRecoveryPublication();
131
126
  return response(result);
132
127
  }
133
- recoveryObservation = {
134
- family,
135
- artifact: input.artifact,
136
- wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
137
- };
138
- const prepared = prepareLocalArtifact(local, content, lifecycleRecoveryAttestation ?? undefined);
139
- observeRecoveryPublication();
140
- const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
141
- if (prepared.result.status === 'failed') return executionResult(commitLocalArtifactProvenance(prepared, commitOptions));
142
- result = commitLocalArtifactProvenance(prepared, commitOptions);
128
+ const prepared = prepareLocalArtifact(local, content);
129
+ result = commitLocalArtifactProvenance(prepared);
143
130
  } else {
144
131
  if (input.overrideTicket) throw new Error('TASK_WORKFLOW_OVERRIDE_UNSUPPORTED');
145
132
  const family = `review-${input.stage}` as 'review-analysis' | 'review-plan' | 'review-code';
146
- recoveryObservation = {
147
- family,
148
- artifact: input.artifact,
149
- wasPassed: readArtifactRecoveryIntent(manifest.repoRoot, request.taskId, family, input.artifact)?.state === 'passed'
150
- };
151
- const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true, startRecovery: true });
152
- observeRecoveryPublication();
133
+ if (request.operation === 'review-preflight') {
134
+ const result = preflightReviewSummary(input, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
135
+ return response(result);
136
+ }
137
+ const prepared = prepareReviewSummaryCandidate(input, content, { repoRoot: manifest.repoRoot, lockAlreadyHeld: true });
153
138
  if (input.dryRun || prepared.result.status === 'planned') return executionResult(prepared.result);
154
- const commitOptions = { afterPublish: () => fault('after-atomic-rename') };
155
- if (prepared.result.status === 'failed') return executionResult(commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions));
156
- result = commitReviewSummaryProvenance(prepared, manifest.repoRoot, commitOptions);
139
+ if (prepared.result.status === 'failed') return executionResult(prepared.result);
140
+ if (prepared.result.changed) replaceCurrentArtifactAtomically(path.join(taskDir, input.artifact), prepared.content);
141
+ result = prepared.result;
157
142
  }
158
- observeRecoveryPublication();
159
143
  publicationCommitted ||= result.changed === true;
160
144
  if (result.status !== 'failed') appendDiagnosticAudit(manifest, 'task-workflow-artifact-finalized', {
161
145
  requestId: request.id, sandboxTaskId: request.taskId, workflowOperation: request.operation,
@@ -165,7 +149,6 @@ export async function executeTaskWorkflow(
165
149
  return response(result);
166
150
  });
167
151
  } catch (error) {
168
- observeRecoveryPublication();
169
152
  const message = error instanceof Error ? error.message : String(error);
170
153
  const code = (error as { code?: string })?.code ?? /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_WORKFLOW_REQUEST_INVALID';
171
154
  return executionResult({ status: 'failed', changed: publicationCommitted ? null : false, error: { code, message } });
@@ -13,7 +13,6 @@ export type ArtifactCommand = Readonly<{
13
13
  family: string;
14
14
  artifact: string;
15
15
  locale?: 'zh-CN' | 'en';
16
- recoveryId?: string;
17
16
  }>;
18
17
 
19
18
  /** One option contract for the CLI and trusted projection executor. */
@@ -26,8 +25,8 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
26
25
  const allowed = {
27
26
  inspect: ['--family'],
28
27
  init: ['--family', '--artifact', '--locale'],
29
- 'preflight': ['--family', '--artifact', '--recovery-id'],
30
- 'finalize-local': ['--family', '--artifact', '--recovery-id']
28
+ 'preflight': ['--family', '--artifact'],
29
+ 'finalize-local': ['--family', '--artifact']
31
30
  }[operation as ArtifactCommand['operation']];
32
31
  for (let index = 2; index < args.length; index += 1) {
33
32
  const flag = args[index]!;
@@ -43,7 +42,7 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
43
42
  if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
44
43
  return {
45
44
  taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
46
- artifact: fields['--artifact'] ?? '', locale, recoveryId: fields['--recovery-id']
45
+ artifact: fields['--artifact'] ?? '', locale
47
46
  };
48
47
  }
49
48
  /** Resolve authoritative task metadata separately from a trusted candidate directory. */
@@ -51,7 +50,7 @@ export function executeArtifactCommand(
51
50
  command: ArtifactCommand,
52
51
  options: Readonly<{ repoRoot?: string; artifactDir?: string }> = {}
53
52
  ): Record<string, unknown> {
54
- const { taskRef, operation, family, artifact, locale, recoveryId } = command;
53
+ const { taskRef, operation, family, artifact, locale } = command;
55
54
  const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
56
55
  if (operation === 'inspect') {
57
56
  const result = resolveArtifactContext(taskRef, family, { repoRoot: options.repoRoot });
@@ -82,6 +81,6 @@ export function executeArtifactCommand(
82
81
  if (family !== 'analysis' && family !== 'plan' && family !== 'code') {
83
82
  return fail('ARTIFACT_PAYLOAD_INVALID', "preflight and finalize-local only support 'analysis', 'plan', and 'code'");
84
83
  }
85
- if (operation === 'preflight') return { ...preflightLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
86
- return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
84
+ if (operation === 'preflight') return { ...preflightLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot }) };
85
+ return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot }) };
87
86
  }
@@ -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 {
@@ -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' });
@@ -187,7 +187,6 @@ function qualificationRecoveryAction(facts: LifecycleFacts): LifecycleAction | n
187
187
  }
188
188
 
189
189
  function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
190
- if (invalidationBlocks(facts.invalidation)) return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
191
190
  if (facts.qualificationStale) {
192
191
  const action = qualificationRecoveryAction(facts);
193
192
  return {