@inflexa-ai/harness 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@ import { createWorkingMemory } from "../memory/working-memory.js";
32
32
  import { conversationPrompt } from "../prompts/conversation.js";
33
33
  import { composeSystemPrompt } from "./system-prompt.js";
34
34
  // Bio-lookup leaf tools (pure — no dependencies).
35
- import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, createSearchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
35
+ import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchGwasCatalogTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, createSearchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
36
36
  import { createNcbiTools, createChemDbTools } from "../tools/bio/keys.js";
37
37
  // Dependency-bearing tool factories.
38
38
  import { createGeneratePlanTool, createLiteratureReviewerTool, createInspectRunTool, createInspectDataProfileTool, createGenerateAnalogyReportTool, } from "../tools/research/index.js";
@@ -68,6 +68,8 @@ export function createConversationAgent(deps) {
68
68
  pubchemTool,
69
69
  // Translational medicine.
70
70
  openTargetsTool,
71
+ // Genetic evidence — SNP-trait associations for target validation / MR support.
72
+ searchGwasCatalogTool,
71
73
  searchPharmgkbTool,
72
74
  searchFaersTool,
73
75
  searchClinicalTrialsTool,
@@ -30,6 +30,13 @@ import type { ChatProvider } from "../providers/types.js";
30
30
  import type { RunSession } from "../auth/types.js";
31
31
  import { type Tool } from "../tools/define-tool.js";
32
32
  import type { BioToolKeys } from "../tools/bio/keys.js";
33
+ /**
34
+ * The synthesizer's agent identity — the `source.agentId` on its loop events
35
+ * and the `agent_id` of the run-phase `synthesis` row `executeAnalysis` writes
36
+ * to `cortex_step_executions`. One definition so the ledger row and the stream
37
+ * events can never disagree about who did the work.
38
+ */
39
+ export declare const SYNTHESIS_AGENT_ID = "run-synthesizer";
33
40
  type SynthesizerOutcome = {
34
41
  kind: "submitted";
35
42
  synthesis: RunSynthesis;
@@ -39,7 +39,13 @@ import { defineTool } from "../tools/define-tool.js";
39
39
  import { createReportBlockerToolFor } from "../tools/sandbox/report-blocker.js";
40
40
  import { createLiteratureReviewerTool } from "../tools/research/literature-reviewer.js";
41
41
  // ── Agent identity / budgets ────────────────────────────────────────
42
- const AGENT_ID = "run-synthesizer";
42
+ /**
43
+ * The synthesizer's agent identity — the `source.agentId` on its loop events
44
+ * and the `agent_id` of the run-phase `synthesis` row `executeAnalysis` writes
45
+ * to `cortex_step_executions`. One definition so the ledger row and the stream
46
+ * events can never disagree about who did the work.
47
+ */
48
+ export const SYNTHESIS_AGENT_ID = "run-synthesizer";
43
49
  /**
44
50
  * Budget for the synthesizer loop: read summaries + 1–3 reviewer delegations
45
51
  * + draft + 1–2 submit/fix cycles + headroom.
@@ -349,7 +355,7 @@ export async function generateRunSynthesis(input) {
349
355
  const blockerTool = buildBlockerTool(holder);
350
356
  const tools = [validateTool, submitTool, blockerTool, reviewer];
351
357
  const agent = {
352
- id: AGENT_ID,
358
+ id: SYNTHESIS_AGENT_ID,
353
359
  systemPrompt: composeSystemPrompt(synthesisAgentPrompt),
354
360
  model: input.model,
355
361
  tools,
@@ -13,14 +13,17 @@
13
13
  */
14
14
  import { KNOWN_AGENT_IDS } from "../agents/sandbox-catalog.js";
15
15
  import { CycleError, DependencyError, topoSortIntoWaves } from "../execution/topo-sort.js";
16
- import { isSafeId, STEP_SUBDIRS } from "../workspace/paths.js";
16
+ import { isSafeId, STEP_SUBDIRS, SYNTHESIS_STEP_ID } from "../workspace/paths.js";
17
17
  const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
18
18
  /**
19
19
  * Reserved step-id names. A step id equal to an artifact subdirectory name
20
20
  * would make its directory (`runs/{runId}/{stepId}`) collide with the
21
- * subdirectory convention agents expect inside a step (see the harness-workspace-tools spec).
21
+ * subdirectory convention agents expect inside a step, and
22
+ * {@link SYNTHESIS_STEP_ID} is the run-phase ledger row `executeAnalysis`
23
+ * writes for run-level synthesis — a plan step with that id would collide with
24
+ * the row's `(run_id, step_id)` primary key (see the harness-workspace-tools spec).
22
25
  */
23
- const RESERVED_STEP_IDS = new Set(STEP_SUBDIRS);
26
+ const RESERVED_STEP_IDS = new Set([...STEP_SUBDIRS, SYNTHESIS_STEP_ID]);
24
27
  /** Derive a filesystem-safe output prefix from a step ID. */
25
28
  export function deriveOutputPrefix(stepId) {
26
29
  return stepId
@@ -97,11 +100,13 @@ export function validatePlan(plan, options) {
97
100
  `${ceiling.maxMemoryGb} per step — reduce memoryGb or restructure the step`);
98
101
  }
99
102
  }
100
- // 5. Reserved step-id names (collide with artifact subdirectories)
103
+ // 5. Reserved step-id names (collide with artifact subdirectories or the
104
+ // run-phase synthesis ledger row)
101
105
  for (const step of plan.steps) {
102
106
  if (RESERVED_STEP_IDS.has(step.id.toLowerCase())) {
103
107
  errors.push(`Step "${step.id}" uses a reserved name — step ids must not be one of: ` +
104
- `${STEP_SUBDIRS.join(", ")} (they collide with the artifact subdirectory convention)`);
108
+ `${[...RESERVED_STEP_IDS].join(", ")} (the artifact subdirectory names collide with the ` +
109
+ `step-directory convention; "${SYNTHESIS_STEP_ID}" is the run-level synthesis phase)`);
105
110
  }
106
111
  }
107
112
  // 6. Step-id path safety. The id becomes the `runs/{runId}/{stepId}` directory
@@ -16,6 +16,7 @@ import { ok } from "neverthrow";
16
16
  import { z } from "zod";
17
17
  import { queryRun, queryRunsByAnalysis, queryStepsByRun } from "../../state/index.js";
18
18
  import { scopeResource } from "../../auth/types.js";
19
+ import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
19
20
  import { unwrapOrThrow } from "../../lib/result.js";
20
21
  import { defineTool } from "../define-tool.js";
21
22
  function formatRun(r, verbose) {
@@ -73,9 +74,15 @@ export function createInspectRunTool(pool) {
73
74
  agentId: s.agentId,
74
75
  wave: s.wave,
75
76
  status: s.status,
76
- // Seeded rows exist before a step ever runs; a summary path for one
77
- // would point the agent at a file that cannot exist yet (or ever).
78
- ...(s.status === "pending" || s.status === "skipped" ? {} : { summaryPath: `runs/${input.runId}/${s.stepId}/output/summary.md` }),
77
+ // No per-step `summaryPath` when the file cannot exist: a seeded row
78
+ // (`pending`/`skipped`) never produced output, and the reserved
79
+ // `synthesis` phase is not a sandbox step — it writes the run-level
80
+ // `synthesis.json`, surfaced as the run's `synthesisPath`, not a
81
+ // `{stepId}/output/summary.md`. Emitting the per-step path for it would
82
+ // point the agent at a file that never exists.
83
+ ...(s.status === "pending" || s.status === "skipped" || s.stepId === SYNTHESIS_STEP_ID
84
+ ? {}
85
+ : { summaryPath: `runs/${input.runId}/${s.stepId}/output/summary.md` }),
79
86
  ...(verbose
80
87
  ? {
81
88
  durationMs: s.durationMs,
@@ -30,6 +30,11 @@ type ReadFileOutput = {
30
30
  } | {
31
31
  status: "out_of_scope";
32
32
  path: string;
33
+ } | {
34
+ status: "binary";
35
+ path: string;
36
+ mode: "head" | "tail" | "full";
37
+ totalSize?: number;
33
38
  };
34
39
  export declare function createReadFileTool(fs: WorkspaceFilesystem, workingDir?: string): import("../define-tool.js").Tool<{
35
40
  path: string;
@@ -13,6 +13,18 @@ import { defineTool } from "../define-tool.js";
13
13
  import { unwrapOrThrow } from "../../lib/result.js";
14
14
  /** Default cap — large enough for typical CSV/MD/JSON, small enough to keep tool results out of context-blowing territory. */
15
15
  const DEFAULT_MAX_BYTES = 256 * 1024;
16
+ /**
17
+ * Treat returned content as binary when it contains a NUL byte — `grep`'s
18
+ * `looksBinary` heuristic, applied here to the whole returned window rather than
19
+ * a 1 KiB probe. These exact bytes are what would be UTF-8-decoded into the
20
+ * transcript, and a NUL both renders that decode as lossy garbage (useless to
21
+ * the model) and cannot be persisted into the JSONB message store — Postgres
22
+ * rejects U+0000 with 22P05, failing the whole turn's append. Reporting the
23
+ * read as `binary` keeps a NUL out of both.
24
+ */
25
+ function looksBinary(content) {
26
+ return content.includes(0);
27
+ }
16
28
  const ReadFileInputSchema = z.object({
17
29
  path: z
18
30
  .string()
@@ -45,8 +57,10 @@ export function createReadFileTool(fs, workingDir) {
45
57
  "as UTF-8 text. Output is capped at " +
46
58
  `${DEFAULT_MAX_BYTES} bytes; pass headLines or tailLines to read a ` +
47
59
  "specific window of a large file (typical for bio CSV/TSV/log files). " +
48
- "Oversize reads come back truncated with a marker. Missing paths and " +
49
- "paths outside the analysis tree return a data variant — not an error.",
60
+ "Oversize reads come back truncated with a marker. This tool is for " +
61
+ "text; a binary file (one holding NUL bytes, e.g. a .gz or image) comes " +
62
+ "back as a `binary` data variant rather than decoded garbage. Missing " +
63
+ "paths and paths outside the analysis tree return a data variant — not an error.",
50
64
  inputSchema: ReadFileInputSchema,
51
65
  execute: async ({ path, headLines, tailLines }, ctx) => {
52
66
  if (headLines !== undefined && tailLines !== undefined) {
@@ -67,6 +81,8 @@ export function createReadFileTool(fs, workingDir) {
67
81
  const mode = headLines !== undefined ? "head" : tailLines !== undefined ? "tail" : "full";
68
82
  switch (result.kind) {
69
83
  case "ok":
84
+ if (looksBinary(result.content))
85
+ return ok({ status: "binary", path, mode });
70
86
  return ok({
71
87
  status: "ok",
72
88
  path,
@@ -74,6 +90,8 @@ export function createReadFileTool(fs, workingDir) {
74
90
  content: result.content.toString("utf8"),
75
91
  });
76
92
  case "truncated":
93
+ if (looksBinary(result.content))
94
+ return ok({ status: "binary", path, mode, totalSize: result.totalSize });
77
95
  return ok({
78
96
  status: "truncated",
79
97
  path,
@@ -63,7 +63,7 @@ import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
63
63
  import type { RunSession } from "../auth/types.js";
64
64
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
65
65
  import type { Logger } from "../lib/logger.js";
66
- import type { SynthesisStatus } from "../state/index.js";
66
+ import type { SynthesisStatus, UpdateStepExecutionInput } from "../state/index.js";
67
67
  import type { AnalysisStep } from "../schemas/workflow-state.js";
68
68
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
69
69
  import type { BioToolKeys } from "../tools/bio/keys.js";
@@ -288,6 +288,19 @@ export declare function composeStepSeed(args: {
288
288
  runId: string;
289
289
  deps: ExecuteAnalysisDeps;
290
290
  }): Promise<string>;
291
+ /**
292
+ * Project a classified synthesis outcome onto the run-phase `synthesis` ledger
293
+ * row's terminal update (see the run-synthesis-outcome spec). The blocker keeps
294
+ * its reason in `blockedReason` — an honest agent-declared blocker, not an
295
+ * error — and a failure carries the same reason string
296
+ * `persist-synthesis-outcome` writes to `cortex_runs.synthesis_reason`, so the
297
+ * two ledgers cannot disagree about why synthesis died. Exported for the
298
+ * projection tests (the `buildChildInput` pattern).
299
+ */
300
+ export declare function synthesisRowUpdate(outcome: {
301
+ status: SynthesisStatus;
302
+ reason: string | null;
303
+ }, durationMs: number): UpdateStepExecutionInput;
291
304
  /**
292
305
  * Run-level literature-grounded synthesis (no sandbox). Returns the
293
306
  * quick-display findings for the run-completed card; empty when synthesis is
@@ -58,17 +58,17 @@
58
58
  * - WRITES NOTHING to the conversation thread (results are pull-only
59
59
  * via `inspectRun`)
60
60
  */
61
- import { DBOS } from "@dbos-inc/dbos-sdk";
61
+ import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
62
62
  import { mkdir } from "node:fs/promises";
63
63
  import { join } from "node:path";
64
64
  import { forStep } from "../auth/types.js";
65
65
  import { createNoopLogger } from "../lib/console-logger.js";
66
66
  import { unwrapOrThrow } from "../lib/result.js";
67
- import { RunDedupCollisionError, countArtifactsForRun, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, } from "../state/index.js";
67
+ import { RunDedupCollisionError, countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
68
68
  import { isBudgetExceeded } from "../loop/budget-exceeded.js";
69
- import { loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
69
+ import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
70
70
  import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing } from "../prompts/briefing.js";
71
- import { runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
71
+ import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
72
72
  import { isChatDataPart } from "../sandbox/sandbox-step-translate.js";
73
73
  import { synthesizeRun } from "../app/synthesize-run.js";
74
74
  import { computeTopologicalLevels, scheduleReady, validatePlanDag } from "./execute-analysis-scheduler.js";
@@ -222,6 +222,31 @@ async function collectUpstreamHandoffs(args) {
222
222
  }
223
223
  return handoffs;
224
224
  }
225
+ /**
226
+ * Project a classified synthesis outcome onto the run-phase `synthesis` ledger
227
+ * row's terminal update (see the run-synthesis-outcome spec). The blocker keeps
228
+ * its reason in `blockedReason` — an honest agent-declared blocker, not an
229
+ * error — and a failure carries the same reason string
230
+ * `persist-synthesis-outcome` writes to `cortex_runs.synthesis_reason`, so the
231
+ * two ledgers cannot disagree about why synthesis died. Exported for the
232
+ * projection tests (the `buildChildInput` pattern).
233
+ */
234
+ export function synthesisRowUpdate(outcome, durationMs) {
235
+ switch (outcome.status) {
236
+ case "produced":
237
+ return { status: "completed", durationMs };
238
+ case "skipped_no_summaries":
239
+ return { status: "skipped", durationMs };
240
+ case "skipped_blocker":
241
+ return { status: "blocked", durationMs, blockedReason: outcome.reason };
242
+ case "failed":
243
+ return { status: "failed", durationMs, error: outcome.reason };
244
+ default: {
245
+ const _exhaustive = outcome.status;
246
+ throw new Error(`unhandled synthesis status: ${String(_exhaustive)}`);
247
+ }
248
+ }
249
+ }
225
250
  /**
226
251
  * Run-level literature-grounded synthesis (no sandbox). Returns the
227
252
  * quick-display findings for the run-completed card; empty when synthesis is
@@ -309,14 +334,32 @@ export async function runExecuteAnalysisBody(input, deps) {
309
334
  // The seed's DO NOTHING conflict action makes this replay-safe; the agent
310
335
  // fallback mirrors `buildChildInput` so seed and mark-running agree on the
311
336
  // row a step's child later upserts.
337
+ //
338
+ // The run-phase `synthesis` row is seeded alongside the DAG — gated on
339
+ // enablement ONLY, because the other half of the run gate for actually
340
+ // executing synthesis (`final.completed.size > 0`) is unknowable until the
341
+ // scheduler returns. A seeded row whose gate later fails simply stays
342
+ // `pending` and the terminal sweep finalizes it to `skipped` — honest
343
+ // ("never ran, never will"), like any never-dispatched DAG step. Its wave
344
+ // sits after every DAG level so ledger-ordered readers render it last.
345
+ const synthesisEnabled = deps.synthesisEnabled ?? true;
346
+ const synthesisWave = Math.max(-1, ...levels.values()) + 1;
312
347
  await DBOS.runStep(async () => {
313
- unwrapOrThrow(await seedStepExecutions(deps.pool, input.steps.map((step) => ({
348
+ const dagRows = input.steps.map((step) => ({
314
349
  runId,
315
350
  stepId: step.id,
316
351
  analysisId: input.analysisId,
317
352
  wave: levels.get(step.id) ?? 0,
318
353
  agentId: input.agentByStepId[step.id] ?? "scientific-executor",
319
- }))));
354
+ }));
355
+ const synthesisRow = {
356
+ runId,
357
+ stepId: SYNTHESIS_STEP_ID,
358
+ analysisId: input.analysisId,
359
+ wave: synthesisWave,
360
+ agentId: SYNTHESIS_AGENT_ID,
361
+ };
362
+ unwrapOrThrow(await seedStepExecutions(deps.pool, synthesisEnabled ? [...dagRows, synthesisRow] : dagRows));
320
363
  }, { name: "seed-step-executions" });
321
364
  // One checkpointed clock read stamps the run's start; the terminal read in
322
365
  // `collectAndComplete` subtracts it for the true run span. Checkpointed →
@@ -352,10 +395,37 @@ export async function runExecuteAnalysisBody(input, deps) {
352
395
  let synthesisError = null;
353
396
  let synthesisFindings = [];
354
397
  // Null when synthesis never ran (disabled or no completed steps): the ledger
355
- // columns stay NULL — "unknown", not a recorded skip.
398
+ // columns stay NULL — "unknown", not a recorded skip — and the seeded
399
+ // `synthesis` row (if any) stays `pending` for the terminal sweep.
356
400
  let synthesisOutcome = null;
357
- const synthesisEnabled = deps.synthesisEnabled ?? true;
358
401
  if (synthesisEnabled && final.completed.size > 0) {
402
+ // Bracketing clock reads for the row's duration_ms — checkpointed, so a
403
+ // recovered replay reuses the original instants (same rule as `startedAtMs`).
404
+ const synthStartedAtMs = await DBOS.now();
405
+ // Mark the seeded `synthesis` row running. The same upsert the DAG children
406
+ // use: it stamps started_at, and it CREATES the row outright when the seed
407
+ // never carried it (synthesisEnabled flipped true across a recovery
408
+ // redeploy) — the mark self-heals rather than trusting the seed's snapshot
409
+ // of the config. Log-don't-fail: a progress row must never fail an
410
+ // otherwise-healthy run. Cancellation still re-propagates — it is not a
411
+ // write failure, and swallowing it here would only defer it one operation.
412
+ try {
413
+ await DBOS.runStep(async () => {
414
+ unwrapOrThrow(await insertStepExecution(deps.pool, {
415
+ runId,
416
+ stepId: SYNTHESIS_STEP_ID,
417
+ analysisId: input.analysisId,
418
+ wave: synthesisWave,
419
+ agentId: SYNTHESIS_AGENT_ID,
420
+ childWorkflowId: null,
421
+ }));
422
+ }, { name: "mark-synthesis-running" });
423
+ }
424
+ catch (err) {
425
+ if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
426
+ throw err;
427
+ logger.error("mark-synthesis-running failed", { runId, ...logger.errorFields(err) });
428
+ }
359
429
  try {
360
430
  const synthOut = await DBOS.runStep(() => synthesizeFindings({
361
431
  analysisId: input.analysisId,
@@ -368,10 +438,37 @@ export async function runExecuteAnalysisBody(input, deps) {
368
438
  synthesisOutcome = { status: synthOut.synthesisStatus, reason: synthOut.synthesisReason };
369
439
  }
370
440
  catch (err) {
441
+ // Cancellation is not a synthesis outcome: re-propagate unchanged (the
442
+ // sandbox-step rule), so a cancelled workflow is never recorded as a
443
+ // synthesis failure — neither on the row nor in cortex_runs. The row is
444
+ // deliberately left `running`: `collectAndComplete` never executes on
445
+ // this path either, so the run row is equally non-terminal and the pair
446
+ // stays consistent — the pre-existing cancelled-mid-flight wedge shape
447
+ // `inflexa run` already detects via its DBOS-status cross-check.
448
+ if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
449
+ throw err;
371
450
  synthesisError = err;
372
451
  synthesisOutcome = { status: "failed", reason: err instanceof Error ? err.message : String(err) };
373
452
  logger.error("synthesizeFindings failed — failing the run", { runId, ...logger.errorFields(err) });
374
453
  }
454
+ // Terminal stamp for the `synthesis` row — deliberately BEFORE
455
+ // `collectAndComplete` writes the run row's terminal status, so no reader
456
+ // can observe a terminal run beside a still-`running` synthesis row. Both
457
+ // branches above set `synthesisOutcome` (or threw), so it is non-null here
458
+ // and the compiler narrows it — no guard needed. Log-don't-fail, like the
459
+ // sibling finalisation writes.
460
+ const rowOutcome = synthesisOutcome;
461
+ try {
462
+ const synthEndedAtMs = await DBOS.now();
463
+ await DBOS.runStep(async () => {
464
+ unwrapOrThrow(await updateStepExecution(deps.pool, runId, SYNTHESIS_STEP_ID, synthesisRowUpdate(rowOutcome, synthEndedAtMs - synthStartedAtMs)));
465
+ }, { name: "persist-synthesis-step" });
466
+ }
467
+ catch (err) {
468
+ if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
469
+ throw err;
470
+ logger.error("persist-synthesis-step failed", { runId, ...logger.errorFields(err) });
471
+ }
375
472
  }
376
473
  // (6) collectAndComplete — terminal block. Runs on EVERY path. When synthesis
377
474
  // failed, force a failed terminal status so the charge/run-authorization close correctly
@@ -181,5 +181,15 @@ export declare function stepSubdir(stepBase: string, type: StepSubdirType): stri
181
181
  export declare function reportDir(reportId: string): string;
182
182
  /** All standard subdirectory types for a step. */
183
183
  export declare const STEP_SUBDIRS: readonly StepSubdirType[];
184
+ /**
185
+ * Reserved id of the run-phase synthesis row in `cortex_step_executions` — the
186
+ * ledger row `executeAnalysis` writes for run-level synthesis so progress
187
+ * readers see the phase. Reserved here beside {@link STEP_SUBDIRS} because the
188
+ * reservation is a layout fact as much as a ledger one: a plan step with this
189
+ * id would collide with the row's primary key AND put its step directory
190
+ * `runs/{runId}/synthesis/` beside the run-level `runs/{runId}/synthesis.json`.
191
+ * Plan validation rejects it (see validate-plan).
192
+ */
193
+ export declare const SYNTHESIS_STEP_ID = "synthesis";
184
194
  /** Get all subdirectory paths for a step. */
185
195
  export declare function allStepSubdirs(stepBase: string): string[];
@@ -213,6 +213,16 @@ export function reportDir(reportId) {
213
213
  }
214
214
  /** All standard subdirectory types for a step. */
215
215
  export const STEP_SUBDIRS = ["scripts", "output", "figures", "logs", "notebooks"];
216
+ /**
217
+ * Reserved id of the run-phase synthesis row in `cortex_step_executions` — the
218
+ * ledger row `executeAnalysis` writes for run-level synthesis so progress
219
+ * readers see the phase. Reserved here beside {@link STEP_SUBDIRS} because the
220
+ * reservation is a layout fact as much as a ledger one: a plan step with this
221
+ * id would collide with the row's primary key AND put its step directory
222
+ * `runs/{runId}/synthesis/` beside the run-level `runs/{runId}/synthesis.json`.
223
+ * Plan validation rejects it (see validate-plan).
224
+ */
225
+ export const SYNTHESIS_STEP_ID = "synthesis";
216
226
  /** Get all subdirectory paths for a step. */
217
227
  export function allStepSubdirs(stepBase) {
218
228
  return STEP_SUBDIRS.map((type) => stepSubdir(stepBase, type));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",