@fitlab-ai/agent-infra 0.9.15 → 0.11.0
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/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +17 -8
- 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 +4 -4
- package/dist/lib/internal/task-operation-registry.js +22 -13
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +12 -7
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +89 -31
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +45 -7
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +87 -35
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +11 -30
- package/dist/lib/task/artifact-operations.js +17 -231
- package/dist/lib/task/artifact-recovery.js +476 -0
- package/dist/lib/task/artifact-repair-intent.js +90 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +39 -8
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +165 -23
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +91 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +212 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +3 -2
- package/dist/lib/task/review-finalization.js +294 -74
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +2 -1
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +17 -8
- 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 +5 -4
- package/lib/internal/task-operation-registry.ts +22 -13
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +11 -7
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +105 -30
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +48 -11
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +6 -1
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +106 -35
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +12 -29
- package/lib/task/artifact-operations.ts +22 -252
- package/lib/task/artifact-recovery.ts +572 -0
- package/lib/task/artifact-repair-intent.ts +126 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +48 -15
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +188 -24
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +66 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +249 -131
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +2 -1
- package/lib/task/review-finalization.ts +298 -84
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +2 -1
- package/lib/task/workflow-dispatch.ts +171 -0
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +12 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
- package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- 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
|
@@ -23,7 +23,7 @@ export type SandboxCleanupTarget = Readonly<{
|
|
|
23
23
|
requestedRef: string;
|
|
24
24
|
branch: string;
|
|
25
25
|
workspace: SandboxWorkspaceKey;
|
|
26
|
-
taskState: TaskWorkspaceState | 'branch-only';
|
|
26
|
+
taskState: TaskWorkspaceState | 'branch-only' | 'unknown';
|
|
27
27
|
}>;
|
|
28
28
|
|
|
29
29
|
export type SandboxContainerWorkspaceIdentity =
|
|
@@ -147,14 +147,30 @@ export function resolveSandboxReentryContext(params: Readonly<{
|
|
|
147
147
|
export function resolveSandboxCleanupTarget(
|
|
148
148
|
requestedRef: string,
|
|
149
149
|
repoRoot: string,
|
|
150
|
-
options: Readonly<{
|
|
150
|
+
options: Readonly<{
|
|
151
|
+
allowProtected?: boolean;
|
|
152
|
+
resolveMissingTask?: (taskId: string) => string | null;
|
|
153
|
+
}> = {}
|
|
151
154
|
): SandboxCleanupTarget {
|
|
152
155
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
153
156
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
if (TASK_ID_RE.test(requestedRef)) {
|
|
157
|
-
|
|
160
|
+
let task: ReturnType<typeof resolveTaskWorkspace>;
|
|
161
|
+
try {
|
|
162
|
+
task = resolveTaskWorkspace(requestedRef, repoRoot);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!(error instanceof Error && error.message === `Task not found: ${requestedRef}`)) throw error;
|
|
165
|
+
const branch = options.resolveMissingTask?.(requestedRef);
|
|
166
|
+
if (!branch) throw error;
|
|
167
|
+
return {
|
|
168
|
+
requestedRef,
|
|
169
|
+
branch,
|
|
170
|
+
workspace: { mode: 'task-bound', taskId: requestedRef },
|
|
171
|
+
taskState: 'unknown'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
158
174
|
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
159
175
|
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
|
|
160
176
|
}
|
|
@@ -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
|
|
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { readStableFileSync } from '../host-control/secure-fs.ts';
|
|
4
2
|
|
|
5
3
|
import { resolveArtifactContext, hasOpenArtifactRound } from './artifact-lifecycle.ts';
|
|
6
4
|
import { parseArtifactName } from './artifact-name.ts';
|
|
7
5
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
8
6
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
9
|
-
import { finalizeLocalArtifact } from './local-artifact-finalization.ts';
|
|
10
|
-
import {
|
|
7
|
+
import { finalizeLocalArtifact, preflightLocalArtifact } from './local-artifact-finalization.ts';
|
|
8
|
+
import { initializeArtifactSkeleton } from './artifact-operations.ts';
|
|
11
9
|
|
|
12
10
|
export type ArtifactCommand = Readonly<{
|
|
13
11
|
taskRef: string;
|
|
14
|
-
operation: 'inspect' | 'init' | '
|
|
12
|
+
operation: 'inspect' | 'init' | 'preflight' | 'finalize-local';
|
|
15
13
|
family: string;
|
|
16
14
|
artifact: string;
|
|
17
15
|
locale?: 'zh-CN' | 'en';
|
|
18
|
-
|
|
19
|
-
expectedSemanticDigest: string;
|
|
16
|
+
recoveryId?: string;
|
|
20
17
|
}>;
|
|
21
18
|
|
|
22
19
|
/** One option contract for the CLI and trusted projection executor. */
|
|
23
20
|
export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
24
21
|
const [taskRef, operation] = args;
|
|
25
|
-
if (!taskRef || !['inspect', 'init', '
|
|
22
|
+
if (!taskRef || !['inspect', 'init', 'preflight', 'finalize-local'].includes(operation ?? '')) {
|
|
26
23
|
throw new Error('task ref and a supported artifact operation are required');
|
|
27
24
|
}
|
|
28
25
|
const fields: Record<string, string> = {};
|
|
29
26
|
const allowed = {
|
|
30
27
|
inspect: ['--family'],
|
|
31
28
|
init: ['--family', '--artifact', '--locale'],
|
|
32
|
-
|
|
33
|
-
'finalize-local': ['--family', '--artifact']
|
|
29
|
+
'preflight': ['--family', '--artifact', '--recovery-id'],
|
|
30
|
+
'finalize-local': ['--family', '--artifact', '--recovery-id']
|
|
34
31
|
}[operation as ArtifactCommand['operation']];
|
|
35
32
|
for (let index = 2; index < args.length; index += 1) {
|
|
36
33
|
const flag = args[index]!;
|
|
@@ -44,13 +41,9 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
|
44
41
|
if (operation !== 'inspect' && !fields['--artifact']) throw new Error("option '--artifact' is required");
|
|
45
42
|
const locale = fields['--locale'];
|
|
46
43
|
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
47
|
-
if (operation === 'repair' && !['--expected-sha256', '--expected-semantic-digest'].every((flag) => /^[a-f0-9]{64}$/u.test(fields[flag] ?? ''))) {
|
|
48
|
-
throw new Error('repair requires lowercase 64-character digests');
|
|
49
|
-
}
|
|
50
44
|
return {
|
|
51
45
|
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
|
|
52
|
-
artifact: fields['--artifact'] ?? '', locale,
|
|
53
|
-
expectedSha256: fields['--expected-sha256'] ?? '', expectedSemanticDigest: fields['--expected-semantic-digest'] ?? ''
|
|
46
|
+
artifact: fields['--artifact'] ?? '', locale, recoveryId: fields['--recovery-id']
|
|
54
47
|
};
|
|
55
48
|
}
|
|
56
49
|
/** Resolve authoritative task metadata separately from a trusted candidate directory. */
|
|
@@ -58,7 +51,7 @@ export function executeArtifactCommand(
|
|
|
58
51
|
command: ArtifactCommand,
|
|
59
52
|
options: Readonly<{ repoRoot?: string; artifactDir?: string }> = {}
|
|
60
53
|
): Record<string, unknown> {
|
|
61
|
-
const { taskRef, operation, family, artifact, locale,
|
|
54
|
+
const { taskRef, operation, family, artifact, locale, recoveryId } = command;
|
|
62
55
|
const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
|
|
63
56
|
if (operation === 'inspect') {
|
|
64
57
|
const result = resolveArtifactContext(taskRef, family, { repoRoot: options.repoRoot });
|
|
@@ -86,19 +79,9 @@ export function executeArtifactCommand(
|
|
|
86
79
|
}
|
|
87
80
|
return { ...initializeArtifactSkeleton({ ...input, ...(locale ? { locale } : {}) }), ...identity };
|
|
88
81
|
}
|
|
89
|
-
if (operation === 'repair') {
|
|
90
|
-
let content: string;
|
|
91
|
-
try { content = readStableFileSync(path.join(taskDir, artifact), { maxBytes: 1024 * 1024 }).bytes.toString('utf8'); }
|
|
92
|
-
catch (error) { return fail('ARTIFACT_REPAIR_TARGET_INVALID', String(error)); }
|
|
93
|
-
const inspection = inspectArtifactStructure(content, schema);
|
|
94
|
-
if (!inspection.repair) {
|
|
95
|
-
return { ...fail('ARTIFACT_REPAIR_UNSAFE', inspection.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ') || 'no deterministic structural repair is available'),
|
|
96
|
-
artifact, family, diagnostics: inspection.diagnostics };
|
|
97
|
-
}
|
|
98
|
-
return { ...applyArtifactRepair({ ...input, expectedSha256, expectedSemanticDigest, operation: inspection.repair }), ...identity };
|
|
99
|
-
}
|
|
100
82
|
if (family !== 'analysis' && family !== 'plan' && family !== 'code') {
|
|
101
|
-
return fail('ARTIFACT_PAYLOAD_INVALID', "finalize-local only
|
|
83
|
+
return fail('ARTIFACT_PAYLOAD_INVALID', "preflight and finalize-local only support 'analysis', 'plan', and 'code'");
|
|
102
84
|
}
|
|
103
|
-
return { ...
|
|
85
|
+
if (operation === 'preflight') return { ...preflightLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
|
|
86
|
+
return { ...finalizeLocalArtifact({ taskRef, family, artifact, repoRoot: resolved.repoRoot, recoveryId }) };
|
|
104
87
|
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { parseArtifactName } from './artifact-name.ts';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { readStableFileSync } from '
|
|
4
|
+
import { readStableFileSync } from './secure-fs.ts';
|
|
5
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
6
6
|
|
|
7
7
|
import { scanVisibleMarkdown, type VisibleMarkdown, type VisibleHeading } from './markdown.ts';
|
|
8
|
-
import { hasOpenArtifactRound } from './artifact-lifecycle.ts';
|
|
9
|
-
import { readArtifactRepairIntent } from './artifact-repair-intent.ts';
|
|
10
|
-
import { resolveTaskRef } from './resolve-ref.ts';
|
|
11
8
|
import {
|
|
12
9
|
getArtifactSchema,
|
|
13
10
|
renderArtifactSkeleton
|
|
14
11
|
} from './artifact-schema.ts';
|
|
12
|
+
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
15
13
|
import type {
|
|
16
14
|
ArtifactSchema,
|
|
17
15
|
ArtifactSchemaFamily,
|
|
18
16
|
ArtifactSection
|
|
19
17
|
} from './artifact-schema.ts';
|
|
20
|
-
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
21
|
-
|
|
22
|
-
type ArtifactRepairOperation = Readonly<{
|
|
23
|
-
kind: 'replace-line' | 'insert-section';
|
|
24
|
-
sectionId: string;
|
|
25
|
-
line: number;
|
|
26
|
-
from: string;
|
|
27
|
-
to: string;
|
|
28
|
-
start: number;
|
|
29
|
-
end: number;
|
|
30
|
-
}>;
|
|
31
18
|
|
|
32
19
|
type ArtifactStructuralDiagnosticCode =
|
|
33
20
|
| 'ARTIFACT_EMPTY'
|
|
@@ -47,8 +34,6 @@ type ArtifactStructuralDiagnostic = Readonly<{
|
|
|
47
34
|
message: string;
|
|
48
35
|
sectionId: string | null;
|
|
49
36
|
line: number | null;
|
|
50
|
-
repairable: boolean;
|
|
51
|
-
operation?: ArtifactRepairOperation;
|
|
52
37
|
}>;
|
|
53
38
|
|
|
54
39
|
type ArtifactStructureResult = Readonly<{
|
|
@@ -56,7 +41,6 @@ type ArtifactStructureResult = Readonly<{
|
|
|
56
41
|
family: ArtifactSchemaFamily;
|
|
57
42
|
semanticDigest: string;
|
|
58
43
|
diagnostics: readonly ArtifactStructuralDiagnostic[];
|
|
59
|
-
repair: ArtifactRepairOperation | null;
|
|
60
44
|
}>;
|
|
61
45
|
|
|
62
46
|
type ArtifactFileResult = Readonly<{
|
|
@@ -64,21 +48,9 @@ type ArtifactFileResult = Readonly<{
|
|
|
64
48
|
changed: boolean;
|
|
65
49
|
artifactSha256: string | null;
|
|
66
50
|
semanticDigest: string | null;
|
|
67
|
-
operation: ArtifactRepairOperation | null;
|
|
68
51
|
error: { code: string; message: string } | null;
|
|
69
52
|
}>;
|
|
70
53
|
|
|
71
|
-
type ArtifactRepairRequest = Readonly<{
|
|
72
|
-
repoRoot: string;
|
|
73
|
-
taskId: string;
|
|
74
|
-
taskDir: string;
|
|
75
|
-
family: ArtifactSchemaFamily;
|
|
76
|
-
artifact: string;
|
|
77
|
-
expectedSha256: string;
|
|
78
|
-
expectedSemanticDigest: string;
|
|
79
|
-
operation: ArtifactRepairOperation;
|
|
80
|
-
}>;
|
|
81
|
-
|
|
82
54
|
type ArtifactInitRequest = Readonly<{
|
|
83
55
|
repoRoot: string;
|
|
84
56
|
taskId: string;
|
|
@@ -147,71 +119,17 @@ function diagnostic(
|
|
|
147
119
|
code: ArtifactStructuralDiagnosticCode,
|
|
148
120
|
message: string,
|
|
149
121
|
sectionId: string | null,
|
|
150
|
-
line: number | null
|
|
151
|
-
repairable = false,
|
|
152
|
-
operation?: ArtifactRepairOperation
|
|
122
|
+
line: number | null
|
|
153
123
|
): ArtifactStructuralDiagnostic {
|
|
154
|
-
return {
|
|
155
|
-
code, message, sectionId, line, repairable,
|
|
156
|
-
...(operation ? { operation } : {})
|
|
157
|
-
};
|
|
124
|
+
return { code, message, sectionId, line };
|
|
158
125
|
}
|
|
159
126
|
|
|
160
127
|
function sectionHeadings(schema: ArtifactSchema, section: ArtifactSection): readonly string[] {
|
|
161
128
|
return [section.headings.zh, section.headings.en];
|
|
162
129
|
}
|
|
163
130
|
|
|
164
|
-
function
|
|
165
|
-
content: string,
|
|
166
|
-
section: ArtifactSection,
|
|
167
|
-
heading: { start: number; end: number; text: string }
|
|
168
|
-
): ArtifactRepairOperation {
|
|
169
|
-
const to = heading.text.slice(0, -1);
|
|
170
|
-
return {
|
|
171
|
-
kind: 'replace-line',
|
|
172
|
-
sectionId: section.id,
|
|
173
|
-
line: lineNumber(content, heading.start),
|
|
174
|
-
from: heading.text,
|
|
175
|
-
to,
|
|
176
|
-
start: heading.start,
|
|
177
|
-
end: heading.end
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function createSectionInsertionRepair(
|
|
182
|
-
content: string,
|
|
183
|
-
section: ArtifactSection,
|
|
184
|
-
marker: { start: number; line: number },
|
|
185
|
-
locale: 'zh-CN' | 'en'
|
|
186
|
-
): ArtifactRepairOperation {
|
|
187
|
-
return {
|
|
188
|
-
kind: 'insert-section',
|
|
189
|
-
sectionId: section.id,
|
|
190
|
-
line: marker.line,
|
|
191
|
-
from: '',
|
|
192
|
-
to: `## ${section.headings[locale === 'en' ? 'en' : 'zh']}\n`,
|
|
193
|
-
start: marker.start,
|
|
194
|
-
end: marker.start
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function applyRepairOperation(content: string, operation: ArtifactRepairOperation): string | null {
|
|
199
|
-
if (operation.start < 0 || operation.end < operation.start || operation.end > content.length) return null;
|
|
200
|
-
if (operation.kind === 'insert-section') {
|
|
201
|
-
if (operation.from !== '' || operation.start !== operation.end || !operation.to) return null;
|
|
202
|
-
return `${content.slice(0, operation.start)}${operation.to}${content.slice(operation.end)}`;
|
|
203
|
-
}
|
|
204
|
-
const raw = content.slice(operation.start, operation.end);
|
|
205
|
-
const offset = raw.indexOf(operation.from);
|
|
206
|
-
if (!operation.from || offset < 0 || raw.indexOf(operation.from, offset + operation.from.length) >= 0) return null;
|
|
207
|
-
return `${content.slice(0, operation.start)}${raw.slice(0, offset)}${operation.to}${raw.slice(offset + operation.from.length)}${content.slice(operation.end)}`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function canonicalContent(content: string, operation: ArtifactRepairOperation | null = null): string {
|
|
131
|
+
function canonicalContent(content: string): string {
|
|
211
132
|
let normalized = content;
|
|
212
|
-
if (operation) {
|
|
213
|
-
normalized = applyRepairOperation(normalized, operation) ?? normalized;
|
|
214
|
-
}
|
|
215
133
|
normalized = normalized.replace(/\r\n/g, '\n');
|
|
216
134
|
normalized = normalized.replace(/^<!--\s*artifact-context:[^\n]+-->\s*\n?/gm, '');
|
|
217
135
|
normalized = normalized.replace(/^\s*<!--\s*artifact-section:[^\n]+-->\s*\n?/gm, '');
|
|
@@ -219,8 +137,8 @@ function canonicalContent(content: string, operation: ArtifactRepairOperation |
|
|
|
219
137
|
return normalized;
|
|
220
138
|
}
|
|
221
139
|
|
|
222
|
-
function canonicalSemanticDigest(content: string
|
|
223
|
-
return sha256Content(canonicalContent(content
|
|
140
|
+
function canonicalSemanticDigest(content: string): string {
|
|
141
|
+
return sha256Content(canonicalContent(content));
|
|
224
142
|
}
|
|
225
143
|
|
|
226
144
|
function inspectArtifactStructure(
|
|
@@ -229,7 +147,6 @@ function inspectArtifactStructure(
|
|
|
229
147
|
): ArtifactStructureResult {
|
|
230
148
|
const diagnostics: ArtifactStructuralDiagnostic[] = [];
|
|
231
149
|
const scanned = scanVisibleMarkdown(content);
|
|
232
|
-
const repairCandidates: ArtifactRepairOperation[] = [];
|
|
233
150
|
if (!content.trim()) diagnostics.push(diagnostic('ARTIFACT_EMPTY', 'artifact is empty', null, null));
|
|
234
151
|
if (scanned.hasUnclosedFence) diagnostics.push(diagnostic('ARTIFACT_UNCLOSED_FENCE', 'artifact contains an unclosed Markdown fence', null, null));
|
|
235
152
|
|
|
@@ -238,7 +155,7 @@ function inspectArtifactStructure(
|
|
|
238
155
|
const aliases = sectionHeadings(schema, section);
|
|
239
156
|
const exact = scanned.headings.filter((heading) => heading.level === 2 && aliases.includes(heading.text));
|
|
240
157
|
const punctuated = scanned.headings.filter((heading) => heading.level === 2 && aliases.some((alias) => heading.text === `${alias}:` || heading.text === `${alias}:`));
|
|
241
|
-
headingsBySection.set(section.id, exact.length
|
|
158
|
+
headingsBySection.set(section.id, exact.length > 0 ? exact : punctuated);
|
|
242
159
|
if (exact.length > 1 || punctuated.length > 0 && exact.length > 0 || punctuated.length > 1) {
|
|
243
160
|
const first = exact[1] ?? punctuated[0] ?? exact[0];
|
|
244
161
|
diagnostics.push(diagnostic(
|
|
@@ -248,16 +165,18 @@ function inspectArtifactStructure(
|
|
|
248
165
|
first ? lineNumber(content, first.start) : null
|
|
249
166
|
));
|
|
250
167
|
} else if (exact.length === 0 && punctuated.length === 1) {
|
|
251
|
-
|
|
168
|
+
diagnostics.push(diagnostic(
|
|
169
|
+
'ARTIFACT_HEADING_TRAILING_PUNCTUATION',
|
|
170
|
+
`visible required H2 '${punctuated[0]!.text}' has trailing punctuation`,
|
|
171
|
+
section.id,
|
|
172
|
+
lineNumber(content, punctuated[0]!.start)
|
|
173
|
+
));
|
|
252
174
|
} else if (exact.length === 0) {
|
|
253
175
|
diagnostics.push(diagnostic('ARTIFACT_MISSING_SECTION', `required section '${aliases[0]}' is missing`, section.id, null));
|
|
254
176
|
}
|
|
255
177
|
}
|
|
256
178
|
|
|
257
179
|
const markers = markerEntries(content, scanned);
|
|
258
|
-
const englishHeadings = scanned.headings.filter((heading) => heading.level === 2 && schema.sections.some((section) => section.headings.en === heading.text)).length;
|
|
259
|
-
const chineseHeadings = scanned.headings.filter((heading) => heading.level === 2 && schema.sections.some((section) => section.headings.zh === heading.text)).length;
|
|
260
|
-
const locale = englishHeadings > chineseHeadings ? 'en' : 'zh-CN';
|
|
261
180
|
for (const section of schema.sections) {
|
|
262
181
|
const matches = markers.filter((entry) => entry.marker === section.marker);
|
|
263
182
|
if (matches.length === 0) {
|
|
@@ -281,9 +200,7 @@ function inspectArtifactStructure(
|
|
|
281
200
|
if (marker && !heading && marker.standalone) {
|
|
282
201
|
const nextHeading = scanned.headings.find((candidate) => candidate.start > marker.start && candidate.level <= 2);
|
|
283
202
|
const body = stripHtmlComments(content.slice(marker.end, nextHeading?.start ?? content.length)).trim();
|
|
284
|
-
if (body
|
|
285
|
-
repairCandidates.push(createSectionInsertionRepair(content, section, marker, locale));
|
|
286
|
-
} else if (!body) {
|
|
203
|
+
if (!body) {
|
|
287
204
|
diagnostics.push(diagnostic('ARTIFACT_EMPTY_SECTION', `section '${section.headings.zh}' has no semantic body`, section.id, marker.line));
|
|
288
205
|
}
|
|
289
206
|
}
|
|
@@ -294,32 +211,6 @@ function inspectArtifactStructure(
|
|
|
294
211
|
}
|
|
295
212
|
}
|
|
296
213
|
|
|
297
|
-
for (const candidate of repairCandidates) {
|
|
298
|
-
if (candidate.kind === 'replace-line') {
|
|
299
|
-
diagnostics.push(diagnostic(
|
|
300
|
-
'ARTIFACT_HEADING_TRAILING_PUNCTUATION',
|
|
301
|
-
`visible required H2 '${candidate.from}' may be normalized to '${candidate.to}'`,
|
|
302
|
-
candidate.sectionId,
|
|
303
|
-
candidate.line,
|
|
304
|
-
true,
|
|
305
|
-
candidate
|
|
306
|
-
));
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const missing = diagnostics.findIndex((item) => item.code === 'ARTIFACT_MISSING_SECTION' && item.sectionId === candidate.sectionId);
|
|
310
|
-
if (missing >= 0) {
|
|
311
|
-
const section = schema.sections.find((item) => item.id === candidate.sectionId)!;
|
|
312
|
-
diagnostics.splice(missing, 1, diagnostic(
|
|
313
|
-
'ARTIFACT_MISSING_SECTION',
|
|
314
|
-
`required section '${section.headings.zh}' is missing and can be inserted at its unique marker`,
|
|
315
|
-
candidate.sectionId,
|
|
316
|
-
candidate.line,
|
|
317
|
-
true,
|
|
318
|
-
candidate
|
|
319
|
-
));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
214
|
const orderedHeadings = schema.sections.flatMap((section) => {
|
|
324
215
|
const heading = headingsBySection.get(section.id)?.[0];
|
|
325
216
|
return heading ? [{ section, start: heading.start }] : [];
|
|
@@ -335,16 +226,11 @@ function inspectArtifactStructure(
|
|
|
335
226
|
diagnostics.push(diagnostic('ARTIFACT_SECTION_ORDER_INVALID', 'section markers are not in schema order', null, null));
|
|
336
227
|
}
|
|
337
228
|
|
|
338
|
-
const repair = repairCandidates.length === 1 && (
|
|
339
|
-
diagnostics.length === 0 ||
|
|
340
|
-
(diagnostics.length === 1 && diagnostics[0]?.operation?.kind === repairCandidates[0]?.kind)
|
|
341
|
-
) ? repairCandidates[0]! : null;
|
|
342
229
|
return {
|
|
343
230
|
ok: diagnostics.length === 0,
|
|
344
231
|
family: schema.family,
|
|
345
|
-
semanticDigest: canonicalSemanticDigest(content
|
|
346
|
-
diagnostics
|
|
347
|
-
repair
|
|
232
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
233
|
+
diagnostics
|
|
348
234
|
};
|
|
349
235
|
}
|
|
350
236
|
|
|
@@ -364,8 +250,7 @@ function inspectArtifactPatterns(
|
|
|
364
250
|
ok: diagnostics.length === 0,
|
|
365
251
|
family: schema.family,
|
|
366
252
|
semanticDigest: canonicalSemanticDigest(content),
|
|
367
|
-
diagnostics
|
|
368
|
-
repair: null
|
|
253
|
+
diagnostics
|
|
369
254
|
};
|
|
370
255
|
}
|
|
371
256
|
|
|
@@ -376,18 +261,16 @@ function inspectArtifactContract(
|
|
|
376
261
|
const structure = inspectArtifactStructure(content, schema);
|
|
377
262
|
const patterns = inspectArtifactPatterns(content, schema);
|
|
378
263
|
const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
|
|
379
|
-
const repair = patterns.diagnostics.length === 0 ? structure.repair : null;
|
|
380
264
|
return {
|
|
381
265
|
ok: diagnostics.length === 0,
|
|
382
266
|
family: schema.family,
|
|
383
|
-
semanticDigest: canonicalSemanticDigest(content
|
|
384
|
-
diagnostics
|
|
385
|
-
repair
|
|
267
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
268
|
+
diagnostics
|
|
386
269
|
};
|
|
387
270
|
}
|
|
388
271
|
|
|
389
272
|
function resultFailure(code: string, message: string): ArtifactFileResult {
|
|
390
|
-
return { status: 'failed', changed: false, artifactSha256: null, semanticDigest: null,
|
|
273
|
+
return { status: 'failed', changed: false, artifactSha256: null, semanticDigest: null, error: { code, message } };
|
|
391
274
|
}
|
|
392
275
|
|
|
393
276
|
function resultNoOp(content: string): ArtifactFileResult {
|
|
@@ -396,124 +279,15 @@ function resultNoOp(content: string): ArtifactFileResult {
|
|
|
396
279
|
changed: false,
|
|
397
280
|
artifactSha256: sha256Content(content),
|
|
398
281
|
semanticDigest: canonicalSemanticDigest(content),
|
|
399
|
-
operation: null,
|
|
400
282
|
error: null
|
|
401
283
|
};
|
|
402
284
|
}
|
|
403
285
|
|
|
404
|
-
function validateRepairContext(request: ArtifactRepairRequest, content: string): { ok: true } | { ok: false; code: string; message: string } {
|
|
405
|
-
const round = artifactRound(request.family, request.artifact);
|
|
406
|
-
if (round === null) return { ok: false, code: 'ARTIFACT_REPAIR_CONTEXT_INVALID', message: 'artifact round is not canonical' };
|
|
407
|
-
const contextPattern = /^<!--\s*artifact-context:([^:\s]+):([^:\s]+):(\d+)\s*-->\s*$/;
|
|
408
|
-
const contexts = scanVisibleMarkdown(content).lines
|
|
409
|
-
.map((line) => line.text.trim().match(contextPattern))
|
|
410
|
-
.filter((match): match is RegExpMatchArray => match !== null);
|
|
411
|
-
if (contexts.length !== 1
|
|
412
|
-
|| contexts[0]![1] !== request.taskId
|
|
413
|
-
|| contexts[0]![2] !== request.family
|
|
414
|
-
|| Number(contexts[0]![3]) !== round) {
|
|
415
|
-
return { ok: false, code: 'ARTIFACT_REPAIR_CONTEXT_INVALID', message: 'artifact context marker does not match the requested task, family, and round' };
|
|
416
|
-
}
|
|
417
|
-
let taskContent: string;
|
|
418
|
-
const resolved = resolveTaskRef(request.taskId, { repoRoot: request.repoRoot });
|
|
419
|
-
if (!resolved.ok) return { ok: false, code: 'ARTIFACT_REPAIR_CONTEXT_INVALID', message: resolved.message };
|
|
420
|
-
try { taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
|
|
421
|
-
catch (error) { return { ok: false, code: 'ARTIFACT_REPAIR_CONTEXT_INVALID', message: `cannot read task lifecycle context: ${String(error)}` }; }
|
|
422
|
-
if (!hasOpenArtifactRound(taskContent, request.family, round)) {
|
|
423
|
-
return { ok: false, code: 'ARTIFACT_REPAIR_CONTEXT_INVALID', message: `artifact '${request.artifact}' does not have exactly one matching open started lifecycle event` };
|
|
424
|
-
}
|
|
425
|
-
let intent;
|
|
426
|
-
try { intent = readArtifactRepairIntent(request.repoRoot, request.taskId, request.family, request.artifact); }
|
|
427
|
-
catch (error) { return { ok: false, code: 'ARTIFACT_REPAIR_PROVENANCE_INVALID', message: String(error) }; }
|
|
428
|
-
if (!intent || intent.state !== 'awaiting-repair') {
|
|
429
|
-
return { ok: false, code: 'ARTIFACT_REPAIR_PROVENANCE_INVALID', message: 'artifact has no awaiting-repair finalization provenance' };
|
|
430
|
-
}
|
|
431
|
-
if (intent.artifactSha256 !== request.expectedSha256 || intent.semanticDigest !== request.expectedSemanticDigest) {
|
|
432
|
-
return { ok: false, code: 'ARTIFACT_REPAIR_PROVENANCE_INVALID', message: 'repair request does not match the current finalization provenance' };
|
|
433
|
-
}
|
|
434
|
-
return { ok: true };
|
|
435
|
-
}
|
|
436
|
-
|
|
437
286
|
function artifactRound(family: ArtifactSchemaFamily, artifact: string): number | null {
|
|
438
287
|
const identity = parseArtifactName(artifact);
|
|
439
288
|
return identity?.family === family ? identity.round : null;
|
|
440
289
|
}
|
|
441
290
|
|
|
442
|
-
function validateTarget(taskDir: string, family: ArtifactSchemaFamily, artifact: string): { path: string } | ArtifactFileResult {
|
|
443
|
-
if (path.basename(artifact) !== artifact || artifactRound(family, artifact) === null || !artifact.endsWith('.md')) return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', 'artifact must be a canonical top-level Markdown file');
|
|
444
|
-
if (!/^TASK-\d{8}-\d{6}$/.test(path.basename(path.resolve(taskDir)))) return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', 'target directory is not a task directory');
|
|
445
|
-
const target = path.join(taskDir, artifact);
|
|
446
|
-
if (path.dirname(path.resolve(target)) !== path.resolve(taskDir)) return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', 'artifact must be inside the current task directory');
|
|
447
|
-
let stat: fs.Stats;
|
|
448
|
-
try { stat = fs.lstatSync(target); }
|
|
449
|
-
catch (error) { return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', `artifact cannot be read: ${String(error)}`); }
|
|
450
|
-
if (stat.isSymbolicLink() || !stat.isFile()) return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', 'artifact is not a regular file');
|
|
451
|
-
try { fs.accessSync(target, fs.constants.R_OK | fs.constants.W_OK); }
|
|
452
|
-
catch (error) { return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', `artifact is not readable and writable: ${String(error)}`); }
|
|
453
|
-
return { path: target };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function applyRepairUnlocked(request: ArtifactRepairRequest): ArtifactFileResult {
|
|
457
|
-
const schema = getArtifactSchema(request.family);
|
|
458
|
-
if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
|
|
459
|
-
const target = validateTarget(request.taskDir, request.family, request.artifact);
|
|
460
|
-
if ('status' in target) return target;
|
|
461
|
-
let content: string;
|
|
462
|
-
try { content = readStableFileSync(target.path, { maxBytes: 1024 * 1024 }).bytes.toString('utf8'); }
|
|
463
|
-
catch (error) { return resultFailure('ARTIFACT_REPAIR_TARGET_INVALID', String(error)); }
|
|
464
|
-
const actualSha256 = sha256Content(content);
|
|
465
|
-
if (actualSha256 !== request.expectedSha256) return resultFailure('ARTIFACT_REPAIR_BASELINE_MISMATCH', 'artifact SHA-256 does not match the expected repair baseline');
|
|
466
|
-
const context = validateRepairContext(request, content);
|
|
467
|
-
if (!context.ok) return resultFailure(context.code, context.message);
|
|
468
|
-
const inspection = inspectArtifactStructure(content, schema);
|
|
469
|
-
if (!inspection.repair || inspection.diagnostics.length !== 1) {
|
|
470
|
-
return resultFailure('ARTIFACT_REPAIR_UNSAFE', inspection.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ') || 'no deterministic structural repair is available');
|
|
471
|
-
}
|
|
472
|
-
if (inspection.repair.kind !== request.operation.kind
|
|
473
|
-
|| inspection.repair.sectionId !== request.operation.sectionId
|
|
474
|
-
|| inspection.repair.from !== request.operation.from
|
|
475
|
-
|| inspection.repair.to !== request.operation.to
|
|
476
|
-
|| inspection.repair.start !== request.operation.start
|
|
477
|
-
|| inspection.repair.end !== request.operation.end) {
|
|
478
|
-
return resultFailure('ARTIFACT_REPAIR_OPERATION_MISMATCH', 'repair operation does not match the current artifact structure');
|
|
479
|
-
}
|
|
480
|
-
if (inspection.semanticDigest !== request.expectedSemanticDigest) return resultFailure('ARTIFACT_REPAIR_BASELINE_MISMATCH', 'artifact semantic digest does not match the expected repair baseline');
|
|
481
|
-
const transformed = applyRepairOperation(content, inspection.repair);
|
|
482
|
-
if (transformed === null) return resultFailure('ARTIFACT_REPAIR_OPERATION_MISMATCH', 'repair operation does not match the current artifact bytes');
|
|
483
|
-
if (transformed === content) return resultFailure('ARTIFACT_REPAIR_NO_PROGRESS', 'repair operation produced no byte change');
|
|
484
|
-
const tempPath = path.join(request.taskDir, `.${request.artifact}.repair-${process.pid}-${Date.now()}.tmp`);
|
|
485
|
-
let mode = 0o600;
|
|
486
|
-
try {
|
|
487
|
-
mode = fs.statSync(target.path).mode & 0o777;
|
|
488
|
-
fs.writeFileSync(tempPath, transformed, { encoding: 'utf8', mode, flag: 'wx' });
|
|
489
|
-
const currentStat = fs.lstatSync(target.path);
|
|
490
|
-
if (currentStat.isSymbolicLink() || !currentStat.isFile() || readStableFileSync(target.path, { maxBytes: 1024 * 1024 }).sha256 !== actualSha256) {
|
|
491
|
-
fs.unlinkSync(tempPath);
|
|
492
|
-
return resultFailure('ARTIFACT_REPAIR_CONFLICT', 'artifact changed or was replaced during repair');
|
|
493
|
-
}
|
|
494
|
-
fs.renameSync(tempPath, target.path);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
try { fs.unlinkSync(tempPath); } catch { /* preserve primary error */ }
|
|
497
|
-
return resultFailure('ARTIFACT_REPAIR_WRITE_FAILED', String(error));
|
|
498
|
-
}
|
|
499
|
-
return {
|
|
500
|
-
status: 'applied',
|
|
501
|
-
changed: true,
|
|
502
|
-
artifactSha256: sha256Content(transformed),
|
|
503
|
-
semanticDigest: canonicalSemanticDigest(transformed),
|
|
504
|
-
operation: inspection.repair,
|
|
505
|
-
error: null
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function applyArtifactRepair(request: ArtifactRepairRequest): ArtifactFileResult {
|
|
510
|
-
try {
|
|
511
|
-
return withTaskExecutionLock(request.repoRoot, request.taskId, 'task-artifact.repair', () => applyRepairUnlocked(request));
|
|
512
|
-
} catch (error) {
|
|
513
|
-
return resultFailure('ARTIFACT_REPAIR_LOCK_FAILED', String(error));
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
291
|
function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
|
|
518
292
|
const schema = getArtifactSchema(request.family);
|
|
519
293
|
if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
|
|
@@ -551,7 +325,6 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
|
|
|
551
325
|
changed: true,
|
|
552
326
|
artifactSha256: sha256Content(content),
|
|
553
327
|
semanticDigest: canonicalSemanticDigest(content),
|
|
554
|
-
operation: null,
|
|
555
328
|
error: null
|
|
556
329
|
};
|
|
557
330
|
});
|
|
@@ -561,7 +334,6 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
|
|
|
561
334
|
}
|
|
562
335
|
|
|
563
336
|
export {
|
|
564
|
-
applyArtifactRepair,
|
|
565
337
|
canonicalSemanticDigest,
|
|
566
338
|
initializeArtifactSkeleton,
|
|
567
339
|
inspectArtifactContract,
|
|
@@ -572,8 +344,6 @@ export {
|
|
|
572
344
|
export type {
|
|
573
345
|
ArtifactFileResult,
|
|
574
346
|
ArtifactInitRequest,
|
|
575
|
-
ArtifactRepairOperation,
|
|
576
|
-
ArtifactRepairRequest,
|
|
577
347
|
ArtifactStructuralDiagnostic,
|
|
578
348
|
ArtifactStructuralDiagnosticCode,
|
|
579
349
|
ArtifactStructureResult
|