voratiq 0.1.0-beta.21 → 0.1.0-beta.22
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 +18 -22
- package/dist/agents/launch/chat.d.ts +3 -1
- package/dist/agents/launch/chat.js +2 -0
- package/dist/bin.js +28 -7
- package/dist/cli/auto.js +1 -0
- package/dist/cli/contract.d.ts +26 -17
- package/dist/cli/contract.js +3 -1
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +115 -0
- package/dist/cli/list.js +4 -1
- package/dist/cli/operator-envelope.d.ts +19 -6
- package/dist/cli/operator-envelope.js +61 -1
- package/dist/cli/run.js +2 -0
- package/dist/cli/verify.d.ts +1 -1
- package/dist/cli/verify.js +48 -9
- package/dist/commands/auto/command.d.ts +1 -0
- package/dist/commands/auto/command.js +22 -12
- package/dist/commands/auto/errors.js +1 -1
- package/dist/commands/doctor/agents.d.ts +5 -0
- package/dist/commands/{init → doctor}/agents.js +37 -19
- package/dist/commands/doctor/command.d.ts +22 -0
- package/dist/commands/doctor/command.js +99 -0
- package/dist/commands/doctor/environment.d.ts +2 -0
- package/dist/commands/{init → doctor}/environment.js +38 -6
- package/dist/commands/{init/types.d.ts → doctor/fix-types.d.ts} +30 -9
- package/dist/commands/doctor/fix.d.ts +2 -0
- package/dist/commands/{init/command.js → doctor/fix.js} +106 -10
- package/dist/commands/doctor/reconcile.d.ts +2 -0
- package/dist/commands/doctor/reconcile.js +101 -0
- package/dist/commands/interactive/lifecycle.d.ts +2 -0
- package/dist/commands/interactive/lifecycle.js +8 -0
- package/dist/commands/list/command.d.ts +1 -0
- package/dist/commands/list/command.js +211 -352
- package/dist/commands/list/normalization.d.ts +56 -0
- package/dist/commands/list/normalization.js +317 -0
- package/dist/commands/message/command.d.ts +2 -1
- package/dist/commands/message/command.js +35 -14
- package/dist/commands/message/errors.d.ts +12 -3
- package/dist/commands/message/errors.js +19 -3
- package/dist/commands/reduce/command.js +16 -17
- package/dist/commands/reduce/errors.d.ts +2 -2
- package/dist/commands/reduce/errors.js +3 -3
- package/dist/commands/reduce/targets.js +11 -2
- package/dist/commands/root-launcher/command.js +12 -6
- package/dist/commands/run/command.d.ts +1 -0
- package/dist/commands/run/command.js +4 -1
- package/dist/commands/run/record-init.d.ts +2 -0
- package/dist/commands/run/record-init.js +2 -1
- package/dist/commands/run/spec-provenance.d.ts +37 -0
- package/dist/commands/run/spec-provenance.js +384 -0
- package/dist/commands/run/validation.d.ts +4 -0
- package/dist/commands/run/validation.js +25 -62
- package/dist/commands/spec/command.js +19 -6
- package/dist/commands/spec/errors.d.ts +5 -0
- package/dist/commands/spec/errors.js +9 -0
- package/dist/commands/verify/agents.d.ts +4 -2
- package/dist/commands/verify/agents.js +4 -11
- package/dist/commands/verify/command.js +15 -5
- package/dist/commands/verify/errors.d.ts +12 -0
- package/dist/commands/verify/errors.js +22 -0
- package/dist/commands/verify/targets.js +108 -12
- package/dist/competition/shared/preflight.d.ts +1 -1
- package/dist/competition/shared/preflight.js +15 -2
- package/dist/contracts/list.d.ts +129 -149
- package/dist/contracts/list.js +47 -99
- package/dist/domain/interactive/persistence/adapter.d.ts +23 -0
- package/dist/domain/interactive/persistence/adapter.js +42 -0
- package/dist/domain/message/model/types.d.ts +32 -0
- package/dist/domain/message/model/types.js +25 -0
- package/dist/domain/reduce/competition/adapter.js +21 -7
- package/dist/domain/reduce/competition/finalize.d.ts +7 -0
- package/dist/domain/reduce/competition/finalize.js +19 -0
- package/dist/domain/reduce/model/types.d.ts +3 -3
- package/dist/domain/run/competition/agents/artifacts.js +4 -2
- package/dist/domain/run/competition/errors.d.ts +1 -1
- package/dist/domain/run/competition/errors.js +4 -7
- package/dist/domain/run/model/types.d.ts +384 -0
- package/dist/domain/run/model/types.js +87 -0
- package/dist/domain/spec/competition/adapter.d.ts +1 -0
- package/dist/domain/spec/competition/adapter.js +6 -1
- package/dist/domain/spec/model/types.d.ts +3 -0
- package/dist/domain/spec/model/types.js +5 -0
- package/dist/domain/verify/competition/finalize.d.ts +9 -0
- package/dist/domain/verify/competition/finalize.js +22 -3
- package/dist/domain/verify/model/types.d.ts +2 -2
- package/dist/interactive/providers/mcp.d.ts +1 -0
- package/dist/interactive/providers/mcp.js +45 -7
- package/dist/interactive/substrate.js +20 -3
- package/dist/mcp/server.js +26 -9
- package/dist/policy/verification.js +18 -1
- package/dist/preflight/agents.d.ts +24 -0
- package/dist/preflight/agents.js +71 -0
- package/dist/preflight/environment.d.ts +6 -0
- package/dist/preflight/environment.js +17 -0
- package/dist/preflight/formatting.d.ts +5 -0
- package/dist/preflight/formatting.js +20 -0
- package/dist/preflight/index.d.ts +2 -0
- package/dist/preflight/index.js +5 -9
- package/dist/preflight/operator.d.ts +32 -0
- package/dist/preflight/operator.js +40 -0
- package/dist/preflight/settings.d.ts +2 -0
- package/dist/preflight/settings.js +17 -0
- package/dist/render/transcripts/interactive.d.ts +16 -0
- package/dist/render/transcripts/interactive.js +42 -0
- package/dist/render/transcripts/list.d.ts +41 -0
- package/dist/render/transcripts/list.js +152 -3
- package/dist/render/transcripts/message.d.ts +2 -1
- package/dist/render/transcripts/message.js +19 -20
- package/dist/render/transcripts/reduce.d.ts +1 -0
- package/dist/render/transcripts/reduce.js +21 -21
- package/dist/render/transcripts/root-launcher.js +2 -12
- package/dist/render/transcripts/run.d.ts +3 -0
- package/dist/render/transcripts/run.js +30 -4
- package/dist/render/transcripts/spec.js +5 -8
- package/dist/render/transcripts/verify.d.ts +5 -3
- package/dist/render/transcripts/verify.js +44 -31
- package/dist/render/utils/duration.d.ts +5 -0
- package/dist/render/utils/duration.js +6 -0
- package/dist/render/utils/runs.d.ts +1 -0
- package/dist/render/utils/runs.js +1 -0
- package/dist/render/utils/transcript-shell.d.ts +2 -1
- package/dist/render/utils/transcript-shell.js +19 -6
- package/dist/utils/errors.d.ts +2 -1
- package/dist/utils/errors.js +3 -1
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.js +25 -2
- package/dist/utils/list-target.d.ts +4 -0
- package/dist/utils/list-target.js +35 -0
- package/dist/utils/terminal.d.ts +1 -0
- package/dist/utils/terminal.js +11 -0
- package/dist/workspace/chat/artifacts.d.ts +7 -0
- package/dist/workspace/chat/artifacts.js +94 -3
- package/dist/workspace/errors.js +2 -2
- package/dist/workspace/managed-state.d.ts +32 -0
- package/dist/workspace/managed-state.js +103 -0
- package/dist/workspace/setup.js +66 -2
- package/dist/workspace/shim.d.ts +1 -0
- package/dist/workspace/shim.js +3 -3
- package/dist/workspace/structure.d.ts +1 -0
- package/dist/workspace/structure.js +1 -0
- package/package.json +2 -2
- package/dist/cli/init.d.ts +0 -15
- package/dist/cli/init.js +0 -70
- package/dist/commands/init/agents.d.ts +0 -4
- package/dist/commands/init/command.d.ts +0 -2
- package/dist/commands/init/environment.d.ts +0 -2
- package/dist/render/transcripts/init.d.ts +0 -7
- package/dist/render/transcripts/init.js +0 -83
- /package/dist/commands/{init/types.js → doctor/fix-types.js} +0 -0
|
@@ -137,7 +137,12 @@ async function assertVerificationTargetEligible(input) {
|
|
|
137
137
|
if (record.status === "aborted") {
|
|
138
138
|
throw new CliError(`Verification session \`${target.id}\` did not complete.`, [`Status: \`${record.status}\`.`], ["Re-run `voratiq verify` to generate a complete artifact set."]);
|
|
139
139
|
}
|
|
140
|
-
const
|
|
140
|
+
const artifactMethods = record.methods.filter((method) => typeof method.artifactPath === "string" &&
|
|
141
|
+
method.artifactPath.trim().length > 0);
|
|
142
|
+
if (artifactMethods.length === 0) {
|
|
143
|
+
throw new CliError(`Verification session \`${target.id}\` has no reduction-ready artifacts.`, [], ["Re-run `voratiq verify` to generate at least one durable artifact."]);
|
|
144
|
+
}
|
|
145
|
+
const missing = await findMissingVerificationArtifacts(root, artifactMethods);
|
|
141
146
|
if (missing.length > 0) {
|
|
142
147
|
throw new CliError(`Verification session \`${target.id}\` is missing required artifacts.`, missing.map((path) => `Missing: \`${path}\`.`), ["Re-run `voratiq verify` to regenerate verification artifacts."]);
|
|
143
148
|
}
|
|
@@ -162,7 +167,11 @@ async function assertReductionTargetEligibleInternal(input) {
|
|
|
162
167
|
if (record.status !== "succeeded") {
|
|
163
168
|
throw new CliError(`Reduction session \`${target.id}\` did not complete.`, [`Status: \`${record.status}\`.`], ["Re-run `voratiq reduce` to generate a complete artifact set."]);
|
|
164
169
|
}
|
|
165
|
-
const
|
|
170
|
+
const succeededReducers = record.reducers.filter((reducer) => reducer.status === "succeeded" && typeof reducer.outputPath === "string");
|
|
171
|
+
if (succeededReducers.length === 0) {
|
|
172
|
+
throw new CliError(`Reduction session \`${target.id}\` has no successful reduction artifacts.`, [], ["Re-run `voratiq reduce` to regenerate reduction artifacts."]);
|
|
173
|
+
}
|
|
174
|
+
const missing = await findMissingReductionArtifacts(root, succeededReducers);
|
|
166
175
|
if (missing.length > 0) {
|
|
167
176
|
throw new CliError(`Reduction session \`${target.id}\` is missing required artifacts.`, missing.map((path) => `Missing: \`${path}\`.`), ["Re-run `voratiq reduce` to regenerate reduction artifacts."]);
|
|
168
177
|
}
|
|
@@ -87,7 +87,9 @@ async function setupRootLauncher(options) {
|
|
|
87
87
|
workspaceAutoInitMode: "when-missing",
|
|
88
88
|
});
|
|
89
89
|
if (context.workspaceAutoInitialized) {
|
|
90
|
-
writeLauncherNotice(writeOutput, renderWorkspaceAutoInitializedNotice()
|
|
90
|
+
writeLauncherNotice(writeOutput, renderWorkspaceAutoInitializedNotice(), {
|
|
91
|
+
leadingNewline: true,
|
|
92
|
+
});
|
|
91
93
|
}
|
|
92
94
|
const diagnostics = loadDiagnostics({ root: context.root });
|
|
93
95
|
assertEnabledAgents(diagnostics);
|
|
@@ -104,9 +106,7 @@ function assertEnabledAgents(diagnostics) {
|
|
|
104
106
|
if (diagnostics.enabledAgents.length > 0) {
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
|
-
throw new CliError("No enabled agents found.", [], [
|
|
108
|
-
"Add agents to `.voratiq/agents.yaml` or run `voratiq init` to set up your workspace.",
|
|
109
|
-
]);
|
|
109
|
+
throw new CliError("No enabled agents found.", [], ["Run `voratiq doctor --fix` to repair workspace setup."]);
|
|
110
110
|
}
|
|
111
111
|
function buildLauncherAvailability(diagnostics) {
|
|
112
112
|
const issuesByAgentId = new Map();
|
|
@@ -144,7 +144,7 @@ async function promptForLaunchPlan(options) {
|
|
|
144
144
|
writeOutput,
|
|
145
145
|
});
|
|
146
146
|
if (availability.launchable.length === 1) {
|
|
147
|
-
|
|
147
|
+
writeLauncherScreen(writeOutput, renderRootLauncherSingleAgentScreen({
|
|
148
148
|
selected: formatAgentLabel(selected.entry),
|
|
149
149
|
unavailable: availability.unavailable.map((agent) => ({
|
|
150
150
|
label: formatAgentLabel(agent.entry),
|
|
@@ -156,7 +156,7 @@ async function promptForLaunchPlan(options) {
|
|
|
156
156
|
}
|
|
157
157
|
async function promptForAgentSelection(options) {
|
|
158
158
|
const { launchable, unavailable, prompt, writeOutput } = options;
|
|
159
|
-
|
|
159
|
+
writeLauncherScreen(writeOutput, renderRootLauncherSelectionScreen({
|
|
160
160
|
launchable: launchable.map((agent) => ({
|
|
161
161
|
label: formatAgentLabel(agent.entry),
|
|
162
162
|
})),
|
|
@@ -206,6 +206,12 @@ function writeLauncherNotice(writeOutput, message, options = {}) {
|
|
|
206
206
|
leadingNewline: options.leadingNewline ?? false,
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
|
+
function writeLauncherScreen(writeOutput, message) {
|
|
210
|
+
writeOutput({
|
|
211
|
+
body: message.trimEnd(),
|
|
212
|
+
leadingNewline: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
209
215
|
async function finalizeLaunchResult(selected, launchResult) {
|
|
210
216
|
if (!launchResult.ok) {
|
|
211
217
|
throw new CliError(`Failed to launch ${formatAgentLabel(selected.entry)}.`, [launchResult.failure.message]);
|
|
@@ -21,7 +21,7 @@ import { validateAndPrepare } from "./validation.js";
|
|
|
21
21
|
* Execute a complete run: validate inputs, prepare workspace, execute agents, and finalize report.
|
|
22
22
|
*/
|
|
23
23
|
export async function executeRunCommand(input) {
|
|
24
|
-
const { root, runsFilePath, specAbsolutePath, specDisplayPath, agentIds, agentOverrideFlag, profileName, maxParallel: requestedMaxParallel, extraContextFiles = [], renderer, } = input;
|
|
24
|
+
const { root, runsFilePath, specsFilePath, specAbsolutePath, specDisplayPath, agentIds, agentOverrideFlag, profileName, maxParallel: requestedMaxParallel, extraContextFiles = [], renderer, } = input;
|
|
25
25
|
const resolution = resolveStageCompetitors({
|
|
26
26
|
root,
|
|
27
27
|
stageId: "run",
|
|
@@ -33,6 +33,8 @@ export async function executeRunCommand(input) {
|
|
|
33
33
|
const validation = await validateAndPrepare({
|
|
34
34
|
root,
|
|
35
35
|
specAbsolutePath,
|
|
36
|
+
specDisplayPath,
|
|
37
|
+
specsFilePath,
|
|
36
38
|
resolvedAgentIds: resolution.agentIds,
|
|
37
39
|
maxParallel: requestedMaxParallel,
|
|
38
40
|
});
|
|
@@ -50,6 +52,7 @@ export async function executeRunCommand(input) {
|
|
|
50
52
|
runsFilePath,
|
|
51
53
|
runId,
|
|
52
54
|
specDisplayPath,
|
|
55
|
+
specTarget: validation.specTarget,
|
|
53
56
|
baseRevisionSha: validation.baseRevisionSha,
|
|
54
57
|
repoDisplayPath,
|
|
55
58
|
createdAt,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { RunRecordInitResult } from "../../domain/run/competition/phases.js";
|
|
2
|
+
import type { RunSpecTarget } from "../../domain/run/model/types.js";
|
|
2
3
|
export interface RecordInitInput {
|
|
3
4
|
readonly root: string;
|
|
4
5
|
readonly runsFilePath: string;
|
|
5
6
|
readonly runId: string;
|
|
6
7
|
readonly specDisplayPath: string;
|
|
8
|
+
readonly specTarget?: RunSpecTarget;
|
|
7
9
|
readonly baseRevisionSha: string;
|
|
8
10
|
readonly repoDisplayPath: string;
|
|
9
11
|
readonly createdAt: string;
|
|
@@ -5,11 +5,12 @@ import { cleanupRunWorkspace } from "../../workspace/cleanup.js";
|
|
|
5
5
|
* Initialize and persist the initial run record.
|
|
6
6
|
*/
|
|
7
7
|
export async function initializeRunRecord(input) {
|
|
8
|
-
const { root, runsFilePath, runId, specDisplayPath, baseRevisionSha, repoDisplayPath, createdAt, startedAt, runRoot, extraContext, extraContextMetadata, } = input;
|
|
8
|
+
const { root, runsFilePath, runId, specDisplayPath, specTarget, baseRevisionSha, repoDisplayPath, createdAt, startedAt, runRoot, extraContext, extraContextMetadata, } = input;
|
|
9
9
|
const initialRecord = {
|
|
10
10
|
runId,
|
|
11
11
|
spec: {
|
|
12
12
|
path: normalizePathForDisplay(specDisplayPath),
|
|
13
|
+
target: specTarget,
|
|
13
14
|
},
|
|
14
15
|
extraContext,
|
|
15
16
|
extraContextMetadata,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RunSpecTarget } from "../../domain/run/model/types.js";
|
|
2
|
+
interface ParsedVoratiqFrontmatter {
|
|
3
|
+
readonly bodyContent: string;
|
|
4
|
+
readonly source?: ParsedVoratiqSourceMetadata;
|
|
5
|
+
readonly invalidProvenance?: Extract<RunSpecTarget, {
|
|
6
|
+
kind: "file";
|
|
7
|
+
}>["provenance"];
|
|
8
|
+
}
|
|
9
|
+
interface ParsedVoratiqSourceMetadata {
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
readonly agentId: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolveRunSpecTargetInput {
|
|
14
|
+
root: string;
|
|
15
|
+
specDisplayPath: string;
|
|
16
|
+
specsFilePath?: string;
|
|
17
|
+
specAbsolutePath?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LoadRunSpecInputResult {
|
|
20
|
+
readonly specContent: string;
|
|
21
|
+
readonly specTarget: RunSpecTarget;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Load run spec content, stripping only Voratiq-owned frontmatter before prompt construction.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadRunSpecInput(input: ResolveRunSpecTargetInput & {
|
|
27
|
+
specAbsolutePath: string;
|
|
28
|
+
}): Promise<LoadRunSpecInputResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a run's spec input to its upstream session identity when the
|
|
31
|
+
* provided spec path is a generated spec artifact or a descendant that
|
|
32
|
+
* carries Voratiq-owned provenance metadata.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveRunSpecTarget(input: ResolveRunSpecTargetInput & {
|
|
35
|
+
parsedFrontmatter?: ParsedVoratiqFrontmatter;
|
|
36
|
+
}): Promise<RunSpecTarget>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { load } from "js-yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { agentIdSchema } from "../../configs/agents/types.js";
|
|
7
|
+
import { readSpecRecords } from "../../domain/spec/persistence/adapter.js";
|
|
8
|
+
import { pathExists } from "../../utils/fs.js";
|
|
9
|
+
import { normalizePathForDisplay } from "../../utils/path.js";
|
|
10
|
+
const FRONTMATTER_PATTERN = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/u;
|
|
11
|
+
const TOP_LEVEL_VORATIQ_BLOCK_PATTERN = /^voratiq\s*:/mu;
|
|
12
|
+
const voratiqFrontmatterSourceSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
operator: z.literal("spec"),
|
|
15
|
+
sessionId: z.string().min(1),
|
|
16
|
+
agentId: agentIdSchema,
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
/**
|
|
20
|
+
* Load run spec content, stripping only Voratiq-owned frontmatter before prompt construction.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadRunSpecInput(input) {
|
|
23
|
+
const rawSpecContent = await readFile(input.specAbsolutePath, "utf8");
|
|
24
|
+
const parsedFrontmatter = parseVoratiqFrontmatter(rawSpecContent);
|
|
25
|
+
return {
|
|
26
|
+
specContent: parsedFrontmatter.bodyContent,
|
|
27
|
+
specTarget: await resolveRunSpecTarget({
|
|
28
|
+
...input,
|
|
29
|
+
parsedFrontmatter,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a run's spec input to its upstream session identity when the
|
|
35
|
+
* provided spec path is a generated spec artifact or a descendant that
|
|
36
|
+
* carries Voratiq-owned provenance metadata.
|
|
37
|
+
*/
|
|
38
|
+
export async function resolveRunSpecTarget(input) {
|
|
39
|
+
const { root, specDisplayPath, specsFilePath, specAbsolutePath } = input;
|
|
40
|
+
const normalizedSpecPath = normalizePathForDisplay(specDisplayPath);
|
|
41
|
+
const parsedFrontmatter = input.parsedFrontmatter ??
|
|
42
|
+
(specAbsolutePath
|
|
43
|
+
? parseVoratiqFrontmatter(await readFile(specAbsolutePath, "utf8"))
|
|
44
|
+
: undefined);
|
|
45
|
+
if (!specsFilePath || !(await pathExists(specsFilePath))) {
|
|
46
|
+
return parsedFrontmatter?.invalidProvenance
|
|
47
|
+
? {
|
|
48
|
+
kind: "file",
|
|
49
|
+
provenance: parsedFrontmatter.invalidProvenance,
|
|
50
|
+
}
|
|
51
|
+
: { kind: "file" };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const exactMatch = await readExactSpecArtifact({
|
|
55
|
+
root,
|
|
56
|
+
specsFilePath,
|
|
57
|
+
normalizedSpecPath,
|
|
58
|
+
specAbsolutePath,
|
|
59
|
+
bodyContent: parsedFrontmatter?.bodyContent,
|
|
60
|
+
});
|
|
61
|
+
if (exactMatch) {
|
|
62
|
+
return exactMatch;
|
|
63
|
+
}
|
|
64
|
+
if (parsedFrontmatter?.source) {
|
|
65
|
+
return await resolveDerivedSpecTarget({
|
|
66
|
+
root,
|
|
67
|
+
specsFilePath,
|
|
68
|
+
source: parsedFrontmatter.source,
|
|
69
|
+
bodyContent: parsedFrontmatter.bodyContent,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Provenance is best-effort metadata. Runs should still proceed when
|
|
75
|
+
// spec-session history cannot be read.
|
|
76
|
+
return parsedFrontmatter?.invalidProvenance
|
|
77
|
+
? {
|
|
78
|
+
kind: "file",
|
|
79
|
+
provenance: parsedFrontmatter.invalidProvenance,
|
|
80
|
+
}
|
|
81
|
+
: { kind: "file" };
|
|
82
|
+
}
|
|
83
|
+
if (parsedFrontmatter?.invalidProvenance) {
|
|
84
|
+
return {
|
|
85
|
+
kind: "file",
|
|
86
|
+
provenance: parsedFrontmatter.invalidProvenance,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { kind: "file" };
|
|
90
|
+
}
|
|
91
|
+
async function readExactSpecArtifact(options) {
|
|
92
|
+
const { root, specsFilePath, normalizedSpecPath, specAbsolutePath, bodyContent, } = options;
|
|
93
|
+
const [record] = await readSpecRecords({
|
|
94
|
+
root,
|
|
95
|
+
specsFilePath,
|
|
96
|
+
limit: 1,
|
|
97
|
+
predicate: (entry) => entry.agents.some((agent) => typeof agent.outputPath === "string" &&
|
|
98
|
+
normalizePathForDisplay(agent.outputPath) === normalizedSpecPath),
|
|
99
|
+
});
|
|
100
|
+
if (!record) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const agent = record.agents.find((entry) => typeof entry.outputPath === "string" &&
|
|
104
|
+
normalizePathForDisplay(entry.outputPath) === normalizedSpecPath);
|
|
105
|
+
if (!agent) {
|
|
106
|
+
return {
|
|
107
|
+
kind: "spec",
|
|
108
|
+
sessionId: record.sessionId,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const source = toRecordedSpecSourceDescriptor({
|
|
112
|
+
sessionId: record.sessionId,
|
|
113
|
+
agent,
|
|
114
|
+
});
|
|
115
|
+
if (!source) {
|
|
116
|
+
return {
|
|
117
|
+
kind: "spec",
|
|
118
|
+
sessionId: record.sessionId,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const currentBodyContent = typeof bodyContent === "string"
|
|
122
|
+
? bodyContent
|
|
123
|
+
: await readSpecBodyContent(specAbsolutePath);
|
|
124
|
+
if (typeof currentBodyContent !== "string") {
|
|
125
|
+
return {
|
|
126
|
+
kind: "spec",
|
|
127
|
+
sessionId: record.sessionId,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const currentContentHash = hashSpecBody(currentBodyContent);
|
|
131
|
+
return {
|
|
132
|
+
kind: "spec",
|
|
133
|
+
sessionId: record.sessionId,
|
|
134
|
+
provenance: currentContentHash === source.contentHash
|
|
135
|
+
? {
|
|
136
|
+
lineage: "exact",
|
|
137
|
+
source,
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
lineage: "derived_modified",
|
|
141
|
+
source,
|
|
142
|
+
currentContentHash,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function resolveDerivedSpecTarget(options) {
|
|
147
|
+
const { root, specsFilePath, source, bodyContent } = options;
|
|
148
|
+
const currentContentHash = hashSpecBody(bodyContent);
|
|
149
|
+
const [record] = await readSpecRecords({
|
|
150
|
+
root,
|
|
151
|
+
specsFilePath,
|
|
152
|
+
limit: 1,
|
|
153
|
+
predicate: (entry) => entry.sessionId === source.sessionId,
|
|
154
|
+
});
|
|
155
|
+
if (!record) {
|
|
156
|
+
return {
|
|
157
|
+
kind: "spec",
|
|
158
|
+
sessionId: source.sessionId,
|
|
159
|
+
provenance: {
|
|
160
|
+
lineage: "invalid",
|
|
161
|
+
issueCode: "stale_source",
|
|
162
|
+
source: toRunSpecSourceHint(source),
|
|
163
|
+
currentContentHash,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const agent = record.agents.find((entry) => entry.agentId === source.agentId);
|
|
168
|
+
const resolvedSource = agent
|
|
169
|
+
? toRecordedSpecSourceDescriptor({
|
|
170
|
+
sessionId: source.sessionId,
|
|
171
|
+
agent,
|
|
172
|
+
})
|
|
173
|
+
: undefined;
|
|
174
|
+
if (!agent) {
|
|
175
|
+
return {
|
|
176
|
+
kind: "spec",
|
|
177
|
+
sessionId: source.sessionId,
|
|
178
|
+
provenance: {
|
|
179
|
+
lineage: "invalid",
|
|
180
|
+
issueCode: "stale_source",
|
|
181
|
+
source: toRunSpecSourceHint(source),
|
|
182
|
+
currentContentHash,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!resolvedSource) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "spec",
|
|
189
|
+
sessionId: source.sessionId,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const sourceContentHash = await readSourceArtifactHash({
|
|
193
|
+
root,
|
|
194
|
+
source: resolvedSource,
|
|
195
|
+
});
|
|
196
|
+
if (sourceContentHash !== resolvedSource.contentHash) {
|
|
197
|
+
return {
|
|
198
|
+
kind: "spec",
|
|
199
|
+
sessionId: source.sessionId,
|
|
200
|
+
provenance: {
|
|
201
|
+
lineage: "invalid",
|
|
202
|
+
issueCode: "stale_source",
|
|
203
|
+
source: resolvedSource,
|
|
204
|
+
currentContentHash,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: "spec",
|
|
210
|
+
sessionId: source.sessionId,
|
|
211
|
+
provenance: {
|
|
212
|
+
lineage: currentContentHash === resolvedSource.contentHash
|
|
213
|
+
? "derived"
|
|
214
|
+
: "derived_modified",
|
|
215
|
+
source: resolvedSource,
|
|
216
|
+
currentContentHash,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function parseVoratiqFrontmatter(content) {
|
|
221
|
+
const frontmatterBlock = extractFrontmatterBlock(content);
|
|
222
|
+
if (!frontmatterBlock) {
|
|
223
|
+
return { bodyContent: content };
|
|
224
|
+
}
|
|
225
|
+
const hasVoratiqBlock = TOP_LEVEL_VORATIQ_BLOCK_PATTERN.test(frontmatterBlock.headerContent);
|
|
226
|
+
let document;
|
|
227
|
+
try {
|
|
228
|
+
document = load(frontmatterBlock.headerContent, { json: false }) ?? {};
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
const bodyContent = hasVoratiqBlock
|
|
232
|
+
? stripVoratiqFrontmatter(frontmatterBlock)
|
|
233
|
+
: content;
|
|
234
|
+
return hasVoratiqBlock
|
|
235
|
+
? {
|
|
236
|
+
bodyContent,
|
|
237
|
+
invalidProvenance: {
|
|
238
|
+
lineage: "invalid",
|
|
239
|
+
issueCode: "malformed_frontmatter",
|
|
240
|
+
currentContentHash: hashSpecBody(bodyContent),
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: { bodyContent: content };
|
|
244
|
+
}
|
|
245
|
+
if (!isObject(document) || !("voratiq" in document)) {
|
|
246
|
+
return { bodyContent: content };
|
|
247
|
+
}
|
|
248
|
+
const bodyContent = stripVoratiqFrontmatter(frontmatterBlock);
|
|
249
|
+
const parsed = parseVoratiqMetadata(document.voratiq, bodyContent);
|
|
250
|
+
return {
|
|
251
|
+
bodyContent,
|
|
252
|
+
...parsed,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function parseVoratiqMetadata(voratiqBlock, bodyContent) {
|
|
256
|
+
if (!isObject(voratiqBlock)) {
|
|
257
|
+
return {
|
|
258
|
+
invalidProvenance: {
|
|
259
|
+
lineage: "invalid",
|
|
260
|
+
issueCode: "malformed_frontmatter",
|
|
261
|
+
currentContentHash: hashSpecBody(bodyContent),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const parsedSource = voratiqFrontmatterSourceSchema.safeParse(voratiqBlock.source);
|
|
266
|
+
if (!parsedSource.success) {
|
|
267
|
+
return {
|
|
268
|
+
invalidProvenance: {
|
|
269
|
+
lineage: "invalid",
|
|
270
|
+
issueCode: "malformed_frontmatter",
|
|
271
|
+
currentContentHash: hashSpecBody(bodyContent),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
source: {
|
|
277
|
+
sessionId: parsedSource.data.sessionId,
|
|
278
|
+
agentId: parsedSource.data.agentId,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function extractFrontmatterBlock(content) {
|
|
283
|
+
const match = FRONTMATTER_PATTERN.exec(content);
|
|
284
|
+
if (!match) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
headerContent: match[1] ?? "",
|
|
289
|
+
bodyContent: content.slice(match[0].length),
|
|
290
|
+
lineBreak: match[0].includes("\r\n") ? "\r\n" : "\n",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function readSourceArtifactHash(options) {
|
|
294
|
+
const { root, source } = options;
|
|
295
|
+
try {
|
|
296
|
+
const sourceContent = await readFile(resolve(root, source.outputPath), "utf8");
|
|
297
|
+
return hashSpecBody(parseVoratiqFrontmatter(sourceContent).bodyContent);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function readSpecBodyContent(specAbsolutePath) {
|
|
304
|
+
if (!specAbsolutePath) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
return parseVoratiqFrontmatter(await readFile(specAbsolutePath, "utf8"))
|
|
309
|
+
.bodyContent;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function stripVoratiqFrontmatter(frontmatterBlock) {
|
|
316
|
+
const strippedHeader = removeTopLevelYamlBlock(frontmatterBlock.headerContent, "voratiq", frontmatterBlock.lineBreak);
|
|
317
|
+
if (strippedHeader.trim().length === 0) {
|
|
318
|
+
return frontmatterBlock.bodyContent;
|
|
319
|
+
}
|
|
320
|
+
return ["---", strippedHeader, "---", frontmatterBlock.bodyContent].join(frontmatterBlock.lineBreak);
|
|
321
|
+
}
|
|
322
|
+
function hashSpecBody(content) {
|
|
323
|
+
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
324
|
+
}
|
|
325
|
+
function toRecordedSpecSourceDescriptor(options) {
|
|
326
|
+
const { sessionId, agent } = options;
|
|
327
|
+
if (typeof agent.outputPath !== "string" ||
|
|
328
|
+
typeof agent.contentHash !== "string") {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: "spec",
|
|
333
|
+
sessionId,
|
|
334
|
+
agentId: agent.agentId,
|
|
335
|
+
outputPath: normalizePathForDisplay(agent.outputPath),
|
|
336
|
+
contentHash: agent.contentHash,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function toRunSpecSourceHint(source) {
|
|
340
|
+
return {
|
|
341
|
+
kind: "spec",
|
|
342
|
+
sessionId: source.sessionId,
|
|
343
|
+
agentId: source.agentId,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function removeTopLevelYamlBlock(headerContent, key, lineBreak) {
|
|
347
|
+
const lines = headerContent.split(lineBreak);
|
|
348
|
+
const startIndex = lines.findIndex((line) => new RegExp(`^${key}\\s*:(?:[ \\t]*(?:#.*)?)?$`, "u").test(line));
|
|
349
|
+
if (startIndex === -1) {
|
|
350
|
+
return headerContent;
|
|
351
|
+
}
|
|
352
|
+
let endIndex = startIndex + 1;
|
|
353
|
+
while (endIndex < lines.length) {
|
|
354
|
+
const currentLine = lines[endIndex];
|
|
355
|
+
if (currentLine === undefined) {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
if (currentLine.trim().length === 0) {
|
|
359
|
+
const nextNonEmptyIndex = findNextNonEmptyLineIndex(lines, endIndex + 1);
|
|
360
|
+
if (nextNonEmptyIndex !== undefined &&
|
|
361
|
+
/^[ \t]/u.test(lines[nextNonEmptyIndex] ?? "")) {
|
|
362
|
+
endIndex += 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
if (!/^[ \t]/u.test(currentLine)) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
endIndex += 1;
|
|
371
|
+
}
|
|
372
|
+
return [...lines.slice(0, startIndex), ...lines.slice(endIndex)].join(lineBreak);
|
|
373
|
+
}
|
|
374
|
+
function findNextNonEmptyLineIndex(lines, startIndex) {
|
|
375
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
376
|
+
if ((lines[index] ?? "").trim().length > 0) {
|
|
377
|
+
return index;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
function isObject(value) {
|
|
383
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
384
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { AgentDefinition } from "../../configs/agents/types.js";
|
|
2
2
|
import type { EnvironmentConfig } from "../../configs/environment/types.js";
|
|
3
|
+
import type { RunSpecTarget } from "../../domain/run/model/types.js";
|
|
3
4
|
export interface ValidationInput {
|
|
4
5
|
readonly root: string;
|
|
5
6
|
readonly specAbsolutePath: string;
|
|
7
|
+
readonly specDisplayPath?: string;
|
|
8
|
+
readonly specsFilePath?: string;
|
|
6
9
|
readonly resolvedAgentIds?: readonly string[];
|
|
7
10
|
readonly maxParallel?: number;
|
|
8
11
|
}
|
|
9
12
|
export interface ValidationResult {
|
|
10
13
|
readonly specContent: string;
|
|
14
|
+
readonly specTarget: RunSpecTarget;
|
|
11
15
|
readonly baseRevisionSha: string;
|
|
12
16
|
readonly agents: readonly AgentDefinition[];
|
|
13
17
|
readonly effectiveMaxParallel: number;
|