@fitlab-ai/agent-infra 0.11.2 → 0.11.4
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 +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +49 -343
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +1 -2
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +37 -38
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +319 -234
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +11 -1
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +29 -13
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +47 -362
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +1 -2
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +41 -48
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +310 -216
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +15 -2
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +56 -27
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/sandbox/control/finalization-response.js +0 -35
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/finalization-response.ts +0 -37
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isCompletionEvidence, type CleanCompletionEvidence } from '../../task/orchestration.ts';
|
|
2
2
|
import { parseControlOutput } from '../../task/control-recovery.ts';
|
|
3
|
-
import { isRecoveryWarning } from '../../task/recovery-warning.ts';
|
|
4
3
|
import { execFileSync } from 'node:child_process';
|
|
5
4
|
import fs from 'node:fs';
|
|
6
5
|
import path from 'node:path';
|
|
@@ -15,7 +14,6 @@ import type {
|
|
|
15
14
|
SandboxControlPayload,
|
|
16
15
|
SandboxControlReservation,
|
|
17
16
|
SandboxControlResultEvidence,
|
|
18
|
-
SandboxControlRecoveryWarning,
|
|
19
17
|
SandboxControlStatus
|
|
20
18
|
} from './protocol.ts';
|
|
21
19
|
import {
|
|
@@ -115,7 +113,6 @@ export type SandboxControlTerminalResult = Readonly<{
|
|
|
115
113
|
status: string;
|
|
116
114
|
changed: boolean | null;
|
|
117
115
|
completionEvidence: CleanCompletionEvidence | null;
|
|
118
|
-
warning?: SandboxControlRecoveryWarning | null;
|
|
119
116
|
}>;
|
|
120
117
|
|
|
121
118
|
export function terminalResultPath(manifest: SandboxControlManifest, requestId: string): string {
|
|
@@ -133,7 +130,6 @@ export function createSandboxControlTerminalResult(
|
|
|
133
130
|
? nested.run as Record<string, unknown> : null;
|
|
134
131
|
const rawCompletion = nested.completionEvidence ?? run?.completionEvidence;
|
|
135
132
|
const operation = request.operation ?? (typeof nested.intent === 'string' ? nested.intent : null) ?? '';
|
|
136
|
-
const warning = isRecoveryWarning(nested.warning) ? nested.warning : null;
|
|
137
133
|
return {
|
|
138
134
|
version: 1,
|
|
139
135
|
requestId: request.id,
|
|
@@ -143,8 +139,7 @@ export function createSandboxControlTerminalResult(
|
|
|
143
139
|
targetState: typeof nested.targetState === 'string' ? nested.targetState : null,
|
|
144
140
|
status: typeof nested.status === 'string' ? nested.status : 'completed',
|
|
145
141
|
changed: typeof nested.changed === 'boolean' ? nested.changed : null,
|
|
146
|
-
completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
|
|
147
|
-
...(warning ? { warning } : {})
|
|
142
|
+
completionEvidence: isCompletionEvidence(rawCompletion) ? rawCompletion : null
|
|
148
143
|
};
|
|
149
144
|
}
|
|
150
145
|
|
|
@@ -178,9 +173,6 @@ export function readSandboxControlTerminalResult(filePath: string): SandboxContr
|
|
|
178
173
|
if (value.completionEvidence !== null && !isCompletionEvidence(value.completionEvidence)) {
|
|
179
174
|
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
180
175
|
}
|
|
181
|
-
if (value.warning !== undefined && value.warning !== null && !isRecoveryWarning(value.warning)) {
|
|
182
|
-
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
183
|
-
}
|
|
184
176
|
if (value.changed === undefined) {
|
|
185
177
|
throw new Error('SANDBOX_CONTROL_TERMINAL_RESULT_INVALID');
|
|
186
178
|
}
|
|
@@ -56,11 +56,10 @@ 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 !==
|
|
59
|
+
if (receipt.version !== 4 || 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
62
|
|| !['pending', 'done', 'skipped'].includes(String(receipt.summary))
|
|
63
|
-
|| !['pending', 'done', 'skipped'].includes(String(receipt.postSummaryVerification))
|
|
64
63
|
|| controlBinding.generation !== generation
|
|
65
64
|
|| typeof controlBinding.requestId !== 'string'
|
|
66
65
|
|| !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)
|
|
@@ -8,7 +8,6 @@ import { enumerateAllTaskDirs } from '../task/resolve-ref.ts';
|
|
|
8
8
|
import { resolveSandboxCleanupTarget } from './workspace-identity.ts';
|
|
9
9
|
import { readSandboxControlManifest } from './control/lifecycle.ts';
|
|
10
10
|
import { readJsonFile, readSandboxControlStatus } from './control/state.ts';
|
|
11
|
-
import { finalizationTerminalResponse } from './control/finalization-response.ts';
|
|
12
11
|
|
|
13
12
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
14
13
|
|
|
@@ -83,13 +82,10 @@ function canonicalPath(input: string): string {
|
|
|
83
82
|
|
|
84
83
|
function taskFinalizationReceiptComplete(
|
|
85
84
|
receipt: TaskFinalizationReceipt,
|
|
86
|
-
taskId: string
|
|
87
|
-
binding: SandboxControlBinding
|
|
85
|
+
taskId: string
|
|
88
86
|
): boolean {
|
|
89
87
|
return receipt.taskId === taskId
|
|
90
|
-
&& taskFinalizationReceiptState(receipt) === 'complete'
|
|
91
|
-
&& receipt.controlBinding?.generation === binding.generation
|
|
92
|
-
&& receipt.controlBinding.requestId === binding.requestId;
|
|
88
|
+
&& taskFinalizationReceiptState(receipt) === 'complete';
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
function terminalControlBindingEvidence(
|
|
@@ -114,24 +110,20 @@ function terminalControlBindingEvidence(
|
|
|
114
110
|
if (errorCode(error) !== 'ENOENT') return false;
|
|
115
111
|
}
|
|
116
112
|
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
117
|
-
if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId
|
|
113
|
+
if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId)) {
|
|
118
114
|
return false;
|
|
119
115
|
}
|
|
120
|
-
const status = readSandboxControlStatus(manifest.publicStatusDir);
|
|
121
|
-
const viewReceipt = status.taskView.receipt;
|
|
122
|
-
const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
|
|
123
|
-
|| (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
|
|
124
|
-
if (status.generation !== binding.generation || status.state !== 'healthy'
|
|
125
|
-
|| status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
|
|
126
|
-
|| !viewReceipt || viewReceipt.generation !== binding.generation
|
|
127
|
-
|| viewReceipt.requestId !== binding.requestId
|
|
128
|
-
|| viewReceipt.receiptId !== receipt.receiptId
|
|
129
|
-
|| viewReceipt.revision !== receipt.revision) return false;
|
|
130
116
|
const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
|
|
131
117
|
if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file')) return false;
|
|
132
118
|
const actual = readJsonFile(responsePath);
|
|
133
|
-
const
|
|
134
|
-
|
|
119
|
+
const response = actual as Record<string, unknown>;
|
|
120
|
+
if (response.version !== 2 || response.id !== binding.requestId || response.phase !== 'completed'
|
|
121
|
+
|| response.exitCode !== 0 || typeof response.stdout !== 'string') return false;
|
|
122
|
+
const envelope = JSON.parse(response.stdout) as Record<string, unknown>;
|
|
123
|
+
const result = envelope.result as Record<string, unknown> | null;
|
|
124
|
+
return envelope.status === 'completed' && envelope.accepted === true
|
|
125
|
+
&& result !== null && typeof result === 'object'
|
|
126
|
+
&& result.status === 'completed' && result.taskId === taskId;
|
|
135
127
|
} catch {
|
|
136
128
|
return false;
|
|
137
129
|
}
|
|
@@ -157,7 +149,7 @@ function removedControlBindingEvidence(
|
|
|
157
149
|
if (frontmatter.id !== taskId || frontmatter.branch !== journal.target.branch) return false;
|
|
158
150
|
try {
|
|
159
151
|
const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
|
|
160
|
-
return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId
|
|
152
|
+
return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId);
|
|
161
153
|
} catch {
|
|
162
154
|
return false;
|
|
163
155
|
}
|
|
@@ -13,6 +13,9 @@ export type ArtifactCommand = Readonly<{
|
|
|
13
13
|
family: string;
|
|
14
14
|
artifact: string;
|
|
15
15
|
locale?: 'zh-CN' | 'en';
|
|
16
|
+
sourceFinding?: string;
|
|
17
|
+
sourceArtifact?: string;
|
|
18
|
+
sourceSha256?: string;
|
|
16
19
|
}>;
|
|
17
20
|
|
|
18
21
|
/** One option contract for the CLI and trusted projection executor. */
|
|
@@ -23,7 +26,7 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
|
23
26
|
}
|
|
24
27
|
const fields: Record<string, string> = {};
|
|
25
28
|
const allowed = {
|
|
26
|
-
inspect: ['--family'],
|
|
29
|
+
inspect: ['--family', '--source-finding', '--source-artifact', '--source-sha256'],
|
|
27
30
|
init: ['--family', '--artifact', '--locale'],
|
|
28
31
|
'preflight': ['--family', '--artifact'],
|
|
29
32
|
'finalize-local': ['--family', '--artifact']
|
|
@@ -41,8 +44,10 @@ export function parseArtifactCommand(args: readonly string[]): ArtifactCommand {
|
|
|
41
44
|
const locale = fields['--locale'];
|
|
42
45
|
if (locale !== undefined && locale !== 'zh-CN' && locale !== 'en') throw new Error("option '--locale' must be 'zh-CN' or 'en'");
|
|
43
46
|
return {
|
|
44
|
-
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'],
|
|
45
|
-
artifact: fields['--artifact'] ?? '', locale
|
|
47
|
+
taskRef, operation: operation as ArtifactCommand['operation'], family: fields['--family'] ?? '',
|
|
48
|
+
artifact: fields['--artifact'] ?? '', locale,
|
|
49
|
+
sourceFinding: fields['--source-finding'], sourceArtifact: fields['--source-artifact'],
|
|
50
|
+
sourceSha256: fields['--source-sha256']
|
|
46
51
|
};
|
|
47
52
|
}
|
|
48
53
|
/** Resolve authoritative task metadata separately from a trusted candidate directory. */
|
|
@@ -53,7 +58,12 @@ export function executeArtifactCommand(
|
|
|
53
58
|
const { taskRef, operation, family, artifact, locale } = command;
|
|
54
59
|
const fail = (code: string, message: string) => ({ status: 'failed', changed: false, error: { code, message } });
|
|
55
60
|
if (operation === 'inspect') {
|
|
56
|
-
const result = resolveArtifactContext(taskRef, family, {
|
|
61
|
+
const result = resolveArtifactContext(taskRef, family, {
|
|
62
|
+
repoRoot: options.repoRoot,
|
|
63
|
+
sourceFinding: command.sourceFinding,
|
|
64
|
+
sourceArtifact: command.sourceArtifact,
|
|
65
|
+
sourceSha256: command.sourceSha256
|
|
66
|
+
});
|
|
57
67
|
return family === 'code' && result.codeMode ? {
|
|
58
68
|
...result, mode: result.codeMode.mode, code_max: result.codeMode.codeMax, rev_max: result.codeMode.reviewMax,
|
|
59
69
|
verdict: result.codeMode.verdict, next_round: result.next?.round ?? null, next_artifact: result.next?.name ?? null,
|
|
@@ -11,8 +11,12 @@ import { extractSection, findSectionHeading } from './sections.ts';
|
|
|
11
11
|
import { receiptForOutput, sha256File } from './artifact-receipts.ts';
|
|
12
12
|
import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
13
13
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
14
|
-
import { validateQualificationAudit } from './qualification-audit.ts';
|
|
15
14
|
import type { ArtifactFamily, ArtifactFamilySpec } from './artifact-name.ts';
|
|
15
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
16
|
+
import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
17
|
+
import { parseReworkIntentDocument } from './rework-intent.ts';
|
|
18
|
+
import { selectArtifactDisposition } from './artifact-selection.ts';
|
|
19
|
+
import type { ArtifactSelection } from './artifact-selection.ts';
|
|
16
20
|
|
|
17
21
|
const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
|
|
18
22
|
const ARTIFACT_STEPS: Readonly<Record<string, string>> = {
|
|
@@ -33,6 +37,16 @@ export function hasOpenArtifactRound(content: string, family: string, round: num
|
|
|
33
37
|
return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
function hasCompletedArtifactRound(content: string, family: string, round: number, name: string): boolean {
|
|
41
|
+
const activity = locateActivityLog(content);
|
|
42
|
+
const step = ARTIFACT_STEPS[family];
|
|
43
|
+
if (!activity || !step) return false;
|
|
44
|
+
const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
|
|
45
|
+
return startedBackedRows(pairEntries(activity.entries)).some((row) =>
|
|
46
|
+
expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
type ArtifactIdentity = {
|
|
37
51
|
family: ArtifactFamily;
|
|
38
52
|
round: number;
|
|
@@ -55,9 +69,12 @@ type ArtifactErrorCode =
|
|
|
55
69
|
| ResolveTaskRefErrorCode | 'ARTIFACT_FAMILY_UNKNOWN' | 'ARTIFACT_DIRECTORY_READ_FAILED'
|
|
56
70
|
| 'ARTIFACT_TOPOLOGY_CONFLICT' | 'ARTIFACT_INPUT_MISSING'
|
|
57
71
|
| 'ARTIFACT_REFERENCE_INVALID' | 'ARTIFACT_PATH_INVALID'
|
|
72
|
+
| 'LIFECYCLE_PATH_INVALID'
|
|
58
73
|
| 'ARTIFACT_IDENTITY_INVALID' | 'ARTIFACT_NOT_FOUND'
|
|
59
74
|
| 'ARTIFACT_NOT_REGULAR' | 'ARTIFACT_NOT_READABLE' | 'ARTIFACT_VERDICT_INVALID'
|
|
60
|
-
| 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
|
|
75
|
+
| 'ARTIFACT_MODE_REFUSED' | 'ARTIFACT_INVALIDATION_INVALID'
|
|
76
|
+
| 'ARTIFACT_SELECTION_INDETERMINATE';
|
|
77
|
+
|
|
61
78
|
type ArtifactError = { code: ArtifactErrorCode; message: string };
|
|
62
79
|
type ArtifactInventoryResult = {
|
|
63
80
|
status: 'ready' | 'failed';
|
|
@@ -79,6 +96,7 @@ type CodeArtifactMode = 'init' | 'fix' | 'decision' | 'refused' | 'error';
|
|
|
79
96
|
type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
|
|
80
97
|
status: ArtifactContextStatus;
|
|
81
98
|
inputs: readonly ArtifactIdentity[];
|
|
99
|
+
selection?: ArtifactSelection | null;
|
|
82
100
|
codeMode: {
|
|
83
101
|
mode: CodeArtifactMode;
|
|
84
102
|
codeMax: number;
|
|
@@ -91,7 +109,13 @@ type ArtifactContextResult = Omit<ArtifactInventoryResult, 'status'> & {
|
|
|
91
109
|
message: string;
|
|
92
110
|
} | null;
|
|
93
111
|
};
|
|
94
|
-
type InspectOptions = {
|
|
112
|
+
type InspectOptions = {
|
|
113
|
+
repoRoot?: string;
|
|
114
|
+
reasonCode?: string;
|
|
115
|
+
sourceFinding?: string;
|
|
116
|
+
sourceArtifact?: string;
|
|
117
|
+
sourceSha256?: string;
|
|
118
|
+
};
|
|
95
119
|
|
|
96
120
|
const BLOCKING_DIAGNOSTICS = new Set<ArtifactDiagnosticCode>([
|
|
97
121
|
'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
|
|
@@ -260,9 +284,11 @@ function resolveCodeInputReceipt(taskDir: string, code: ArtifactIdentity): { inp
|
|
|
260
284
|
try { receipt = receiptForOutput(taskContent, code.name); }
|
|
261
285
|
catch { return null; }
|
|
262
286
|
if (!receipt || receipt.event !== 'code.completed') return null;
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
const
|
|
287
|
+
const identity = parseArtifactName(receipt.input);
|
|
288
|
+
if (!identity || !['analysis', 'plan'].includes(identity.family)) return null;
|
|
289
|
+
const source = inspectArtifactDirectory(taskDir, identity.family);
|
|
290
|
+
if (source.status !== 'ready') return null;
|
|
291
|
+
const input = source.artifacts.find((artifact) => artifact.name === receipt.input);
|
|
266
292
|
return input ? { input, inputSha256: receipt.inputSha256 } : null;
|
|
267
293
|
}
|
|
268
294
|
|
|
@@ -270,6 +296,16 @@ function resolveCodePlanInput(taskDir: string, code: ArtifactIdentity): { input:
|
|
|
270
296
|
return resolveCodeInputReceipt(taskDir, code);
|
|
271
297
|
}
|
|
272
298
|
|
|
299
|
+
function lifecyclePathState(taskDir: string): LifecyclePathState {
|
|
300
|
+
const analysis = inspectArtifactDirectory(taskDir, 'analysis');
|
|
301
|
+
if (analysis.status !== 'ready' || !analysis.latest) return { status: 'missing', decision: null, message: 'analysis artifact is missing' };
|
|
302
|
+
try {
|
|
303
|
+
return parseLifecyclePathDecision(fs.readFileSync(analysis.latest.path, 'utf8'), analysis.latest.name, sha256File(analysis.latest.path));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { status: 'invalid', decision: null, message: error instanceof Error ? error.message : String(error) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
273
309
|
function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactError | null {
|
|
274
310
|
if (inventory.status === 'failed') return inventory.error;
|
|
275
311
|
const blockers = inventory.diagnostics.filter((item) => BLOCKING_DIAGNOSTICS.has(item.code));
|
|
@@ -282,21 +318,127 @@ function assertWritableInventory(inventory: ArtifactInventoryResult): ArtifactEr
|
|
|
282
318
|
const REQUIRED_INPUT: Partial<Record<ArtifactFamily, ArtifactFamily>> = {
|
|
283
319
|
'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
|
|
284
320
|
};
|
|
285
|
-
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily
|
|
286
|
-
analysis: { family: 'review-analysis'
|
|
287
|
-
plan: { family: 'review-plan'
|
|
321
|
+
const OPTIONAL_CONTEXT: Partial<Record<ArtifactFamily, { family: ArtifactFamily }>> = {
|
|
322
|
+
analysis: { family: 'review-analysis' },
|
|
323
|
+
plan: { family: 'review-plan' },
|
|
288
324
|
'review-code': { family: 'review-plan' },
|
|
289
325
|
'manual-validation': { family: 'review-code' },
|
|
290
326
|
'validation-run': { family: 'review-code' }
|
|
291
327
|
};
|
|
292
|
-
|
|
328
|
+
function hasSelectionEvidence(
|
|
329
|
+
content: string,
|
|
330
|
+
family: ArtifactFamily,
|
|
331
|
+
options: InspectOptions,
|
|
332
|
+
context: ArtifactContextResult
|
|
333
|
+
): boolean {
|
|
334
|
+
if (options.sourceFinding && options.sourceArtifact && options.sourceSha256) return true;
|
|
335
|
+
if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt') return true;
|
|
336
|
+
if (family.startsWith('review-')) {
|
|
337
|
+
const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
338
|
+
const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
|
|
339
|
+
if (currentInput && context.reviewedInput?.name !== currentInput.name) return true;
|
|
340
|
+
}
|
|
341
|
+
const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
|
|
342
|
+
if (target) {
|
|
343
|
+
const parsed = parseReworkIntentDocument(content);
|
|
344
|
+
if (!parsed.ok) throw new Error(`${parsed.code}: ${parsed.message}`);
|
|
345
|
+
if (parsed.intents.some((intent) => intent.status === 'pending'
|
|
346
|
+
&& (intent.target === target || family === 'analysis' && intent.target === 'plan'))) return true;
|
|
347
|
+
}
|
|
348
|
+
if (family === 'code') {
|
|
349
|
+
if (context.latest && context.codeMode?.mode === 'init') return true;
|
|
350
|
+
if (parseImplementationInputs(content).rows.some((input) => input.needsImplementation && input.status === 'pending')) return true;
|
|
351
|
+
}
|
|
352
|
+
return context.inputs.filter((candidate) => candidate.family.startsWith('review-')).some((input) => {
|
|
353
|
+
const verdict = parseVerdict(input.path);
|
|
354
|
+
if (!verdict.ok || verdict.verdict !== 'Changes Requested' || !context.latest) return false;
|
|
355
|
+
const receipt = receiptForOutput(content, input.name);
|
|
356
|
+
return receipt?.event === reviewEventName(input.family)
|
|
357
|
+
&& receipt.input === context.latest.name
|
|
358
|
+
&& receipt.inputSha256 === sha256File(context.latest.path);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function attachArtifactSelection(
|
|
363
|
+
context: ArtifactContextResult,
|
|
364
|
+
pathState: LifecyclePathState,
|
|
365
|
+
options: InspectOptions
|
|
366
|
+
): ArtifactContextResult {
|
|
367
|
+
if (context.status !== 'ready' || !context.taskDir || !context.taskId
|
|
368
|
+
|| (context.family !== 'analysis' && pathState.status !== 'valid')) return context;
|
|
369
|
+
const family = context.family as ArtifactFamily;
|
|
370
|
+
const fail = (code: ArtifactErrorCode, message: string): ArtifactContextResult => ({
|
|
371
|
+
...context, status: 'failed', selection: null, error: { code, message }
|
|
372
|
+
});
|
|
373
|
+
if (!context.next) return fail('ARTIFACT_SELECTION_INDETERMINATE', 'artifact inventory did not provide a writable identity');
|
|
374
|
+
let taskContent: string;
|
|
375
|
+
try {
|
|
376
|
+
taskContent = fs.readFileSync(path.join(context.taskDir, 'task.md'), 'utf8');
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
379
|
+
}
|
|
380
|
+
const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
|
|
381
|
+
const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
|
|
382
|
+
const open = openLatest || openNext;
|
|
383
|
+
const selectedIdentity = openLatest ? context.latest! : context.next;
|
|
384
|
+
try {
|
|
385
|
+
const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
|
|
386
|
+
const selection = selectArtifactDisposition({
|
|
387
|
+
next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
|
|
388
|
+
latest: context.latest,
|
|
389
|
+
open,
|
|
390
|
+
hasChangeEvidence
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
...context,
|
|
394
|
+
selection,
|
|
395
|
+
next: selection.writeRequired ? { round: selection.artifact.round, name: selection.artifact.name } : null
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
293
401
|
|
|
294
402
|
function resolveArtifactContext(taskRef: string, family: string, options: InspectOptions = {}): ArtifactContextResult {
|
|
295
403
|
const inventory = inspectTaskArtifacts(taskRef, family, options);
|
|
296
404
|
if (inventory.status === 'failed') return { ...inventory, inputs: [], codeMode: null };
|
|
297
405
|
const writableError = assertWritableInventory(inventory);
|
|
298
406
|
if (writableError) return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
|
|
299
|
-
|
|
407
|
+
const pathState = lifecyclePathState(inventory.taskDir!);
|
|
408
|
+
if (inventory.family !== 'analysis' && pathState.status !== 'valid') {
|
|
409
|
+
return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
|
|
410
|
+
}
|
|
411
|
+
if (inventory.family === 'code') {
|
|
412
|
+
const code = resolveCodeContext(inventory, options);
|
|
413
|
+
const selected = attachArtifactSelection(code, pathState, options);
|
|
414
|
+
if (selected.status !== 'ready' || !selected.selection) return selected;
|
|
415
|
+
if (selected.selection.disposition === 'reuse') return selected;
|
|
416
|
+
if (selected.codeMode?.mode === 'error') {
|
|
417
|
+
return {
|
|
418
|
+
...selected,
|
|
419
|
+
status: 'ready',
|
|
420
|
+
codeMode: {
|
|
421
|
+
...selected.codeMode,
|
|
422
|
+
mode: 'init',
|
|
423
|
+
message: 'A new implementation round was requested.'
|
|
424
|
+
},
|
|
425
|
+
error: null
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (selected.codeMode?.mode === 'refused') {
|
|
429
|
+
return {
|
|
430
|
+
...selected,
|
|
431
|
+
status: 'ready',
|
|
432
|
+
codeMode: {
|
|
433
|
+
...selected.codeMode,
|
|
434
|
+
mode: 'init',
|
|
435
|
+
message: 'A new implementation round was requested.'
|
|
436
|
+
},
|
|
437
|
+
error: null
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return selected;
|
|
441
|
+
}
|
|
300
442
|
const required = REQUIRED_INPUT[inventory.family as ArtifactFamily];
|
|
301
443
|
const inputs: ArtifactIdentity[] = [];
|
|
302
444
|
if (required) {
|
|
@@ -304,116 +446,113 @@ function resolveArtifactContext(taskRef: string, family: string, options: Inspec
|
|
|
304
446
|
if (input.status === 'failed' || !input.latest) {
|
|
305
447
|
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
|
|
306
448
|
}
|
|
307
|
-
const qualificationError = qualificationErrorForArtifact(input.latest);
|
|
308
|
-
if (qualificationError) return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
|
|
309
449
|
inputs.push(input.latest);
|
|
310
450
|
}
|
|
311
451
|
const optional = OPTIONAL_CONTEXT[inventory.family as ArtifactFamily];
|
|
312
452
|
if (optional) {
|
|
313
453
|
const context = inspectTaskArtifacts(taskRef, optional.family, options);
|
|
314
454
|
if (context.status === 'ready' && context.latest) {
|
|
315
|
-
if (optional.requireReference && !context.reviewedInput) {
|
|
316
|
-
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: `${context.latest.name} has no valid reviewed input` } };
|
|
317
|
-
}
|
|
318
|
-
const qualificationError = qualificationErrorForArtifact(context.latest);
|
|
319
|
-
if (qualificationError && !QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS.has(inventory.family as ArtifactFamily)) {
|
|
320
|
-
return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
|
|
321
|
-
}
|
|
322
455
|
inputs.push(context.latest);
|
|
323
456
|
}
|
|
324
457
|
}
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function qualificationErrorForArtifact(artifact: ArtifactIdentity): string | null {
|
|
329
|
-
try {
|
|
330
|
-
const taskContent = fs.readFileSync(path.join(path.dirname(artifact.path), 'task.md'), 'utf8');
|
|
331
|
-
const artifactContent = fs.readFileSync(artifact.path, 'utf8');
|
|
332
|
-
const result = validateQualificationAudit(taskContent, artifactContent);
|
|
333
|
-
return result.ok ? null : `${result.code}: ${result.message}`;
|
|
334
|
-
} catch (error) {
|
|
335
|
-
return error instanceof Error ? error.message : String(error);
|
|
458
|
+
if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
|
|
459
|
+
return { ...inventory, inputs, codeMode: null };
|
|
336
460
|
}
|
|
461
|
+
return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
|
|
337
462
|
}
|
|
338
463
|
|
|
339
464
|
function resolveCodeContext(inventory: ArtifactInventoryResult, options: InspectOptions): ArtifactContextResult {
|
|
340
465
|
const taskRef = inventory.requestRef;
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
const
|
|
466
|
+
const pathState = lifecyclePathState(inventory.taskDir!);
|
|
467
|
+
if (pathState.status !== 'valid') return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
|
|
468
|
+
const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
|
|
469
|
+
const source = inspectTaskArtifacts(taskRef, inputFamily, options);
|
|
470
|
+
if (source.status === 'failed' || !source.latest) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
|
|
344
471
|
const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
|
|
345
472
|
const latestCode = inventory.latest;
|
|
346
473
|
const codeMax = latestCode?.round ?? 0;
|
|
347
474
|
const reviewMax = reviewCode.latest?.round ?? 0;
|
|
348
|
-
const inputs = [
|
|
349
|
-
const planQualificationError = qualificationErrorForArtifact(plan.latest);
|
|
350
|
-
if (planQualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', planQualificationError);
|
|
475
|
+
const inputs = [source.latest];
|
|
351
476
|
if (latestCode) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
477
|
+
try {
|
|
478
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
479
|
+
if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
|
|
480
|
+
const review = reviewCode.latest;
|
|
481
|
+
const openInputs = review ? [...inputs, review] : inputs;
|
|
482
|
+
const openVerdict = review ? parseVerdict(review.path) : null;
|
|
483
|
+
const openRow = locateActivityLog(taskContent)
|
|
484
|
+
? startedBackedRows(pairEntries(locateActivityLog(taskContent)!.entries))
|
|
485
|
+
.find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
|
|
486
|
+
: null;
|
|
487
|
+
const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
488
|
+
const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
|
|
489
|
+
const mode = decisionInput ? 'decision'
|
|
490
|
+
: fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
|
|
491
|
+
: 'init';
|
|
492
|
+
return withCodeMode(
|
|
493
|
+
inventory, openInputs, 'ready', mode, codeMax, reviewMax,
|
|
494
|
+
openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null,
|
|
495
|
+
`Resuming open code round ${latestCode.round}.`,
|
|
496
|
+
decisionInput
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
|
|
501
|
+
}
|
|
358
502
|
}
|
|
359
503
|
if (!latestCode) {
|
|
360
|
-
if (!reviewPlan.latest || reviewPlan.reviewedInput?.name !== plan.latest.name) {
|
|
361
|
-
return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest plan '${plan.latest.name}' requires a matching approved review-plan`);
|
|
362
|
-
}
|
|
363
|
-
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
364
|
-
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
|
|
365
|
-
if (verdict.verdict !== 'Approved') {
|
|
366
|
-
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `latest ${reviewPlan.latest.name} is not approved`, reviewPlan.latest.name);
|
|
367
|
-
}
|
|
368
504
|
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
369
505
|
'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
|
|
370
506
|
}
|
|
371
507
|
const codePlanInput = resolveCodePlanInput(inventory.taskDir!, latestCode);
|
|
372
508
|
if (!codePlanInput) {
|
|
373
|
-
return
|
|
509
|
+
return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null,
|
|
510
|
+
`Code input receipt for ${latestCode.name} is unavailable.`);
|
|
374
511
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const qualificationError = qualificationErrorForArtifact(reviewPlan.latest);
|
|
380
|
-
if (qualificationError) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewPlan.latest.name);
|
|
381
|
-
}
|
|
382
|
-
if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
|
|
383
|
-
const verdict = parseVerdict(reviewPlan.latest.path);
|
|
384
|
-
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
|
|
385
|
-
const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
|
|
386
|
-
if (verdict.ok && verdict.verdict === 'Approved' && (
|
|
387
|
-
codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256
|
|
388
|
-
)) {
|
|
389
|
-
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name,
|
|
390
|
-
`Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
391
|
-
}
|
|
512
|
+
const currentInputSha256 = sha256File(source.latest.path);
|
|
513
|
+
if (codePlanInput.input.name !== source.latest.name || codePlanInput.inputSha256 !== currentInputSha256) {
|
|
514
|
+
return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null,
|
|
515
|
+
`Latest ${source.latest.name} is not captured by the latest code input receipt. Entering replan-driven init.`);
|
|
392
516
|
}
|
|
393
517
|
if (!reviewCode.latest || reviewCode.reviewedInput?.name !== latestCode.name) {
|
|
394
518
|
const expected = reviewCode.next?.name ?? artifactName('review-code', reviewMax + 1);
|
|
395
|
-
return
|
|
519
|
+
return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, expected,
|
|
520
|
+
`${expected} is required before another code round`);
|
|
396
521
|
}
|
|
397
522
|
const review = reviewCode.latest;
|
|
398
523
|
if (!review) return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest review-code artifact is required');
|
|
399
524
|
const verdict = parseVerdict(review.path);
|
|
400
525
|
if (!verdict.ok) return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, review.name);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
526
|
+
const decision = resolveDecisionImplementationInput(inventory, review);
|
|
527
|
+
if ('error' in decision) return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
|
|
528
|
+
if (decision.input) {
|
|
529
|
+
return withCodeMode(
|
|
530
|
+
inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax,
|
|
531
|
+
verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`,
|
|
532
|
+
decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const taskContent = fs.readFileSync(path.join(inventory.taskDir!, 'task.md'), 'utf8');
|
|
537
|
+
const rework = parseReworkIntentDocument(taskContent);
|
|
538
|
+
const reviewSha256 = sha256File(review.path);
|
|
539
|
+
if (rework.ok && rework.intents.some((intent) => intent.status === 'pending'
|
|
540
|
+
&& intent.target === 'code'
|
|
541
|
+
&& intent.sourceArtifact === review.name
|
|
542
|
+
&& intent.sourceSha256 === reviewSha256)) {
|
|
543
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
|
|
544
|
+
`Pending rework evidence requires a new code round after ${review.name}.`);
|
|
410
545
|
}
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error), review.name);
|
|
411
548
|
}
|
|
412
|
-
if (verdict.verdict === 'Approved'
|
|
413
|
-
return withCodeMode(inventory, [...inputs, review], '
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
549
|
+
if (verdict.verdict === 'Approved') {
|
|
550
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
|
|
551
|
+
`Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`);
|
|
552
|
+
}
|
|
553
|
+
if (verdict.verdict === 'Rejected') {
|
|
554
|
+
return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name,
|
|
555
|
+
`Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
|
|
417
556
|
}
|
|
418
557
|
return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name,
|
|
419
558
|
`Latest ${review.name} requires or permits fixes. Entering fix mode.`);
|
|
@@ -482,7 +621,19 @@ export function validateArtifactPublication(taskDir: string, family: ArtifactFam
|
|
|
482
621
|
const inventory = inspectArtifactDirectory(taskDir, family);
|
|
483
622
|
const error = assertWritableInventory(inventory);
|
|
484
623
|
if (error) return error;
|
|
485
|
-
if (inventory.next?.name === name
|
|
624
|
+
if (inventory.next?.name === name) return null;
|
|
625
|
+
const existing = inventory.artifacts.find((artifact) => artifact.name === name);
|
|
626
|
+
if (existing) {
|
|
627
|
+
try {
|
|
628
|
+
const task = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
629
|
+
if (hasCompletedArtifactRound(task, family, existing.round, name)) {
|
|
630
|
+
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
486
637
|
return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
|
|
487
638
|
}
|
|
488
639
|
|