@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.
- package/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +6 -2
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-operation-registry.js +22 -12
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +10 -5
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +84 -32
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +184 -162
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +90 -22
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +28 -2
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +66 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +85 -18
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +198 -11
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +2 -0
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +6 -2
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/internal/task-operation-registry.ts +22 -12
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +9 -5
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +98 -31
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +203 -188
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +1 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +113 -22
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +28 -2
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +55 -38
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +91 -15
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +198 -9
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +2 -0
- package/lib/task/workflow-dispatch.ts +171 -0
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +11 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- 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'
|
|
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 '../../
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
result
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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(`${
|
|
239
|
+
throw new Error(`${input.name} content digest does not match receipt`);
|
|
254
240
|
}
|
|
255
|
-
return { ...
|
|
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,
|
|
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
|
-
|
|
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 '
|
|
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
|
|
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' });
|