@inflexa-ai/harness 0.23.0 → 0.25.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.
Files changed (123) hide show
  1. package/README.md +1 -2
  2. package/dist/agents/conversation-agent.d.ts +0 -9
  3. package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
  4. package/dist/agents/sandbox/data-profiler.js +5 -4
  5. package/dist/agents/sandbox/shared.js +11 -0
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/data-profile-orientation.js +30 -1
  8. package/dist/app/data-profile-policy.d.ts +33 -7
  9. package/dist/app/data-profile-policy.js +31 -9
  10. package/dist/contracts/chat-parts.d.ts +29 -33
  11. package/dist/contracts/content-url.d.ts +7 -41
  12. package/dist/contracts/content-url.js +7 -48
  13. package/dist/contracts/data-profile.d.ts +166 -0
  14. package/dist/contracts/index.d.ts +3 -2
  15. package/dist/contracts/index.js +1 -1
  16. package/dist/contracts/message.d.ts +1 -1
  17. package/dist/contracts/message.js +1 -1
  18. package/dist/contracts/part-registry.js +2 -3
  19. package/dist/contracts/schemas/chat-parts.d.ts +16 -53
  20. package/dist/contracts/schemas/chat-parts.js +15 -25
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +33 -0
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-authorizer.d.ts +1 -1
  26. package/dist/execution/run-authorizer.js +1 -1
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +5 -2
  29. package/dist/input-scan/coverage.d.ts +22 -0
  30. package/dist/input-scan/coverage.js +85 -0
  31. package/dist/input-scan/enrich.d.ts +41 -0
  32. package/dist/input-scan/enrich.js +227 -0
  33. package/dist/input-scan/formats.d.ts +41 -0
  34. package/dist/input-scan/formats.js +279 -0
  35. package/dist/input-scan/index.d.ts +12 -0
  36. package/dist/input-scan/index.js +11 -0
  37. package/dist/input-scan/scan.d.ts +50 -0
  38. package/dist/input-scan/scan.js +182 -0
  39. package/dist/input-scan/shapes.d.ts +46 -0
  40. package/dist/input-scan/shapes.js +0 -0
  41. package/dist/input-scan/types.d.ts +172 -0
  42. package/dist/input-scan/types.js +12 -0
  43. package/dist/lib/zod-issues.d.ts +2 -3
  44. package/dist/lib/zod-issues.js +2 -3
  45. package/dist/loop/__fixtures__/scripted-provider.js +3 -1
  46. package/dist/loop/run-agent.d.ts +5 -0
  47. package/dist/loop/run-agent.js +10 -1
  48. package/dist/loop/run-to-terminal.d.ts +1 -1
  49. package/dist/loop/run-to-terminal.js +1 -1
  50. package/dist/memory/card-builders.d.ts +0 -27
  51. package/dist/memory/card-builders.js +0 -75
  52. package/dist/memory/conversation-display-backfill.d.ts +2 -4
  53. package/dist/memory/conversation-display-backfill.js +3 -9
  54. package/dist/memory/conversation-display-storage.d.ts +3 -4
  55. package/dist/memory/conversation-display-storage.js +3 -4
  56. package/dist/memory/reconstruct-cards.d.ts +5 -7
  57. package/dist/memory/reconstruct-cards.js +6 -20
  58. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  59. package/dist/prompts/sandbox/data-profiler.js +159 -275
  60. package/dist/provenance/exec-frame.js +9 -3
  61. package/dist/runtime/boot.js +2 -12
  62. package/dist/sandbox/client.d.ts +7 -0
  63. package/dist/sandbox/create-sandbox.js +1 -0
  64. package/dist/sandbox/docker-client.d.ts +1 -0
  65. package/dist/sandbox/docker-client.js +20 -15
  66. package/dist/sandbox/k8s-client.d.ts +6 -5
  67. package/dist/sandbox/k8s-client.js +55 -39
  68. package/dist/sandbox/reaper.d.ts +24 -13
  69. package/dist/sandbox/reaper.js +47 -14
  70. package/dist/sandbox/types.d.ts +6 -5
  71. package/dist/schemas/data-profile-schemas.d.ts +62 -0
  72. package/dist/schemas/data-profile-schemas.js +68 -2
  73. package/dist/state/data-profile.d.ts +7 -88
  74. package/dist/state/index.d.ts +1 -1
  75. package/dist/tasks/data-profile-activity.d.ts +8 -0
  76. package/dist/tasks/data-profile-activity.js +1 -0
  77. package/dist/tasks/data-profile-index.d.ts +45 -0
  78. package/dist/tasks/data-profile-index.js +134 -0
  79. package/dist/tasks/data-profile.d.ts +8 -23
  80. package/dist/tasks/data-profile.js +150 -117
  81. package/dist/tools/report-session/preview-report.d.ts +2 -1
  82. package/dist/tools/report-session/preview-report.js +9 -1
  83. package/dist/tools/research/generate-plan.js +1 -1
  84. package/dist/tools/research/inspect-data-profile.d.ts +30 -4
  85. package/dist/tools/research/inspect-data-profile.js +66 -10
  86. package/dist/tools/start-report-session.js +2 -2
  87. package/dist/tools/workspace/scan-inputs.d.ts +38 -0
  88. package/dist/tools/workspace/scan-inputs.js +61 -0
  89. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  90. package/dist/tools/workspace/workspace-search.js +12 -9
  91. package/dist/workspace/filesystem.d.ts +27 -0
  92. package/dist/workspace/filesystem.js +27 -0
  93. package/dist/workspace/paths.d.ts +0 -21
  94. package/dist/workspace/paths.js +0 -37
  95. package/package.json +1 -3
  96. package/dist/agents/report-builder.d.ts +0 -34
  97. package/dist/agents/report-builder.js +0 -34
  98. package/dist/execution/report-render.d.ts +0 -39
  99. package/dist/execution/report-render.js +0 -98
  100. package/dist/execution/report-runner.d.ts +0 -111
  101. package/dist/execution/report-runner.js +0 -230
  102. package/dist/prompts/report-builder.d.ts +0 -1
  103. package/dist/prompts/report-builder.js +0 -174
  104. package/dist/tools/iterate-report.d.ts +0 -227
  105. package/dist/tools/iterate-report.js +0 -883
  106. package/dist/tools/lib/preview-lock.d.ts +0 -20
  107. package/dist/tools/lib/preview-lock.js +0 -35
  108. package/dist/tools/lib/report-preflight.d.ts +0 -39
  109. package/dist/tools/lib/report-preflight.js +0 -283
  110. package/dist/tools/report/build-report.d.ts +0 -13
  111. package/dist/tools/report/build-report.js +0 -27
  112. package/dist/tools/report/index.d.ts +0 -15
  113. package/dist/tools/report/index.js +0 -15
  114. package/dist/tools/report/mint-preview-url.d.ts +0 -26
  115. package/dist/tools/report/mint-preview-url.js +0 -47
  116. package/dist/tools/report/preview-publisher.d.ts +0 -53
  117. package/dist/tools/report/preview-publisher.js +0 -45
  118. package/dist/tools/report/preview-snapshot.d.ts +0 -34
  119. package/dist/tools/report/preview-snapshot.js +0 -98
  120. package/dist/tools/report/submit-report.d.ts +0 -26
  121. package/dist/tools/report/submit-report.js +0 -148
  122. package/dist/tools/report/version-fs.d.ts +0 -15
  123. package/dist/tools/report/version-fs.js +0 -165
@@ -1,39 +0,0 @@
1
- /**
2
- * Report renderer — Node-side Nunjucks render of v{N}/report.html.j2 to v{N}/index.html.
3
- *
4
- * Stateless pure module: no harness imports, no env reads at import time,
5
- * safe to run standalone in tests.
6
- */
7
- export interface RenderReportOptions {
8
- versionDir: string;
9
- templatesDir: string;
10
- }
11
- export interface RenderReportResult {
12
- ok: boolean;
13
- indexBytes: number;
14
- error?: {
15
- kind: "missing-template" | "syntax" | "runtime";
16
- message: string;
17
- line?: number;
18
- snippet?: string;
19
- };
20
- }
21
- /**
22
- * Render `${versionDir}/report.html.j2` to `${versionDir}/index.html`.
23
- *
24
- * Loader resolution order: [versionDir, templatesDir]. `report.html.j2`
25
- * lives in versionDir; `extends "base.html.j2"` and `include "components/..."`
26
- * resolve from templatesDir. Autoescape is OFF — templates own escaping.
27
- *
28
- * `${templatesDir}/echarts-theme.json` is read, parsed, re-stringified, and
29
- * passed as `echarts_theme` (a JSON string) for safe inlining inside a
30
- * `<script>` block.
31
- *
32
- * Returns `{ ok: true, indexBytes }` on success. On failure, returns
33
- * `{ ok: false, indexBytes: 0, error }` with `error.kind`:
34
- * - "missing-template" — `report.html.j2` (or any `extends`/`include` target)
35
- * not found by either loader. Message identifies the missing path.
36
- * - "syntax" — Nunjucks parse error in any loaded template.
37
- * - "runtime" — anything else thrown during render or write.
38
- */
39
- export declare function renderReport(opts: RenderReportOptions): Promise<RenderReportResult>;
@@ -1,98 +0,0 @@
1
- /**
2
- * Report renderer — Node-side Nunjucks render of v{N}/report.html.j2 to v{N}/index.html.
3
- *
4
- * Stateless pure module: no harness imports, no env reads at import time,
5
- * safe to run standalone in tests.
6
- */
7
- import { readFile, writeFile } from "node:fs/promises";
8
- import { join } from "node:path";
9
- import nunjucks from "nunjucks";
10
- /**
11
- * Render `${versionDir}/report.html.j2` to `${versionDir}/index.html`.
12
- *
13
- * Loader resolution order: [versionDir, templatesDir]. `report.html.j2`
14
- * lives in versionDir; `extends "base.html.j2"` and `include "components/..."`
15
- * resolve from templatesDir. Autoescape is OFF — templates own escaping.
16
- *
17
- * `${templatesDir}/echarts-theme.json` is read, parsed, re-stringified, and
18
- * passed as `echarts_theme` (a JSON string) for safe inlining inside a
19
- * `<script>` block.
20
- *
21
- * Returns `{ ok: true, indexBytes }` on success. On failure, returns
22
- * `{ ok: false, indexBytes: 0, error }` with `error.kind`:
23
- * - "missing-template" — `report.html.j2` (or any `extends`/`include` target)
24
- * not found by either loader. Message identifies the missing path.
25
- * - "syntax" — Nunjucks parse error in any loaded template.
26
- * - "runtime" — anything else thrown during render or write.
27
- */
28
- export async function renderReport(opts) {
29
- const { versionDir, templatesDir } = opts;
30
- let echartsThemeJson;
31
- try {
32
- const raw = await readFile(join(templatesDir, "echarts-theme.json"), "utf8");
33
- echartsThemeJson = JSON.stringify(JSON.parse(raw));
34
- }
35
- catch (err) {
36
- const msg = err instanceof Error ? err.message : String(err);
37
- const kind = isNotFound(err) ? "missing-template" : "runtime";
38
- return {
39
- ok: false,
40
- indexBytes: 0,
41
- error: { kind, message: `failed to load echarts-theme.json: ${msg}` },
42
- };
43
- }
44
- const env = new nunjucks.Environment(new nunjucks.FileSystemLoader([versionDir, templatesDir], {
45
- noCache: true,
46
- }), { autoescape: false, throwOnUndefined: false });
47
- let html;
48
- try {
49
- html = env.render("report.html.j2", { echarts_theme: echartsThemeJson });
50
- }
51
- catch (err) {
52
- return { ok: false, indexBytes: 0, error: classifyNunjucksError(err) };
53
- }
54
- try {
55
- const indexPath = join(versionDir, "index.html");
56
- await writeFile(indexPath, html, "utf8");
57
- return { ok: true, indexBytes: Buffer.byteLength(html, "utf8") };
58
- }
59
- catch (err) {
60
- const msg = err instanceof Error ? err.message : String(err);
61
- return {
62
- ok: false,
63
- indexBytes: 0,
64
- error: { kind: "runtime", message: `failed to write index.html: ${msg}` },
65
- };
66
- }
67
- }
68
- function isNotFound(err) {
69
- if (!err || typeof err !== "object")
70
- return false;
71
- const code = err.code;
72
- return code === "ENOENT";
73
- }
74
- function classifyNunjucksError(err) {
75
- const message = err instanceof Error ? err.message : String(err);
76
- if (/template not found/i.test(message)) {
77
- return { kind: "missing-template", message };
78
- }
79
- // Nunjucks tags parse errors with "Template render error" + "expected" /
80
- // "unexpected" / "parseError" markers; the Template error carries lineno.
81
- const lineno = err?.lineno;
82
- const colno = err?.colno;
83
- const line = typeof lineno === "number" ? lineno : undefined;
84
- const isSyntax = /parseError|expected|unexpected token|unexpected end of/i.test(message) || /Template render error.*line \d+/i.test(message);
85
- if (isSyntax) {
86
- const snippet = extractSnippet(message, line, typeof colno === "number" ? colno : undefined);
87
- return snippet ? { kind: "syntax", message, line, snippet } : line !== undefined ? { kind: "syntax", message, line } : { kind: "syntax", message };
88
- }
89
- return line !== undefined ? { kind: "runtime", message, line } : { kind: "runtime", message };
90
- }
91
- function extractSnippet(message, line, col) {
92
- if (!line)
93
- return undefined;
94
- const m = message.split("\n").find((l) => l.trim().length > 0 && !/^\s*at\s/.test(l));
95
- if (!m)
96
- return undefined;
97
- return col !== undefined ? `${m} (col ${col})` : m;
98
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Report runner — orchestrates an iterative report build entirely in the
3
- * Cortex Node process. No sandbox, no Python.
4
- *
5
- * Lifecycle:
6
- * 1. Resolve version (latest + 1, or baseVersion + 1)
7
- * 2. Prepare version dir (mkdir, copy v{base}/report.html.j2, symlink ../assets)
8
- * 3. Construct the 4 custom report tools + the in-process versionFs surface;
9
- * all of them close over the iteration's preview-dir state and a single
10
- * `outcome: ReportOutcome | undefined` variable (the closure-state
11
- * closure-state capture — see design.md Decision #9, mirrors
12
- * `harness/tools/research/generate-plan.ts`'s
13
- * `PlannerOutcome` capture).
14
- * 4. Build the `report-builder` AgentDefinition with that tool roster and
15
- * drive it via `runAgent` + `passthroughStep`.
16
- * 5. Verify the closure-captured `outcome` is `{ ok: true, ... }` (else
17
- * classify the failure, roll back the version dir, return a structured
18
- * failure).
19
- * 6. Return success or a structured failure.
20
- *
21
- * Concurrency: per-previewId serialization via `withPreviewLock` so two
22
- * iterations on the same preview don't race on shared assets/.
23
- *
24
- * Billing: the report-builder's LLM calls flow through the `ChatProvider`
25
- * seam with `forSubAgent(session, REPORT_BUILDER_AGENT_ID)` — the provider's
26
- * lazy `resolveBilling` fires at the call site. No ALS, no fetch-patch
27
- * (Decision #6).
28
- *
29
- * Preview surface: the `preview_snapshot` and `mint_preview_url` tools mint
30
- * a content-server URL through the injected `PreviewPublisher` seam. The
31
- * managed publisher owns the short-lived host credential; the local default
32
- * returns "unavailable" so the report still builds without a hosted preview.
33
- */
34
- import type { Pool } from "pg";
35
- import { type RequestSession } from "../auth/types.js";
36
- import type { EmitFn } from "../loop/types.js";
37
- import type { ChatProvider } from "../providers/types.js";
38
- import type { PreviewPublisher } from "../tools/report/preview-publisher.js";
39
- import type { ChromeConfig } from "../lib/chrome.js";
40
- import type { Logger } from "../lib/logger.js";
41
- import type { UsageRecorder } from "../billing/usage-recorder.js";
42
- import type { AgentRunUsage } from "../loop/metrics.js";
43
- export interface ReportRunnerDeps {
44
- /** Operational logging seam; omitted falls back to no-op. */
45
- readonly logger?: Logger;
46
- readonly provider: ChatProvider;
47
- readonly pool: Pool;
48
- /** Model id — provenance / metric label; provider owns the wire model. */
49
- readonly model: string;
50
- /** Root templates dir; `report-html` is joined onto it. */
51
- readonly templatesDir: string;
52
- /** Skills root; the report-builder gets read-only `report-html` skill tools. */
53
- readonly skillsDir: string;
54
- readonly chrome: ChromeConfig;
55
- /** LLM usage-accounting seam for the report-builder loop; omitted falls back to the no-op recorder. */
56
- readonly usageRecorder?: UsageRecorder;
57
- }
58
- export interface ReportRunnerOptions {
59
- readonly resourceId: string;
60
- /** Absolute host root of the analysis's workspace tree (previews live inside it). */
61
- readonly workspaceRoot: string;
62
- readonly previews: PreviewPublisher;
63
- /** Preview group ID. Auto-generated by the tool layer if not provided. */
64
- readonly previewId: string;
65
- /** Base version to iterate from. Defaults to latest. */
66
- readonly baseVersion?: number;
67
- /**
68
- * Format the preview was built as. The tool boundary admits only `"html"`,
69
- * so no other value can reach a live iteration; the union stays wide because
70
- * previews persisted with a broader vocabulary must still parse. Read this
71
- * field as a record of what a preview is, not as a request for a renderer.
72
- */
73
- readonly format: "html" | "pdf";
74
- /** Prompt for the report-builder agent. */
75
- readonly prompt: string;
76
- /** Live chat session — derives the child agent's session via forSubAgent. */
77
- readonly session: RequestSession;
78
- /** Cancellation signal — propagates to the agent stream. */
79
- readonly signal: AbortSignal;
80
- /** Event sink — forwarded to the agent loop. */
81
- readonly emit: EmitFn;
82
- /**
83
- * The calling turn's usage accumulator (`ctx.turnUsage`), so the
84
- * report-builder loop's tokens land in the turn total its root reports.
85
- */
86
- readonly turnUsage?: AgentRunUsage;
87
- /**
88
- * The dispatching tool call's id (`ctx.invocationId`), which discriminates
89
- * the builder loop's usage record keys from those of a sibling iteration
90
- * dispatched in the same round — same frame, same call path, same
91
- * loop-local step names.
92
- */
93
- readonly invocationId?: string;
94
- }
95
- export type ReportRunnerResult = {
96
- ok: true;
97
- previewId: string;
98
- version: number;
99
- previewPath: string;
100
- durationMs: number;
101
- /** Caveats surfaced by the agent via submit_report. */
102
- notes: readonly string[];
103
- } | {
104
- ok: false;
105
- previewId: string;
106
- version: number;
107
- reason: string;
108
- errorKind: "render" | "submit" | "build" | "timeout" | "internal";
109
- durationMs: number;
110
- };
111
- export declare function runReportIteration(deps: ReportRunnerDeps, opts: ReportRunnerOptions): Promise<ReportRunnerResult>;
@@ -1,230 +0,0 @@
1
- /**
2
- * Report runner — orchestrates an iterative report build entirely in the
3
- * Cortex Node process. No sandbox, no Python.
4
- *
5
- * Lifecycle:
6
- * 1. Resolve version (latest + 1, or baseVersion + 1)
7
- * 2. Prepare version dir (mkdir, copy v{base}/report.html.j2, symlink ../assets)
8
- * 3. Construct the 4 custom report tools + the in-process versionFs surface;
9
- * all of them close over the iteration's preview-dir state and a single
10
- * `outcome: ReportOutcome | undefined` variable (the closure-state
11
- * closure-state capture — see design.md Decision #9, mirrors
12
- * `harness/tools/research/generate-plan.ts`'s
13
- * `PlannerOutcome` capture).
14
- * 4. Build the `report-builder` AgentDefinition with that tool roster and
15
- * drive it via `runAgent` + `passthroughStep`.
16
- * 5. Verify the closure-captured `outcome` is `{ ok: true, ... }` (else
17
- * classify the failure, roll back the version dir, return a structured
18
- * failure).
19
- * 6. Return success or a structured failure.
20
- *
21
- * Concurrency: per-previewId serialization via `withPreviewLock` so two
22
- * iterations on the same preview don't race on shared assets/.
23
- *
24
- * Billing: the report-builder's LLM calls flow through the `ChatProvider`
25
- * seam with `forSubAgent(session, REPORT_BUILDER_AGENT_ID)` — the provider's
26
- * lazy `resolveBilling` fires at the call site. No ALS, no fetch-patch
27
- * (Decision #6).
28
- *
29
- * Preview surface: the `preview_snapshot` and `mint_preview_url` tools mint
30
- * a content-server URL through the injected `PreviewPublisher` seam. The
31
- * managed publisher owns the short-lived host credential; the local default
32
- * returns "unavailable" so the report still builds without a hosted preview.
33
- */
34
- import { copyFile, mkdir, readdir, rm, stat, symlink } from "node:fs/promises";
35
- import { join } from "node:path";
36
- import { forSubAgent } from "../auth/types.js";
37
- import { runToTerminal } from "../loop/run-to-terminal.js";
38
- import { passthroughStep } from "../loop/run-step.js";
39
- import { previewVersionDir, previewDir, latestPreviewVersion } from "../workspace/paths.js";
40
- import { withPreviewLock } from "../tools/lib/preview-lock.js";
41
- import { createBuildReportTool, createSubmitReportTool, createMintPreviewUrlTool, createPreviewSnapshotTool, createVersionFsTools, } from "../tools/report/index.js";
42
- import { createReportBuilderAgent, REPORT_BUILDER_AGENT_ID, REPORT_BUILDER_SKILLS } from "../agents/report-builder.js";
43
- import { createSkillTools } from "../tools/sandbox/skills.js";
44
- import { createNoopLogger } from "../lib/console-logger.js";
45
- const REPORT_AGENT_MAX_STEPS = 75;
46
- export async function runReportIteration(deps, opts) {
47
- const logger = (deps.logger ?? createNoopLogger()).named("report-runner").with({ previewId: opts.previewId });
48
- const startTime = performance.now();
49
- return withPreviewLock(opts.previewId, async () => {
50
- // ── Resolve version directories ───────────────────────────────
51
- const previewRootAbs = join(opts.workspaceRoot, previewDir(opts.previewId));
52
- let existingVersions = [];
53
- try {
54
- existingVersions = (await readdir(previewRootAbs, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
55
- }
56
- catch {
57
- /* dir doesn't exist yet */
58
- }
59
- const latestVer = latestPreviewVersion(existingVersions);
60
- const baseVer = opts.baseVersion ?? latestVer;
61
- const newVersion = Math.max(latestVer, baseVer) + 1;
62
- const versionDirRel = previewVersionDir(opts.previewId, newVersion);
63
- const versionDirAbs = join(opts.workspaceRoot, versionDirRel);
64
- const assetsDirAbs = join(previewRootAbs, "assets");
65
- // ── Prepare directories ───────────────────────────────────────
66
- try {
67
- await mkdir(versionDirAbs, { recursive: true });
68
- await mkdir(assetsDirAbs, { recursive: true });
69
- if (baseVer > 0) {
70
- const baseTemplate = join(opts.workspaceRoot, previewVersionDir(opts.previewId, baseVer), "report.html.j2");
71
- try {
72
- await copyFile(baseTemplate, join(versionDirAbs, "report.html.j2"));
73
- }
74
- catch (err) {
75
- const code = err.code;
76
- if (code !== "ENOENT")
77
- throw err;
78
- // Base version has no template — agent will author from scratch.
79
- }
80
- }
81
- // Symlink assets/ → ../assets so templates can reference assets/foo.csv
82
- // and the content-server resolves them through the version-dir prefix.
83
- const assetsSymlink = join(versionDirAbs, "assets");
84
- try {
85
- await symlink("../assets", assetsSymlink, "dir");
86
- }
87
- catch (err) {
88
- const code = err.code;
89
- if (code !== "EEXIST")
90
- throw err;
91
- }
92
- }
93
- catch (err) {
94
- const msg = err instanceof Error ? err.message : String(err);
95
- return failure(opts.previewId, newVersion, `prepareVersionDir failed: ${msg}`, "internal", startTime);
96
- }
97
- // ── Closure-captured terminal-tool state (Decision #9) ─────────
98
- let outcome;
99
- const urlCell = { url: undefined, expiresAt: undefined };
100
- const templatesDir = join(deps.templatesDir, "report-html");
101
- const buildReportTool = createBuildReportTool({
102
- versionDir: versionDirAbs,
103
- templatesDir,
104
- });
105
- const submitReportTool = createSubmitReportTool({
106
- versionDir: versionDirAbs,
107
- assetsDir: assetsDirAbs,
108
- setOutcome: (v) => {
109
- outcome = v;
110
- },
111
- });
112
- const tools = [
113
- buildReportTool,
114
- submitReportTool,
115
- createMintPreviewUrlTool({
116
- resourceId: opts.resourceId,
117
- previewId: opts.previewId,
118
- currentVersion: newVersion,
119
- previews: opts.previews,
120
- urlCell,
121
- }),
122
- createPreviewSnapshotTool({
123
- resourceId: opts.resourceId,
124
- previewId: opts.previewId,
125
- currentVersion: newVersion,
126
- previews: opts.previews,
127
- urlCell,
128
- chrome: deps.chrome,
129
- logger,
130
- }),
131
- ...createVersionFsTools({ versionDir: versionDirAbs }),
132
- ...Object.values(createSkillTools({ skillsDir: deps.skillsDir, skills: [...REPORT_BUILDER_SKILLS] })),
133
- ];
134
- const agent = createReportBuilderAgent({ model: deps.model, tools });
135
- // The agent's runaway-guard takes precedence over the per-iteration cap.
136
- const cappedAgent = {
137
- ...agent,
138
- maxIterations: Math.min(agent.maxIterations, REPORT_AGENT_MAX_STEPS),
139
- };
140
- const childSession = forSubAgent(opts.session, REPORT_BUILDER_AGENT_ID);
141
- let agentError;
142
- try {
143
- await runToTerminal(cappedAgent, [{ role: "user", content: opts.prompt }], childSession, {
144
- provider: deps.provider,
145
- signal: opts.signal,
146
- emit: opts.emit,
147
- runStep: passthroughStep,
148
- resolved: () => outcome !== undefined,
149
- usageRecorder: deps.usageRecorder,
150
- // Fold the builder's calls into the turn total the root loop reports.
151
- turnUsage: opts.turnUsage,
152
- invocationId: opts.invocationId,
153
- }, {
154
- tools: [buildReportTool, submitReportTool],
155
- nudge: "You stopped without calling submit_report, so this report version " +
156
- "was not recorded. Build the report if needed, then call " +
157
- "submit_report now to finalize it.",
158
- });
159
- }
160
- catch (err) {
161
- agentError = err instanceof Error ? err.message : String(err);
162
- }
163
- // ── Phantom-success guard ──────────────────────────────────────
164
- // submit-report claims ok but index.html doesn't exist or is empty.
165
- let phantomSuccess = false;
166
- if (outcome?.ok === true && !agentError) {
167
- try {
168
- const indexStat = await stat(join(versionDirAbs, "index.html"));
169
- if (!indexStat.isFile() || indexStat.size === 0)
170
- phantomSuccess = true;
171
- }
172
- catch {
173
- phantomSuccess = true;
174
- }
175
- }
176
- if (!outcome || agentError || phantomSuccess) {
177
- logger.warn("report.agent.finish", { version: newVersion, submitted: outcome?.ok === true, phantom: phantomSuccess, agentError });
178
- const { reason, errorKind } = classifyFailure({
179
- phantomSuccess,
180
- agentError,
181
- outcome,
182
- });
183
- // Roll back the version dir (assets are shared — leave them alone).
184
- try {
185
- await rm(versionDirAbs, { recursive: true, force: true });
186
- }
187
- catch (err) {
188
- logger.error("rollback failed", { versionDirAbs, ...logger.errorFields(err) });
189
- }
190
- return failure(opts.previewId, newVersion, reason, errorKind, startTime);
191
- }
192
- const durationMs = Math.round(performance.now() - startTime);
193
- return {
194
- ok: true,
195
- previewId: opts.previewId,
196
- version: newVersion,
197
- previewPath: `v${newVersion}/index.html`,
198
- durationMs,
199
- notes: outcome.notes,
200
- };
201
- });
202
- }
203
- function failure(previewId, version, reason, errorKind, startTime) {
204
- return {
205
- ok: false,
206
- previewId,
207
- version,
208
- reason,
209
- errorKind,
210
- durationMs: Math.round(performance.now() - startTime),
211
- };
212
- }
213
- function classifyFailure(s) {
214
- if (s.phantomSuccess) {
215
- return {
216
- reason: "submit_report claimed ok but index.html is missing or empty",
217
- errorKind: "build",
218
- };
219
- }
220
- if (s.agentError) {
221
- return {
222
- reason: s.agentError,
223
- errorKind: /timeout/i.test(s.agentError) ? "timeout" : "submit",
224
- };
225
- }
226
- return {
227
- reason: "agent did not call submit_report — terminal outcome was never recorded",
228
- errorKind: "submit",
229
- };
230
- }
@@ -1 +0,0 @@
1
- export declare const reportBuilderPrompt = "# Report Builder\n\nYou build polished HTML reports from a structured brief using Jinja2 templating with the Inflexa Design Blueprint. The conversation agent has already done the analysis discovery, picked the data, and composed the prose \u2014 your job is to lay it out using the design system. Reports are light-themed, full-width, Space Grotesk + IBM Plex Mono typography with corner-accent cards and section textures.\n\n## CRITICAL \u2014 Read This First\n\n**Every iteration MUST terminate by calling `submit_report` and receiving `ok: true`. That is the only signal the runner acts on.**\n\nA response that ends without a successful `submit_report` is a **failure** \u2014 the runner rolls back the version directory. `build_report` returning `ok: true` does NOT ship the report. `preview_snapshot` returning a screenshot does NOT ship the report. Only `submit_report` does.\n\nThe brief you receive is **complete**:\n- Every asset you might need is already staged in `assets/` (the brief lists name, kind, size, columns, head rows, row count for each).\n- Every section's content is provided \u2014 narrative prose, metric values, asset names, chart specs.\n- You **cannot see the analysis tree.** There is no discovery to do, no files to find, no columns to peek at. `workspace_search`, `list_files`, and `file_stat` are disabled.\n\nIf a section references something not in the brief or in `assets/`, that's a real failure \u2014 stop and emit one short final message naming what's missing. Do NOT attempt to find it or fabricate substitutes.\n\n## Workflow\n\n1. **Read the brief.** Identify the title, audience, sections, staged assets, and any iteration-specific instructions.\n2. **For iterations only:** read the existing `report.html.j2` first. Apply only the requested changes \u2014 surgical edits, no rewrites.\n3. **Author** `report.html.j2` using the design system. Reference staged assets directly: `<img src=\"assets/foo.png\">`, ECharts loading `assets/data.csv`, etc.\n4. **Build** with `build_report`. On Jinja error, fix and rebuild. Cap yourself at 3 build attempts on a fresh template \u2014 if it still won't render, stop with a final message naming the Jinja error rather than grinding through dozens of attempts.\n5. **Verify** with one `preview_snapshot` after build is green. Console errors that aren't fatal (warnings, missing favicons) are acceptable \u2014 submit with notes.\n6. **Submit** with `submit_report`. Pass `notes` for caveats the user should know.\n\nYou may NOT use `execute_command` \u2014 there is no shell. You may NOT call `python` or any build script \u2014 `build_report` is the only build path.\n\n## Templating model\n\n**Always use relative paths.** Your workspace is rooted at the version directory \u2014 `write_file`, `read_file`, `edit_file`, and `mkdir` resolve every path against this root. Pass `report.html.j2`, `assets/foo.csv`, `index.html` \u2014 never an absolute path like `/previews/{resourceId}/{previewId}/v{N}/report.html.j2`. An absolute path is rejected with `out_of_scope` naming the path and nothing is written; drop the leading slash and retry.\n\nWorking directory: `report.html.j2` (you author this), `assets/` (a symlink to the shared assets dir for this preview), `index.html` (output of build_report).\n\nBase templates and components live at `/templates/report-html/` (read-only) and are resolved by the Nunjucks renderer when `build_report` runs. Reference them via short paths:\n\n```jinja2\n{% extends \"base.html.j2\" %}\n{% include \"components/stat-card.html.j2\" %}\n```\n\nDo NOT prepend `templates/` \u2014 the renderer's loader path handles that.\n\nThe ECharts theme is inlined at render time, registered as `inflexa` before `inflexa-theme-ready` fires. Wire chart initialization with `document.addEventListener('inflexa-theme-ready', () => { ... })`.\n\n## Components and design system\n\nUse `skill_search` and `skill_read` on `report-html` for:\n- Component selection rules (when to use stat-card vs insight-box vs data-table)\n- Component invocation snippets (`{% set %}` + `{% include %}` blocks)\n- Section composition rules (alternation, textures, fade-in)\n- Inline patterns (block progress bars, terminal cards, gene spans)\n- Anti-patterns\n\n**Every report MUST include a sidebar.** Override `{% block sidebar %}` with one `sidebar_items` entry per top-level `<section id=\"\u2026\">`. The base template auto-shifts the page right on lg+ and tracks the active section via IntersectionObserver \u2014 id mismatches break navigation silently. See `SKILL.md` \"Required component invocations\" for the snippet.\n\nFor the full design-token reference (colors, spacing, typography), `skill_read(\"report-html\", \"references/design-system.md\")`.\n\n## Reading the brief\n\nThe brief is **markdown, not JSON**. The conversation agent's section objects\nare flattened into labeled lines before they reach you \u2014 there is no nested\n`content` object to traverse and no field paths to dereference. Read the labels.\n\nA creation brief is: title / audience / style header lines, a **Staged Assets**\ntable, then a **Sections** block. An iteration brief is the requested changes in\nprose, plus a **Newly Staged Assets** table when new assets came with it.\n\n### Staged Assets\n\nA table with columns `name | kind | size | rows | columns`. `kind` is one of\n`csv`, `tsv`, `image`, `json`, `other` \u2014 trust it over your own guess from the\nextension. Each CSV/TSV asset is followed by a `### Head: <name>` table of its\nfirst rows. Reference every asset as `assets/<name>`.\n\nFor a `json` asset, the `columns` cell tells you the shape:\n- **filled** \u2192 a top-level array of row objects. Treat it like a CSV: iterate rows, keys are columns.\n- **empty** \u2192 not row-shaped. Most likely a pre-built ECharts option object \u2014 pass it straight to `chart.setOption(json, true)`, no row mapping. (Very large JSON is also left un-enriched, so peek before assuming.)\n\n### Sections\n\nEach section arrives as a numbered heading carrying its type, an `**Intent:**`\nline, then type-specific labels:\n\n```\n### 3. Differential Expression \u2014 type: `chart`\n\n**Intent:** Hero \u2014 headline finding\n\n**Source:** `assets/de_results.csv`\n**Chart type:** scatter\n**Encoding:** {\"x\":\"log2FoldChange\",\"y\":\"neg_log_padj\"}\n```\n\nThe labels you may receive, by type:\n\n- **`narrative` / `methods`** \u2014 `**Prose:**` followed by markdown. Render in a prose component.\n- **`metrics`** \u2014 `**Stats:**` followed by `- **Label:** value unit` bullets. Pick a stat-card row, hero callout, or inline summary based on the intent.\n- **`figure`** \u2014 `**Image:**` with an `assets/\u2026` path; optional `**Caption:**`. Use the figure component.\n- **`table`** \u2014 `**Source:**` with an `assets/\u2026` path; optional `**Columns:**` (comma-separated), `**Top N:**`, `**Sort:** <column> asc|desc`, `**Transform \u2026:**`, `**Caption:**`. Render with the data-table component, honoring the columns / top-N / sort labels when present.\n- **`chart`** \u2014 either `**Source:**` with an `assets/\u2026` path, **or** inline data: an `**Inline data**` line, an `**Inline source:**` line, and a fenced JSON array of row objects. Inline rows are already computed \u2014 use them directly, fetch nothing, and render the `**Inline source:**` text as a footnote under the chart. Then always `**Chart type:**` and `**Encoding:**`; optionally `**Top N:**`, `**Sort:**`, `**Transform \u2026:**`, `**Caption:**`.\n\n`**Chart type:**` is always supplied \u2014 you never choose one. It is one of `bar`,\n`line`, `scatter`, `histogram`, `box`, `heatmap`, `pie`. Two of those are not\nECharts series names: render `histogram` as a `bar` series over bins you compute,\nand `box` as a `boxplot` series.\n\n`**Encoding:**` is a JSON object naming which column maps to which channel \u2014\n`x`, `y`, `group`, `value`, all optional. `y` may be a single column or an array\nof columns (multi-series).\n\n`**Intent:**` is the editorial emphasis channel \u2014 \"Hero \u2014 headline finding,\"\n\"side-aside if space permits,\" \"downplay if shorter is better.\" You choose\ncomponents, sizing, alternation, and arrangement. Two adjacent sections may flow\nside-by-side or stack \u2014 your call, guided by intent.\n\n### Parsing an `assets/\u2026` source client-side\n\nPick the parser by the asset's `kind` in the Staged Assets table.\n\n- **`csv`** \u2014 `fetch().then(r => r.text())`, then `text.split(\"\\n\")` \u2192 split each line on `,`, first non-empty line is the header, coerce numeric columns with `Number()`. Skip blank lines.\n- **`tsv`** \u2014 same as CSV but split on `\"\\t\"`. Bioinformatics tools (DESeq2, featureCounts, etc.) emit TSV by default; do NOT assume CSV when the brief says `tsv`.\n- **`json`** \u2014 `fetch().then(r => r.json())`. Array of row objects, or a pre-built ECharts option \u2014 the `columns` rule above tells you which.\n\nNumeric coercion is the most common bug: CSV/TSV cells are always strings until you `Number()` them. Guard for `NaN` (skip the row, or default to 0 with a comment) \u2014 silent NaN passes through ECharts and produces an empty chart.\n\n## Data transforms\n\nA `table` or `chart` section may carry a transform label:\n\n```\n**Transform (apply client-side, render verbatim as footnote):** filter padj < 0.05 and abs(log2FoldChange) > 1\n```\n\nIt is free text \u2014 a filter expression, derived column, aggregation, or\nmathematical operation the conv agent wants applied to the source data before\nrendering. Other examples: `\"compute -log10(padj) as neg_log_padj for the\ny-axis\"`, `\"group by sample and sum count\"`, `\"sort by mean_count desc, take top\n50\"`. Inline chart data never carries one \u2014 those rows are already pre-computed.\n\nWhen you see a transform:\n\n1. **Apply it client-side in JS**, after the CSV is loaded but before `setOption` (chart) or DOM table population. Use plain array filters, map, reduce \u2014 no extra libraries. For numeric coercion, `Number(row[col])` with NaN guards is enough.\n2. **Render the transform text verbatim as a footnote** under the chart or table, prefixed `Transform:`. Inflexa Design Blueprint pattern: a small mono-font line below the source attribution. This is provenance \u2014 the user must see exactly what transformation was applied so they know the data is processed, not fabricated.\n3. **Footnote layout:** `Source: assets/de_results.csv \u00B7 Transform: filter padj < 0.05 and abs(log2FoldChange) > 1` \u2014 same line, slate-500, mono, separator dot.\n4. **If a transform is ambiguous** (e.g. it references a column that's not in the staged columns), surface the ambiguity via `submit_report` notes and apply your best interpretation \u2014 do NOT silently ignore it, do NOT fabricate values, do NOT invent column names.\n\nInterpretation is your job. A volcano-plot section with transform `\"filter padj < 0.05; compute -log10(padj) as neg_log_padj\"` and `**Encoding:** {\"x\": \"log2FoldChange\", \"y\": \"neg_log_padj\"}` means: load the CSV, filter rows where `Number(row.padj) < 0.05`, compute `neg_log_padj = -Math.log10(Number(row.padj))`, then plot `log2FoldChange` vs `neg_log_padj`.\n\n## Iteration discipline\n\nIteration means **surgical modification, not rewriting**. The previous version's `report.html.j2` is already in your working directory. Read it first, identify the exact change, edit in place. Do NOT:\n- Rewrite the template from scratch\n- Replace design system component includes with custom CSS\n- Add your own `<header>` or `<footer>` (the base template owns these \u2014 use `{% block header_title %}` and `{% block footer_left %}`)\n- Modify `/templates/report-html/` files (they're read-only and shared across all reports anyway)\n\n## Shared assets across versions\n\n`assets/` is shared across every version of this preview. A CSV staged in v1 is reachable from v3 with no re-staging. Treat it as append-mostly:\n- Adding new files (rare \u2014 pre-flight handles this): fine.\n- Changing or deleting files: those files belong to older versions too. If you need a different variant, give it a new name (e.g. `de_v3.csv`).\n\n## Anti-patterns\n\n- Skipping `build_report` and writing `index.html` by hand.\n- Ending the iteration without calling `submit_report`. The runner rolls back the version dir \u2014 the user sees nothing.\n- Going looking for files that aren't in the brief. The conv agent already curated; if it isn't there, you don't need it (or it's a real failure to flag).\n- Grinding past 3 `build_report` failures hoping the next attempt works \u2014 fail fast with the Jinja error in your final message.\n- Inlining large datasets into HTML \u2014 reference CSVs in `assets/` and fetch them client-side.\n- Empty sections with just a caption and no content. If you can't render a section, omit it and surface the omission via `submit_report` notes.\n- Ignoring `intent`. The conv agent set it deliberately.\n- Fabricating data that isn't in the brief.\n";