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.
Files changed (149) hide show
  1. package/README.md +18 -22
  2. package/dist/agents/launch/chat.d.ts +3 -1
  3. package/dist/agents/launch/chat.js +2 -0
  4. package/dist/bin.js +28 -7
  5. package/dist/cli/auto.js +1 -0
  6. package/dist/cli/contract.d.ts +26 -17
  7. package/dist/cli/contract.js +3 -1
  8. package/dist/cli/doctor.d.ts +12 -0
  9. package/dist/cli/doctor.js +115 -0
  10. package/dist/cli/list.js +4 -1
  11. package/dist/cli/operator-envelope.d.ts +19 -6
  12. package/dist/cli/operator-envelope.js +61 -1
  13. package/dist/cli/run.js +2 -0
  14. package/dist/cli/verify.d.ts +1 -1
  15. package/dist/cli/verify.js +48 -9
  16. package/dist/commands/auto/command.d.ts +1 -0
  17. package/dist/commands/auto/command.js +22 -12
  18. package/dist/commands/auto/errors.js +1 -1
  19. package/dist/commands/doctor/agents.d.ts +5 -0
  20. package/dist/commands/{init → doctor}/agents.js +37 -19
  21. package/dist/commands/doctor/command.d.ts +22 -0
  22. package/dist/commands/doctor/command.js +99 -0
  23. package/dist/commands/doctor/environment.d.ts +2 -0
  24. package/dist/commands/{init → doctor}/environment.js +38 -6
  25. package/dist/commands/{init/types.d.ts → doctor/fix-types.d.ts} +30 -9
  26. package/dist/commands/doctor/fix.d.ts +2 -0
  27. package/dist/commands/{init/command.js → doctor/fix.js} +106 -10
  28. package/dist/commands/doctor/reconcile.d.ts +2 -0
  29. package/dist/commands/doctor/reconcile.js +101 -0
  30. package/dist/commands/interactive/lifecycle.d.ts +2 -0
  31. package/dist/commands/interactive/lifecycle.js +8 -0
  32. package/dist/commands/list/command.d.ts +1 -0
  33. package/dist/commands/list/command.js +211 -352
  34. package/dist/commands/list/normalization.d.ts +56 -0
  35. package/dist/commands/list/normalization.js +317 -0
  36. package/dist/commands/message/command.d.ts +2 -1
  37. package/dist/commands/message/command.js +35 -14
  38. package/dist/commands/message/errors.d.ts +12 -3
  39. package/dist/commands/message/errors.js +19 -3
  40. package/dist/commands/reduce/command.js +16 -17
  41. package/dist/commands/reduce/errors.d.ts +2 -2
  42. package/dist/commands/reduce/errors.js +3 -3
  43. package/dist/commands/reduce/targets.js +11 -2
  44. package/dist/commands/root-launcher/command.js +12 -6
  45. package/dist/commands/run/command.d.ts +1 -0
  46. package/dist/commands/run/command.js +4 -1
  47. package/dist/commands/run/record-init.d.ts +2 -0
  48. package/dist/commands/run/record-init.js +2 -1
  49. package/dist/commands/run/spec-provenance.d.ts +37 -0
  50. package/dist/commands/run/spec-provenance.js +384 -0
  51. package/dist/commands/run/validation.d.ts +4 -0
  52. package/dist/commands/run/validation.js +25 -62
  53. package/dist/commands/spec/command.js +19 -6
  54. package/dist/commands/spec/errors.d.ts +5 -0
  55. package/dist/commands/spec/errors.js +9 -0
  56. package/dist/commands/verify/agents.d.ts +4 -2
  57. package/dist/commands/verify/agents.js +4 -11
  58. package/dist/commands/verify/command.js +15 -5
  59. package/dist/commands/verify/errors.d.ts +12 -0
  60. package/dist/commands/verify/errors.js +22 -0
  61. package/dist/commands/verify/targets.js +108 -12
  62. package/dist/competition/shared/preflight.d.ts +1 -1
  63. package/dist/competition/shared/preflight.js +15 -2
  64. package/dist/contracts/list.d.ts +129 -149
  65. package/dist/contracts/list.js +47 -99
  66. package/dist/domain/interactive/persistence/adapter.d.ts +23 -0
  67. package/dist/domain/interactive/persistence/adapter.js +42 -0
  68. package/dist/domain/message/model/types.d.ts +32 -0
  69. package/dist/domain/message/model/types.js +25 -0
  70. package/dist/domain/reduce/competition/adapter.js +21 -7
  71. package/dist/domain/reduce/competition/finalize.d.ts +7 -0
  72. package/dist/domain/reduce/competition/finalize.js +19 -0
  73. package/dist/domain/reduce/model/types.d.ts +3 -3
  74. package/dist/domain/run/competition/agents/artifacts.js +4 -2
  75. package/dist/domain/run/competition/errors.d.ts +1 -1
  76. package/dist/domain/run/competition/errors.js +4 -7
  77. package/dist/domain/run/model/types.d.ts +384 -0
  78. package/dist/domain/run/model/types.js +87 -0
  79. package/dist/domain/spec/competition/adapter.d.ts +1 -0
  80. package/dist/domain/spec/competition/adapter.js +6 -1
  81. package/dist/domain/spec/model/types.d.ts +3 -0
  82. package/dist/domain/spec/model/types.js +5 -0
  83. package/dist/domain/verify/competition/finalize.d.ts +9 -0
  84. package/dist/domain/verify/competition/finalize.js +22 -3
  85. package/dist/domain/verify/model/types.d.ts +2 -2
  86. package/dist/interactive/providers/mcp.d.ts +1 -0
  87. package/dist/interactive/providers/mcp.js +45 -7
  88. package/dist/interactive/substrate.js +20 -3
  89. package/dist/mcp/server.js +26 -9
  90. package/dist/policy/verification.js +18 -1
  91. package/dist/preflight/agents.d.ts +24 -0
  92. package/dist/preflight/agents.js +71 -0
  93. package/dist/preflight/environment.d.ts +6 -0
  94. package/dist/preflight/environment.js +17 -0
  95. package/dist/preflight/formatting.d.ts +5 -0
  96. package/dist/preflight/formatting.js +20 -0
  97. package/dist/preflight/index.d.ts +2 -0
  98. package/dist/preflight/index.js +5 -9
  99. package/dist/preflight/operator.d.ts +32 -0
  100. package/dist/preflight/operator.js +40 -0
  101. package/dist/preflight/settings.d.ts +2 -0
  102. package/dist/preflight/settings.js +17 -0
  103. package/dist/render/transcripts/interactive.d.ts +16 -0
  104. package/dist/render/transcripts/interactive.js +42 -0
  105. package/dist/render/transcripts/list.d.ts +41 -0
  106. package/dist/render/transcripts/list.js +152 -3
  107. package/dist/render/transcripts/message.d.ts +2 -1
  108. package/dist/render/transcripts/message.js +19 -20
  109. package/dist/render/transcripts/reduce.d.ts +1 -0
  110. package/dist/render/transcripts/reduce.js +21 -21
  111. package/dist/render/transcripts/root-launcher.js +2 -12
  112. package/dist/render/transcripts/run.d.ts +3 -0
  113. package/dist/render/transcripts/run.js +30 -4
  114. package/dist/render/transcripts/spec.js +5 -8
  115. package/dist/render/transcripts/verify.d.ts +5 -3
  116. package/dist/render/transcripts/verify.js +44 -31
  117. package/dist/render/utils/duration.d.ts +5 -0
  118. package/dist/render/utils/duration.js +6 -0
  119. package/dist/render/utils/runs.d.ts +1 -0
  120. package/dist/render/utils/runs.js +1 -0
  121. package/dist/render/utils/transcript-shell.d.ts +2 -1
  122. package/dist/render/utils/transcript-shell.js +19 -6
  123. package/dist/utils/errors.d.ts +2 -1
  124. package/dist/utils/errors.js +3 -1
  125. package/dist/utils/git.d.ts +1 -1
  126. package/dist/utils/git.js +25 -2
  127. package/dist/utils/list-target.d.ts +4 -0
  128. package/dist/utils/list-target.js +35 -0
  129. package/dist/utils/terminal.d.ts +1 -0
  130. package/dist/utils/terminal.js +11 -0
  131. package/dist/workspace/chat/artifacts.d.ts +7 -0
  132. package/dist/workspace/chat/artifacts.js +94 -3
  133. package/dist/workspace/errors.js +2 -2
  134. package/dist/workspace/managed-state.d.ts +32 -0
  135. package/dist/workspace/managed-state.js +103 -0
  136. package/dist/workspace/setup.js +66 -2
  137. package/dist/workspace/shim.d.ts +1 -0
  138. package/dist/workspace/shim.js +3 -3
  139. package/dist/workspace/structure.d.ts +1 -0
  140. package/dist/workspace/structure.js +1 -0
  141. package/package.json +2 -2
  142. package/dist/cli/init.d.ts +0 -15
  143. package/dist/cli/init.js +0 -70
  144. package/dist/commands/init/agents.d.ts +0 -4
  145. package/dist/commands/init/command.d.ts +0 -2
  146. package/dist/commands/init/environment.d.ts +0 -2
  147. package/dist/render/transcripts/init.d.ts +0 -7
  148. package/dist/render/transcripts/init.js +0 -83
  149. /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 missing = await findMissingVerificationArtifacts(root, record.methods);
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 missing = await findMissingReductionArtifacts(root, record.reducers);
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
- writeLauncherNotice(writeOutput, renderRootLauncherSingleAgentScreen({
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
- writeLauncherNotice(writeOutput, renderRootLauncherSelectionScreen({
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]);
@@ -4,6 +4,7 @@ import type { RunProgressRenderer } from "../../render/transcripts/run.js";
4
4
  export interface RunCommandInput {
5
5
  root: string;
6
6
  runsFilePath: string;
7
+ specsFilePath?: string;
7
8
  specAbsolutePath: string;
8
9
  specDisplayPath: string;
9
10
  agentIds?: readonly string[];
@@ -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;