markform 0.1.1 → 0.1.3

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 (36) hide show
  1. package/DOCS.md +546 -0
  2. package/README.md +340 -71
  3. package/SPEC.md +2779 -0
  4. package/dist/ai-sdk.d.mts +2 -2
  5. package/dist/ai-sdk.mjs +5 -3
  6. package/dist/{apply-BQdd-fdx.mjs → apply-00UmzDKL.mjs} +849 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-D--Lel-e.mjs} +1374 -428
  9. package/dist/cli.mjs +6 -3
  10. package/dist/{coreTypes--6etkcwb.d.mts → coreTypes-BXhhz9Iq.d.mts} +1946 -794
  11. package/dist/coreTypes-Dful87E0.mjs +537 -0
  12. package/dist/index.d.mts +116 -19
  13. package/dist/index.mjs +5 -3
  14. package/dist/session-Bqnwi9wp.mjs +110 -0
  15. package/dist/session-DdAtY2Ni.mjs +4 -0
  16. package/dist/shared-D7gf27Tr.mjs +3 -0
  17. package/dist/shared-N_s1M-_K.mjs +176 -0
  18. package/dist/src-Dm8jZ5dl.mjs +7587 -0
  19. package/examples/celebrity-deep-research/celebrity-deep-research.form.md +912 -0
  20. package/examples/earnings-analysis/earnings-analysis.form.md +6 -1
  21. package/examples/earnings-analysis/earnings-analysis.valid.ts +119 -59
  22. package/examples/movie-research/movie-research-basic.form.md +164 -0
  23. package/examples/movie-research/movie-research-deep.form.md +486 -0
  24. package/examples/movie-research/movie-research-minimal.form.md +54 -0
  25. package/examples/simple/simple-mock-filled.form.md +17 -13
  26. package/examples/simple/simple-skipped-filled.form.md +32 -9
  27. package/examples/simple/simple-with-skips.session.yaml +102 -143
  28. package/examples/simple/simple.form.md +13 -13
  29. package/examples/simple/simple.session.yaml +80 -69
  30. package/examples/startup-deep-research/startup-deep-research.form.md +60 -8
  31. package/examples/startup-research/startup-research-mock-filled.form.md +1 -1
  32. package/examples/startup-research/startup-research.form.md +1 -1
  33. package/package.json +10 -14
  34. package/dist/src-Cs4_9lWP.mjs +0 -2151
  35. package/examples/political-research/political-research.form.md +0 -233
  36. package/examples/political-research/political-research.mock.lincoln.form.md +0 -355
@@ -1,172 +1,19 @@
1
- import { C as parseRolesFlag, b as USER_ROLE, d as serializeRawMarkdown, et as PatchSchema, f as AGENT_ROLE, g as DEFAULT_PORT, h as DEFAULT_MAX_TURNS, m as DEFAULT_MAX_PATCHES_PER_TURN, p as DEFAULT_MAX_ISSUES, r as inspect, t as applyPatches, u as serialize, x as formatSuggestedLlms, y as SUGGESTED_LLMS } from "./apply-BQdd-fdx.mjs";
2
- import { _ as parseForm, a as resolveModel, c as createMockAgent, h as serializeSession, i as getProviderNames, o as createLiveAgent, r as getProviderInfo, t as VERSION, u as createHarness } from "./src-Cs4_9lWP.mjs";
1
+ import { N as PatchSchema } from "./coreTypes-Dful87E0.mjs";
2
+ import { A as parseRolesFlag, D as deriveReportPath, E as deriveExportPath, F as hasWebSearchSupport, M as WEB_SEARCH_CONFIG, N as formatSuggestedLlms, O as detectFileType, T as USER_ROLE, _ as DEFAULT_MAX_TURNS, b as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, d as serializeRawMarkdown, f as serializeReportMarkdown, g as DEFAULT_MAX_PATCHES_PER_TURN, h as DEFAULT_MAX_ISSUES_PER_TURN, j as SUGGESTED_LLMS, k as getFormsDir, m as DEFAULT_FORMS_DIR, p as AGENT_ROLE, r as inspect, t as applyPatches, u as serialize, v as DEFAULT_PORT, w as REPORT_EXTENSION, x as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-00UmzDKL.mjs";
3
+ import { a as resolveHarnessConfig, c as getProviderNames, f as createMockAgent, i as runResearch, l as resolveModel, m as createHarness, s as getProviderInfo, t as VERSION, u as createLiveAgent, v as parseForm } from "./src-Dm8jZ5dl.mjs";
4
+ import { n as serializeSession } from "./session-Bqnwi9wp.mjs";
5
+ import { a as getCommandContext, c as logInfo, d as logVerbose, f as logWarn, h as writeFile, i as formatPath, l as logSuccess, n as ensureFormsDir, o as logDryRun, p as readFile$1, r as formatOutput, s as logError, t as OUTPUT_FORMATS, u as logTiming } from "./shared-N_s1M-_K.mjs";
3
6
  import YAML from "yaml";
7
+ import { basename, dirname, join, resolve } from "node:path";
4
8
  import { Command } from "commander";
5
9
  import pc from "picocolors";
6
- import { basename, dirname, join, relative, resolve } from "node:path";
7
- import * as p from "@clack/prompts";
8
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { readFile } from "node:fs/promises";
11
+ import { existsSync, readFileSync } from "node:fs";
9
12
  import { fileURLToPath } from "node:url";
10
- import { exec } from "node:child_process";
13
+ import * as p from "@clack/prompts";
14
+ import { exec, spawn } from "node:child_process";
11
15
  import { createServer } from "node:http";
12
16
 
13
- //#region src/cli/lib/naming.ts
14
- /**
15
- * Naming convention utilities for JSON/YAML output.
16
- *
17
- * Converts between camelCase (TypeScript internal) and snake_case (JSON/YAML output).
18
- */
19
- /**
20
- * Convert a camelCase string to snake_case.
21
- *
22
- * @example
23
- * toSnakeCase("fieldCount") // "field_count"
24
- * toSnakeCase("parentFieldId") // "parent_field_id"
25
- * toSnakeCase("already_snake") // "already_snake"
26
- */
27
- function toSnakeCase(str) {
28
- return str.replace(/([A-Z])/g, "_$1").toLowerCase();
29
- }
30
- /**
31
- * Recursively convert all object keys from camelCase to snake_case.
32
- *
33
- * Handles nested objects and arrays. Primitives are returned unchanged.
34
- */
35
- function convertKeysToSnakeCase(obj) {
36
- if (obj === null || obj === void 0) return obj;
37
- if (Array.isArray(obj)) return obj.map(convertKeysToSnakeCase);
38
- if (typeof obj === "object") {
39
- const result = {};
40
- for (const [key, value] of Object.entries(obj)) {
41
- const snakeKey = toSnakeCase(key);
42
- result[snakeKey] = convertKeysToSnakeCase(value);
43
- }
44
- return result;
45
- }
46
- return obj;
47
- }
48
-
49
- //#endregion
50
- //#region src/cli/lib/shared.ts
51
- /**
52
- * Valid format options for Commander choice validation.
53
- */
54
- const OUTPUT_FORMATS = [
55
- "console",
56
- "plaintext",
57
- "yaml",
58
- "json",
59
- "markform",
60
- "markdown"
61
- ];
62
- /**
63
- * Extract command context from Commander options.
64
- */
65
- function getCommandContext(command) {
66
- const opts = command.optsWithGlobals();
67
- return {
68
- dryRun: opts.dryRun ?? false,
69
- verbose: opts.verbose ?? false,
70
- quiet: opts.quiet ?? false,
71
- format: opts.format ?? "console"
72
- };
73
- }
74
- /**
75
- * Check if output should use colors.
76
- * Returns true for console format when stdout is a TTY.
77
- */
78
- function shouldUseColors(ctx) {
79
- if (ctx.format === "plaintext" || ctx.format === "yaml" || ctx.format === "json") return false;
80
- return process.stdout.isTTY && !process.env.NO_COLOR;
81
- }
82
- /**
83
- * Format structured data according to output format.
84
- *
85
- * JSON and YAML outputs are converted to snake_case keys for consistency.
86
- */
87
- function formatOutput(ctx, data, consoleFormatter) {
88
- switch (ctx.format) {
89
- case "json": return JSON.stringify(convertKeysToSnakeCase(data), null, 2);
90
- case "yaml": return YAML.stringify(convertKeysToSnakeCase(data));
91
- case "plaintext":
92
- case "console":
93
- default:
94
- if (consoleFormatter) return consoleFormatter(data, shouldUseColors(ctx));
95
- return YAML.stringify(convertKeysToSnakeCase(data));
96
- }
97
- }
98
- /**
99
- * Log a dry-run message.
100
- */
101
- function logDryRun(message, details) {
102
- console.log(pc.yellow(`[DRY RUN] ${message}`));
103
- if (details) console.log(pc.dim(JSON.stringify(details, null, 2)));
104
- }
105
- /**
106
- * Log a verbose message (only shown if --verbose is set).
107
- */
108
- function logVerbose(ctx, message) {
109
- if (ctx.verbose) console.log(pc.dim(message));
110
- }
111
- /**
112
- * Log an info message (hidden if --quiet is set).
113
- */
114
- function logInfo(ctx, message) {
115
- if (!ctx.quiet) console.log(message);
116
- }
117
- /**
118
- * Log an error message (always shown).
119
- */
120
- function logError(message) {
121
- console.error(pc.red(`Error: ${message}`));
122
- }
123
- /**
124
- * Log a success message (hidden if --quiet is set).
125
- */
126
- function logSuccess(ctx, message) {
127
- if (!ctx.quiet) console.log(pc.green(message));
128
- }
129
- /**
130
- * Log a timing message (hidden if --quiet is set).
131
- */
132
- function logTiming(ctx, label, durationMs) {
133
- if (!ctx.quiet) {
134
- const seconds = (durationMs / 1e3).toFixed(1);
135
- console.log(pc.cyan(`⏰ ${label}: ${seconds}s`));
136
- }
137
- }
138
- /**
139
- * Log a warning message (hidden if --quiet is set).
140
- */
141
- function logWarn(ctx, message) {
142
- if (!ctx.quiet) console.log(pc.yellow(`⚠️ ${message}`));
143
- }
144
- /**
145
- * Format a file path for display: relative to cwd, colored green.
146
- * If the path is within the cwd, shows as relative (e.g., "./simple-filled1.form.md")
147
- * If outside cwd, shows the absolute path.
148
- */
149
- function formatPath(absolutePath, cwd = process.cwd()) {
150
- const relativePath = relative(cwd, absolutePath);
151
- const displayPath = relativePath.startsWith("..") ? absolutePath : `./${relativePath}`;
152
- return pc.green(displayPath);
153
- }
154
- /**
155
- * Read a file and return its contents.
156
- */
157
- async function readFile(filePath) {
158
- const { readFile: fsReadFile } = await import("node:fs/promises");
159
- return fsReadFile(filePath, "utf-8");
160
- }
161
- /**
162
- * Write contents to a file.
163
- */
164
- async function writeFile(filePath, contents) {
165
- const { writeFile: fsWriteFile } = await import("node:fs/promises");
166
- await fsWriteFile(filePath, contents, "utf-8");
167
- }
168
-
169
- //#endregion
170
17
  //#region src/cli/commands/apply.ts
171
18
  /**
172
19
  * Format state badge for console output.
@@ -200,9 +47,9 @@ function formatConsoleReport$2(report, useColors) {
200
47
  const counts = report.progress.counts;
201
48
  lines.push(bold("Progress:"));
202
49
  lines.push(` Total fields: ${counts.totalFields}`);
203
- lines.push(` Complete: ${counts.completeFields}`);
204
- lines.push(` Incomplete: ${counts.incompleteFields}`);
205
- lines.push(` Empty (required): ${counts.emptyRequiredFields}`);
50
+ lines.push(` Valid: ${counts.validFields}, Invalid: ${counts.invalidFields}`);
51
+ lines.push(` Filled: ${counts.filledFields}, Empty: ${counts.emptyFields}`);
52
+ lines.push(` Empty required: ${counts.emptyRequiredFields}`);
206
53
  lines.push("");
207
54
  if (report.issues.length > 0) {
208
55
  lines.push(bold(`Issues (${report.issues.length}):`));
@@ -225,7 +72,7 @@ function registerApplyCommand(program) {
225
72
  process.exit(1);
226
73
  }
227
74
  logVerbose(ctx, `Reading file: ${file}`);
228
- const content = await readFile(file);
75
+ const content = await readFile$1(file);
229
76
  logVerbose(ctx, "Parsing form...");
230
77
  const form = parseForm(content);
231
78
  logVerbose(ctx, "Parsing patches...");
@@ -297,78 +144,92 @@ function registerApplyCommand(program) {
297
144
  }
298
145
 
299
146
  //#endregion
300
- //#region src/cli/commands/dump.ts
147
+ //#region src/cli/commands/docs.ts
301
148
  /**
302
- * Format a field value for console display.
149
+ * Get the path to the DOCS.md file.
150
+ * Works both during development and when installed as a package.
303
151
  */
304
- function formatFieldValue$1(value, useColors) {
305
- const dim = useColors ? pc.dim : (s) => s;
306
- const green = useColors ? pc.green : (s) => s;
307
- if (!value) return dim("(empty)");
308
- switch (value.kind) {
309
- case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
310
- case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
311
- case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
312
- case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
313
- case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
314
- case "checkboxes": {
315
- const entries = Object.entries(value.values);
316
- if (entries.length === 0) return dim("(no entries)");
317
- return entries.map(([k, v]) => `${k}:${v}`).join(", ");
318
- }
319
- default: return dim("(unknown)");
320
- }
152
+ function getDocsPath() {
153
+ const thisDir = dirname(fileURLToPath(import.meta.url));
154
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "DOCS.md");
155
+ return join(dirname(dirname(dirname(thisDir))), "DOCS.md");
321
156
  }
322
157
  /**
323
- * Format values for console output.
158
+ * Load the docs content.
324
159
  */
325
- function formatConsoleValues(values, useColors) {
326
- const lines = [];
327
- const bold = useColors ? pc.bold : (s) => s;
328
- for (const [fieldId, value] of Object.entries(values)) {
329
- const valueStr = formatFieldValue$1(value, useColors);
330
- lines.push(`${bold(fieldId)}: ${valueStr}`);
331
- }
332
- if (lines.length === 0) {
333
- const dim = useColors ? pc.dim : (s) => s;
334
- lines.push(dim("(no values)"));
160
+ function loadDocs() {
161
+ const docsPath = getDocsPath();
162
+ try {
163
+ return readFileSync(docsPath, "utf-8");
164
+ } catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ throw new Error(`Failed to load DOCS from ${docsPath}: ${message}`);
335
167
  }
336
- return lines.join("\n");
337
168
  }
338
169
  /**
339
- * Convert FieldValue to a plain value for JSON/YAML serialization.
170
+ * Apply basic terminal formatting to markdown content.
171
+ * Colorizes headers, code blocks, and other elements for better readability.
340
172
  */
341
- function toPlainValue(value) {
342
- switch (value.kind) {
343
- case "string": return value.value ?? null;
344
- case "number": return value.value ?? null;
345
- case "string_list": return value.items;
346
- case "single_select": return value.selected ?? null;
347
- case "multi_select": return value.selected;
348
- case "checkboxes": return value.values;
349
- default: return null;
173
+ function formatMarkdown$3(content, useColors) {
174
+ if (!useColors) return content;
175
+ const lines = content.split("\n");
176
+ const formatted = [];
177
+ let inCodeBlock = false;
178
+ for (const line of lines) {
179
+ if (line.startsWith("```")) {
180
+ inCodeBlock = !inCodeBlock;
181
+ formatted.push(pc.dim(line));
182
+ continue;
183
+ }
184
+ if (inCodeBlock) {
185
+ formatted.push(pc.dim(line));
186
+ continue;
187
+ }
188
+ if (line.startsWith("# ")) {
189
+ formatted.push(pc.bold(pc.cyan(line)));
190
+ continue;
191
+ }
192
+ if (line.startsWith("## ")) {
193
+ formatted.push(pc.bold(pc.blue(line)));
194
+ continue;
195
+ }
196
+ if (line.startsWith("### ")) {
197
+ formatted.push(pc.bold(line));
198
+ continue;
199
+ }
200
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
201
+ return pc.yellow(code);
202
+ });
203
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
204
+ return pc.bold(text);
205
+ });
206
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
207
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
208
+ });
209
+ formatted.push(formattedLine);
350
210
  }
211
+ return formatted.join("\n");
351
212
  }
352
213
  /**
353
- * Register the dump command.
214
+ * Check if stdout is an interactive terminal.
354
215
  */
355
- function registerDumpCommand(program) {
356
- program.command("dump <file>").description("Extract and display form values only (lightweight inspect)").action(async (file, _options, cmd) => {
216
+ function isInteractive$3() {
217
+ return process.stdout.isTTY === true;
218
+ }
219
+ /**
220
+ * Display content.
221
+ */
222
+ function displayContent$2(content) {
223
+ console.log(content);
224
+ }
225
+ /**
226
+ * Register the docs command.
227
+ */
228
+ function registerDocsCommand(program) {
229
+ program.command("docs").description("Display concise Markform syntax reference (agent-friendly)").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
357
230
  const ctx = getCommandContext(cmd);
358
231
  try {
359
- logVerbose(ctx, `Reading file: ${file}`);
360
- const content = await readFile(file);
361
- logVerbose(ctx, "Parsing form...");
362
- const form = parseForm(content);
363
- if (ctx.format === "json" || ctx.format === "yaml") {
364
- const plainValues = {};
365
- for (const [fieldId, value] of Object.entries(form.valuesByFieldId)) plainValues[fieldId] = toPlainValue(value);
366
- const output = formatOutput(ctx, plainValues, () => "");
367
- console.log(output);
368
- } else {
369
- const output = formatOutput(ctx, form.valuesByFieldId, (data, useColors) => formatConsoleValues(data, useColors));
370
- console.log(output);
371
- }
232
+ displayContent$2(formatMarkdown$3(loadDocs(), !options.raw && isInteractive$3() && !ctx.quiet));
372
233
  } catch (error) {
373
234
  logError(error instanceof Error ? error.message : String(error));
374
235
  process.exit(1);
@@ -387,72 +248,209 @@ function registerDumpCommand(program) {
387
248
  * - YAML values (.yml) - extracted field values
388
249
  */
389
250
  /**
390
- * Convert field values to plain values for YAML export.
251
+ * Convert field responses to structured format for export (markform-218).
391
252
  *
392
- * Extracts the underlying values from the typed FieldValue wrappers
393
- * for a cleaner YAML representation.
253
+ * Includes state for all fields:
254
+ * - { state: 'unanswered' } for unfilled fields
255
+ * - { state: 'skipped' } for skipped fields
256
+ * - { state: 'aborted' } for aborted fields
257
+ * - { state: 'answered', value: ... } for answered fields
394
258
  */
395
- function toPlainValues(form) {
259
+ function toStructuredValues(form) {
396
260
  const result = {};
397
- for (const [fieldId, value] of Object.entries(form.valuesByFieldId)) switch (value.kind) {
398
- case "string":
399
- result[fieldId] = value.value ?? null;
400
- break;
401
- case "number":
402
- result[fieldId] = value.value ?? null;
403
- break;
404
- case "string_list":
405
- result[fieldId] = value.items;
406
- break;
407
- case "single_select":
408
- result[fieldId] = value.selected ?? null;
409
- break;
410
- case "multi_select":
411
- result[fieldId] = value.selected;
412
- break;
413
- case "checkboxes":
414
- result[fieldId] = value.values;
415
- break;
261
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
262
+ if (!response || response.state === "unanswered") {
263
+ result[fieldId] = { state: "unanswered" };
264
+ continue;
265
+ }
266
+ if (response.state === "skipped") {
267
+ result[fieldId] = {
268
+ state: "skipped",
269
+ ...response.reason && { reason: response.reason }
270
+ };
271
+ continue;
272
+ }
273
+ if (response.state === "aborted") {
274
+ result[fieldId] = {
275
+ state: "aborted",
276
+ ...response.reason && { reason: response.reason }
277
+ };
278
+ continue;
279
+ }
280
+ if (!response.value) {
281
+ result[fieldId] = {
282
+ state: "answered",
283
+ value: null
284
+ };
285
+ continue;
286
+ }
287
+ const value = response.value;
288
+ let exportValue;
289
+ switch (value.kind) {
290
+ case "string":
291
+ exportValue = value.value ?? null;
292
+ break;
293
+ case "number":
294
+ exportValue = value.value ?? null;
295
+ break;
296
+ case "string_list":
297
+ exportValue = value.items;
298
+ break;
299
+ case "single_select":
300
+ exportValue = value.selected ?? null;
301
+ break;
302
+ case "multi_select":
303
+ exportValue = value.selected;
304
+ break;
305
+ case "checkboxes":
306
+ exportValue = value.values;
307
+ break;
308
+ case "url":
309
+ exportValue = value.value ?? null;
310
+ break;
311
+ case "url_list":
312
+ exportValue = value.items;
313
+ break;
314
+ }
315
+ result[fieldId] = {
316
+ state: "answered",
317
+ value: exportValue
318
+ };
416
319
  }
417
320
  return result;
418
321
  }
419
322
  /**
323
+ * Convert notes to export format (markform-219).
324
+ */
325
+ function toNotesArray(form) {
326
+ return form.notes.map((note) => ({
327
+ id: note.id,
328
+ ref: note.ref,
329
+ role: note.role,
330
+ text: note.text
331
+ }));
332
+ }
333
+ /**
420
334
  * Derive export paths from a base form path.
335
+ * Uses centralized extension constants from settings.ts.
336
+ *
337
+ * Standard exports: report, values (yaml), form.
338
+ * Raw markdown is available via CLI but not in standard exports.
421
339
  *
422
340
  * @param basePath - Path to the .form.md file
423
341
  * @returns Object with paths for all export formats
424
342
  */
425
343
  function deriveExportPaths(basePath) {
426
344
  return {
427
- formPath: basePath,
428
- rawPath: basePath.replace(/\.form\.md$/, ".raw.md"),
429
- yamlPath: basePath.replace(/\.form\.md$/, ".yml")
345
+ reportPath: deriveReportPath(basePath),
346
+ yamlPath: deriveExportPath(basePath, "yaml"),
347
+ formPath: deriveExportPath(basePath, "form")
430
348
  };
431
349
  }
432
350
  /**
433
351
  * Export form to multiple formats.
434
352
  *
435
- * Writes:
353
+ * Standard exports:
354
+ * - Report format (.report.md) - filtered markdown (excludes instructions, report=false)
355
+ * - YAML values (.yml) - structured format with state and notes
436
356
  * - Markform format (.form.md) - canonical form with directives
437
- * - Raw markdown (.raw.md) - plain readable markdown (no directives)
438
- * - YAML values (.yml) - extracted field values only
357
+ *
358
+ * Note: Raw markdown (.raw.md) is available via CLI `markform export --raw`
359
+ * but is not included in standard multi-format export.
439
360
  *
440
361
  * @param form - The parsed form to export
441
362
  * @param basePath - Base path for the .form.md file (other paths are derived)
442
363
  * @returns Paths to all exported files
443
364
  */
444
- function exportMultiFormat(form, basePath) {
365
+ async function exportMultiFormat(form, basePath) {
445
366
  const paths = deriveExportPaths(basePath);
367
+ const reportContent = serializeReportMarkdown(form);
368
+ await writeFile(paths.reportPath, reportContent);
369
+ const values = toStructuredValues(form);
370
+ const notes = toNotesArray(form);
371
+ const exportData = {
372
+ values,
373
+ ...notes.length > 0 && { notes }
374
+ };
375
+ const yamlContent = YAML.stringify(exportData);
376
+ await writeFile(paths.yamlPath, yamlContent);
446
377
  const formContent = serialize(form);
447
- writeFileSync(paths.formPath, formContent, "utf-8");
448
- const rawContent = serializeRawMarkdown(form);
449
- writeFileSync(paths.rawPath, rawContent, "utf-8");
450
- const values = toPlainValues(form);
451
- const yamlContent = YAML.stringify(values);
452
- writeFileSync(paths.yamlPath, yamlContent, "utf-8");
378
+ await writeFile(paths.formPath, formContent);
453
379
  return paths;
454
380
  }
455
381
 
382
+ //#endregion
383
+ //#region src/cli/commands/dump.ts
384
+ /**
385
+ * Format a field response for console display, including state information.
386
+ */
387
+ function formatFieldResponse(response, useColors) {
388
+ const dim = useColors ? pc.dim : (s) => s;
389
+ const green = useColors ? pc.green : (s) => s;
390
+ const yellow = useColors ? pc.yellow : (s) => s;
391
+ if (response.state === "unanswered") return dim("(unanswered)");
392
+ if (response.state === "skipped") return yellow(`[skipped]${response.reason ? ` ${response.reason}` : ""}`);
393
+ if (response.state === "aborted") return yellow(`[aborted]${response.reason ? ` ${response.reason}` : ""}`);
394
+ const value = response.value;
395
+ if (!value) return dim("(empty)");
396
+ switch (value.kind) {
397
+ case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
398
+ case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
399
+ case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
400
+ case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
401
+ case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
402
+ case "checkboxes": {
403
+ const entries = Object.entries(value.values);
404
+ if (entries.length === 0) return dim("(no entries)");
405
+ return entries.map(([k, v]) => `${k}:${v}`).join(", ");
406
+ }
407
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
408
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
409
+ default: return dim("(unknown)");
410
+ }
411
+ }
412
+ /**
413
+ * Format form responses for console output, showing all fields with their states.
414
+ */
415
+ function formatConsoleResponses(form, useColors) {
416
+ const lines = [];
417
+ const bold = useColors ? pc.bold : (s) => s;
418
+ const dim = useColors ? pc.dim : (s) => s;
419
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
420
+ const valueStr = formatFieldResponse(response, useColors);
421
+ lines.push(`${bold(fieldId)}: ${valueStr}`);
422
+ }
423
+ if (lines.length === 0) lines.push(dim("(no fields)"));
424
+ return lines.join("\n");
425
+ }
426
+ /**
427
+ * Register the dump command.
428
+ */
429
+ function registerDumpCommand(program) {
430
+ program.command("dump <file>").description("Extract and display form values with state (lightweight inspect)").action(async (file, _options, cmd) => {
431
+ const ctx = getCommandContext(cmd);
432
+ try {
433
+ logVerbose(ctx, `Reading file: ${file}`);
434
+ const content = await readFile$1(file);
435
+ logVerbose(ctx, "Parsing form...");
436
+ const form = parseForm(content);
437
+ if (ctx.format === "json" || ctx.format === "yaml") {
438
+ const output = formatOutput(ctx, {
439
+ values: toStructuredValues(form),
440
+ ...form.notes.length > 0 && { notes: toNotesArray(form) }
441
+ }, () => "");
442
+ console.log(output);
443
+ } else {
444
+ const output = formatOutput(ctx, form, (data, useColors) => formatConsoleResponses(data, useColors));
445
+ console.log(output);
446
+ }
447
+ } catch (error) {
448
+ logError(error instanceof Error ? error.message : String(error));
449
+ process.exit(1);
450
+ }
451
+ });
452
+ }
453
+
456
454
  //#endregion
457
455
  //#region src/cli/lib/patchFormat.ts
458
456
  /** Maximum characters for a patch value display before truncation */
@@ -477,8 +475,13 @@ function formatPatchValue(patch) {
477
475
  case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
478
476
  case "clear_field": return "(cleared)";
479
477
  case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
478
+ case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
480
479
  case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
481
480
  case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
481
+ case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
482
+ case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
483
+ case "add_note": return truncate(`note: ${patch.text}`);
484
+ case "remove_note": return `(remove note ${patch.noteId})`;
482
485
  }
483
486
  }
484
487
  /**
@@ -494,46 +497,103 @@ function formatPatchType(patch) {
494
497
  case "set_checkboxes": return "checkboxes";
495
498
  case "clear_field": return "clear";
496
499
  case "skip_field": return "skip";
500
+ case "abort_field": return "abort";
497
501
  case "set_url": return "url";
498
502
  case "set_url_list": return "url_list";
503
+ case "set_date": return "date";
504
+ case "set_year": return "year";
505
+ case "add_note": return "note";
506
+ case "remove_note": return "remove_note";
499
507
  }
500
508
  }
501
509
 
510
+ //#endregion
511
+ //#region src/cli/lib/formatting.ts
512
+ /**
513
+ * Get a short status word from an issue reason.
514
+ */
515
+ function issueReasonToStatus(reason) {
516
+ switch (reason) {
517
+ case "required_missing": return "missing";
518
+ case "validation_error": return "invalid";
519
+ case "checkbox_incomplete": return "incomplete";
520
+ case "min_items_not_met": return "too-few";
521
+ case "optional_empty": return "empty";
522
+ default: return "issue";
523
+ }
524
+ }
525
+ /**
526
+ * Format a single issue as "fieldId (status)".
527
+ */
528
+ function formatIssueBrief(issue) {
529
+ const status = issueReasonToStatus(issue.reason);
530
+ return `${issue.ref} (${status})`;
531
+ }
532
+ /**
533
+ * Format issues for turn logging - shows count and brief field list.
534
+ * Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
535
+ */
536
+ function formatTurnIssues(issues, maxShow = 5) {
537
+ const count = issues.length;
538
+ if (count === 0) return "0 issues";
539
+ return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
540
+ }
541
+
502
542
  //#endregion
503
543
  //#region src/cli/examples/exampleRegistry.ts
504
544
  /**
505
545
  * Example form registry.
506
546
  * Provides form content from the examples directory for the examples CLI command.
547
+ *
548
+ * Metadata (title, description) is loaded dynamically from the form's YAML frontmatter
549
+ * rather than being duplicated here, following the single source of truth principle.
550
+ */
551
+ /**
552
+ * Example definitions without content or metadata.
553
+ * Title and description are loaded dynamically from frontmatter.
507
554
  */
508
- /** Example definitions without content - content is loaded lazily. */
509
555
  const EXAMPLE_DEFINITIONS = [
510
556
  {
511
557
  id: "simple",
512
- title: "Simple Test Form",
513
- description: "User and agent roles for testing full workflow. User fills required fields, agent fills optional.",
514
558
  filename: "simple.form.md",
515
- path: "simple/simple.form.md"
559
+ path: "simple/simple.form.md",
560
+ type: "fill"
561
+ },
562
+ {
563
+ id: "movie-research-minimal",
564
+ filename: "movie-research-minimal.form.md",
565
+ path: "movie-research/movie-research-minimal.form.md",
566
+ type: "research"
567
+ },
568
+ {
569
+ id: "movie-research-basic",
570
+ filename: "movie-research-basic.form.md",
571
+ path: "movie-research/movie-research-basic.form.md",
572
+ type: "research"
516
573
  },
517
574
  {
518
- id: "political-research",
519
- title: "Political Research",
520
- description: "Biographical research form with one user field (name) and agent-filled details. Uses web search.",
521
- filename: "political-research.form.md",
522
- path: "political-research/political-research.form.md"
575
+ id: "movie-research-deep",
576
+ filename: "movie-research-deep.form.md",
577
+ path: "movie-research/movie-research-deep.form.md",
578
+ type: "research"
523
579
  },
524
580
  {
525
581
  id: "earnings-analysis",
526
- title: "Company Quarterly Analysis",
527
- description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
528
582
  filename: "earnings-analysis.form.md",
529
- path: "earnings-analysis/earnings-analysis.form.md"
583
+ path: "earnings-analysis/earnings-analysis.form.md",
584
+ type: "research"
530
585
  },
531
586
  {
532
587
  id: "startup-deep-research",
533
- title: "Startup Deep Research",
534
- description: "Comprehensive startup intelligence gathering with company info, founders, funding, competitors, social media, and community presence.",
535
588
  filename: "startup-deep-research.form.md",
536
- path: "startup-deep-research/startup-deep-research.form.md"
589
+ path: "startup-deep-research/startup-deep-research.form.md",
590
+ type: "research"
591
+ },
592
+ {
593
+ id: "celebrity-deep-research",
594
+ filename: "celebrity-deep-research.form.md",
595
+ path: "celebrity-deep-research/celebrity-deep-research.form.md",
596
+ type: "research"
537
597
  }
538
598
  ];
539
599
  /**
@@ -547,7 +607,7 @@ function getExamplesDir() {
547
607
  }
548
608
  /**
549
609
  * Load the content of an example form.
550
- * @param exampleId - The example ID (e.g., 'simple', 'political-research')
610
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
551
611
  * @returns The form content as a string
552
612
  * @throws Error if the example is not found
553
613
  */
@@ -569,7 +629,7 @@ function getExampleById(id) {
569
629
  }
570
630
  /**
571
631
  * Get the absolute path to an example's source file.
572
- * @param exampleId - The example ID (e.g., 'simple', 'political-research')
632
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
573
633
  * @returns The absolute path to the example form file
574
634
  * @throws Error if the example is not found
575
635
  */
@@ -578,6 +638,48 @@ function getExamplePath(exampleId) {
578
638
  if (!example) throw new Error(`Unknown example: ${exampleId}`);
579
639
  return join(getExamplesDir(), example.path);
580
640
  }
641
+ /**
642
+ * Extract YAML frontmatter from a markdown file content.
643
+ * @param content - The markdown file content
644
+ * @returns The parsed frontmatter object or null if no frontmatter found
645
+ */
646
+ function extractFrontmatter(content) {
647
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
648
+ if (!frontmatterMatch || !frontmatterMatch[1]) return null;
649
+ try {
650
+ return YAML.parse(frontmatterMatch[1]);
651
+ } catch {
652
+ return null;
653
+ }
654
+ }
655
+ /**
656
+ * Load metadata (title, description) from an example's YAML frontmatter.
657
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
658
+ * @returns Object with title and description from frontmatter
659
+ */
660
+ function loadExampleMetadata(exampleId) {
661
+ const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
662
+ if (!frontmatter || !frontmatter.markform) return {};
663
+ const markform = frontmatter.markform;
664
+ return {
665
+ title: typeof markform.title === "string" ? markform.title : void 0,
666
+ description: typeof markform.description === "string" ? markform.description : void 0
667
+ };
668
+ }
669
+ /**
670
+ * Get all example definitions with metadata loaded from frontmatter.
671
+ * @returns Array of ExampleDefinition with title and description populated
672
+ */
673
+ function getAllExamplesWithMetadata() {
674
+ return EXAMPLE_DEFINITIONS.map((example) => {
675
+ const metadata = loadExampleMetadata(example.id);
676
+ return {
677
+ ...example,
678
+ title: metadata.title,
679
+ description: metadata.description
680
+ };
681
+ });
682
+ }
581
683
 
582
684
  //#endregion
583
685
  //#region src/cli/lib/versioning.ts
@@ -654,6 +756,29 @@ function generateVersionedPath(filePath) {
654
756
  }
655
757
  return candidate;
656
758
  }
759
+ /**
760
+ * Generate a versioned filename within the forms directory.
761
+ *
762
+ * Derives the base name from the input path and creates a versioned
763
+ * output path within the specified forms directory.
764
+ *
765
+ * @param inputPath - Original input file path (used to derive basename)
766
+ * @param formsDir - Absolute path to the forms directory
767
+ * @returns Absolute path to a non-existent versioned file in formsDir
768
+ */
769
+ function generateVersionedPathInFormsDir(inputPath, formsDir) {
770
+ const inputFilename = basename(inputPath);
771
+ const parsed = parseVersionedPath(inputFilename);
772
+ const baseName = parsed?.base ?? inputFilename.replace(/\.form\.md$/i, "");
773
+ const extension = parsed?.extension ?? ".form.md";
774
+ let version = 1;
775
+ let candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
776
+ while (existsSync(candidate)) {
777
+ version++;
778
+ candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
779
+ }
780
+ return candidate;
781
+ }
657
782
 
658
783
  //#endregion
659
784
  //#region src/cli/lib/interactivePrompts.ts
@@ -683,7 +808,7 @@ function getFieldById(form, fieldId) {
683
808
  */
684
809
  function formatFieldLabel(ctx) {
685
810
  const required = ctx.field.required ? pc.red("*") : "";
686
- const progress = pc.dim(`(${ctx.index} of ${ctx.total})`);
811
+ const progress = `(${ctx.index} of ${ctx.total})`;
687
812
  return `${ctx.field.label}${required} ${progress}`;
688
813
  }
689
814
  /**
@@ -693,6 +818,7 @@ function createSkipPatch(field) {
693
818
  return {
694
819
  op: "skip_field",
695
820
  fieldId: field.id,
821
+ role: "user",
696
822
  reason: "User skipped in console"
697
823
  };
698
824
  }
@@ -1057,9 +1183,10 @@ async function runInteractiveFill(form, issues) {
1057
1183
  const field = getFieldById(form, issue.ref);
1058
1184
  if (!field) continue;
1059
1185
  index++;
1186
+ const response = form.responsesByFieldId[field.id];
1060
1187
  const patch = await promptForField({
1061
1188
  field,
1062
- currentValue: form.valuesByFieldId[field.id],
1189
+ currentValue: response?.state === "answered" ? response.value : void 0,
1063
1190
  description: getFieldDescription(form, field.id),
1064
1191
  index,
1065
1192
  total: uniqueFieldIssues.length
@@ -1110,6 +1237,196 @@ function showInteractiveOutro(patchCount, cancelled) {
1110
1237
  p.outro(`✓ ${patchCount} field(s) updated.`);
1111
1238
  }
1112
1239
 
1240
+ //#endregion
1241
+ //#region src/cli/lib/fileViewer.ts
1242
+ /**
1243
+ * File viewer utility for displaying files with colorization and pagination.
1244
+ *
1245
+ * Provides a modern console experience:
1246
+ * - Syntax highlighting for markdown and YAML
1247
+ * - Pagination using system pager (less) when available
1248
+ * - Fallback to console output when not interactive
1249
+ */
1250
+ /**
1251
+ * Check if stdout is an interactive terminal.
1252
+ */
1253
+ function isInteractive$2() {
1254
+ return process.stdout.isTTY === true;
1255
+ }
1256
+ /**
1257
+ * Apply terminal formatting to markdown content.
1258
+ * Colorizes headers, code blocks, and other elements.
1259
+ */
1260
+ function formatMarkdown$2(content) {
1261
+ const lines = content.split("\n");
1262
+ const formatted = [];
1263
+ let inCodeBlock = false;
1264
+ for (const line of lines) {
1265
+ if (line.startsWith("```")) {
1266
+ inCodeBlock = !inCodeBlock;
1267
+ formatted.push(pc.dim(line));
1268
+ continue;
1269
+ }
1270
+ if (inCodeBlock) {
1271
+ formatted.push(pc.cyan(line));
1272
+ continue;
1273
+ }
1274
+ if (line.startsWith("# ")) {
1275
+ formatted.push(pc.bold(pc.magenta(line)));
1276
+ continue;
1277
+ }
1278
+ if (line.startsWith("## ")) {
1279
+ formatted.push(pc.bold(pc.blue(line)));
1280
+ continue;
1281
+ }
1282
+ if (line.startsWith("### ")) {
1283
+ formatted.push(pc.bold(pc.cyan(line)));
1284
+ continue;
1285
+ }
1286
+ if (line.startsWith("#### ")) {
1287
+ formatted.push(pc.bold(line));
1288
+ continue;
1289
+ }
1290
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
1291
+ return pc.yellow(code);
1292
+ });
1293
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
1294
+ return pc.bold(text);
1295
+ });
1296
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
1297
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
1298
+ });
1299
+ formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
1300
+ return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
1301
+ });
1302
+ formatted.push(formattedLine);
1303
+ }
1304
+ return formatted.join("\n");
1305
+ }
1306
+ /**
1307
+ * Apply terminal formatting to YAML content.
1308
+ */
1309
+ function formatYaml(content) {
1310
+ const lines = content.split("\n");
1311
+ const formatted = [];
1312
+ for (const line of lines) {
1313
+ if (line.trim().startsWith("#")) {
1314
+ formatted.push(pc.dim(line));
1315
+ continue;
1316
+ }
1317
+ const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
1318
+ if (match) {
1319
+ const [, indent, key, colon, value] = match;
1320
+ formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
1321
+ continue;
1322
+ }
1323
+ if (line.trim().startsWith("-")) {
1324
+ formatted.push(pc.green(line));
1325
+ continue;
1326
+ }
1327
+ formatted.push(line);
1328
+ }
1329
+ return formatted.join("\n");
1330
+ }
1331
+ /**
1332
+ * Format file content based on extension.
1333
+ */
1334
+ function formatContent(content, filename) {
1335
+ if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
1336
+ if (filename.endsWith(".md")) return formatMarkdown$2(content);
1337
+ return content;
1338
+ }
1339
+ /**
1340
+ * Display content using system pager (less) if available.
1341
+ * Falls back to console.log if not interactive or pager unavailable.
1342
+ *
1343
+ * @returns Promise that resolves when viewing is complete
1344
+ */
1345
+ async function displayWithPager(content, title) {
1346
+ if (!isInteractive$2()) {
1347
+ console.log(content);
1348
+ return;
1349
+ }
1350
+ const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
1351
+ return new Promise((resolve$1) => {
1352
+ const pager = spawn("less", [
1353
+ "-R",
1354
+ "-S",
1355
+ "-X",
1356
+ "-F",
1357
+ "-K"
1358
+ ], { stdio: [
1359
+ "pipe",
1360
+ "inherit",
1361
+ "inherit"
1362
+ ] });
1363
+ pager.on("error", () => {
1364
+ console.log(header);
1365
+ console.log("");
1366
+ console.log(content);
1367
+ console.log("");
1368
+ resolve$1();
1369
+ });
1370
+ pager.on("close", () => {
1371
+ resolve$1();
1372
+ });
1373
+ pager.stdin.write(header + "\n\n");
1374
+ pager.stdin.write(content);
1375
+ pager.stdin.end();
1376
+ });
1377
+ }
1378
+ /**
1379
+ * Load and display a file with formatting and pagination.
1380
+ */
1381
+ async function viewFile(filePath) {
1382
+ const content = readFileSync(filePath, "utf-8");
1383
+ const filename = basename(filePath);
1384
+ await displayWithPager(formatContent(content, filename), filename);
1385
+ }
1386
+ /**
1387
+ * Show an interactive file viewer chooser.
1388
+ *
1389
+ * Presents a list of files to view:
1390
+ * - "Show report:" for the report output (.report.md) at the top
1391
+ * - "Show source:" for other files (.form.md, .raw.md, .yml)
1392
+ * - "Quit" at the bottom
1393
+ *
1394
+ * Loops until the user selects Quit.
1395
+ *
1396
+ * @param files Array of file options to display
1397
+ */
1398
+ async function showFileViewerChooser(files) {
1399
+ if (!isInteractive$2()) return;
1400
+ console.log("");
1401
+ const reportFile = files.find((f) => f.path.endsWith(".report.md"));
1402
+ const sourceFiles = files.filter((f) => !f.path.endsWith(".report.md"));
1403
+ while (true) {
1404
+ const options = [];
1405
+ if (reportFile) options.push({
1406
+ value: reportFile.path,
1407
+ label: `Show report: ${pc.green(basename(reportFile.path))}`,
1408
+ hint: reportFile.hint ?? ""
1409
+ });
1410
+ for (const file of sourceFiles) options.push({
1411
+ value: file.path,
1412
+ label: `Show source: ${pc.green(basename(file.path))}`,
1413
+ hint: file.hint ?? ""
1414
+ });
1415
+ options.push({
1416
+ value: "quit",
1417
+ label: "Quit",
1418
+ hint: ""
1419
+ });
1420
+ const selection = await p.select({
1421
+ message: "View files:",
1422
+ options
1423
+ });
1424
+ if (p.isCancel(selection) || selection === "quit") break;
1425
+ await viewFile(selection);
1426
+ console.log("");
1427
+ }
1428
+ }
1429
+
1113
1430
  //#endregion
1114
1431
  //#region src/cli/commands/examples.ts
1115
1432
  /**
@@ -1117,10 +1434,12 @@ function showInteractiveOutro(patchCount, cancelled) {
1117
1434
  */
1118
1435
  function printExamplesList() {
1119
1436
  console.log(pc.bold("Available examples:\n"));
1120
- for (const example of EXAMPLE_DEFINITIONS) {
1121
- console.log(` ${pc.cyan(example.id)}`);
1122
- console.log(` ${pc.bold(example.title)}`);
1123
- console.log(` ${pc.dim(example.description)}`);
1437
+ const examples = getAllExamplesWithMetadata();
1438
+ for (const example of examples) {
1439
+ const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
1440
+ console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
1441
+ console.log(` ${pc.bold(example.title ?? example.id)}`);
1442
+ console.log(` ${example.description ?? "No description"}`);
1124
1443
  console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
1125
1444
  console.log("");
1126
1445
  }
@@ -1129,12 +1448,12 @@ function printExamplesList() {
1129
1448
  * Display API availability status at startup.
1130
1449
  */
1131
1450
  function showApiStatus() {
1132
- console.log(pc.dim("API Status:"));
1451
+ console.log("API Status:");
1133
1452
  for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
1134
1453
  const info = getProviderInfo(provider);
1135
1454
  const hasKey = !!process.env[info.envVar];
1136
- const status = hasKey ? pc.green("✓") : pc.dim("○");
1137
- const envVar = hasKey ? pc.dim(info.envVar) : pc.yellow(info.envVar);
1455
+ const status = hasKey ? pc.green("✓") : "○";
1456
+ const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
1138
1457
  console.log(` ${status} ${provider} (${envVar})`);
1139
1458
  }
1140
1459
  console.log("");
@@ -1146,7 +1465,7 @@ function buildModelOptions() {
1146
1465
  const options = [];
1147
1466
  for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1148
1467
  const info = getProviderInfo(provider);
1149
- const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : pc.dim("○");
1468
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1150
1469
  for (const model of models) options.push({
1151
1470
  value: `${provider}/${model}`,
1152
1471
  label: `${provider}/${model}`,
@@ -1184,39 +1503,91 @@ async function promptForModel() {
1184
1503
  return selection;
1185
1504
  }
1186
1505
  /**
1506
+ * Build model options filtered to providers with web search support.
1507
+ */
1508
+ function buildWebSearchModelOptions() {
1509
+ const options = [];
1510
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1511
+ if (!hasWebSearchSupport(provider)) continue;
1512
+ const info = getProviderInfo(provider);
1513
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1514
+ for (const model of models) options.push({
1515
+ value: `${provider}/${model}`,
1516
+ label: `${provider}/${model}`,
1517
+ hint: `${keyStatus} ${info.envVar}`
1518
+ });
1519
+ }
1520
+ options.push({
1521
+ value: "custom",
1522
+ label: "Enter custom model ID...",
1523
+ hint: "provider/model-id format"
1524
+ });
1525
+ return options;
1526
+ }
1527
+ /**
1528
+ * Prompt user to select a model with web search capability for research workflow.
1529
+ */
1530
+ async function promptForWebSearchModel() {
1531
+ const modelOptions = buildWebSearchModelOptions();
1532
+ if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
1533
+ const selection = await p.select({
1534
+ message: "Select LLM model (web search required):",
1535
+ options: modelOptions
1536
+ });
1537
+ if (p.isCancel(selection)) return null;
1538
+ if (selection === "custom") {
1539
+ const customModel = await p.text({
1540
+ message: "Model ID (provider/model-id):",
1541
+ placeholder: "openai/gpt-5-mini",
1542
+ validate: (value) => {
1543
+ if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
1544
+ }
1545
+ });
1546
+ if (p.isCancel(customModel)) return null;
1547
+ return customModel;
1548
+ }
1549
+ return selection;
1550
+ }
1551
+ /**
1187
1552
  * Run the agent fill workflow.
1553
+ * Accepts optional harness config overrides - research uses different defaults.
1188
1554
  */
1189
- async function runAgentFill(form, modelId, _outputPath) {
1555
+ async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1190
1556
  const spinner = p.spinner();
1191
1557
  try {
1192
1558
  spinner.start(`Resolving model: ${modelId}`);
1193
- const { model } = await resolveModel(modelId);
1559
+ const { model, provider } = await resolveModel(modelId);
1194
1560
  spinner.stop(`Model resolved: ${modelId}`);
1195
1561
  const harnessConfig = {
1196
- maxTurns: DEFAULT_MAX_TURNS,
1197
- maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
1198
- maxIssues: DEFAULT_MAX_ISSUES,
1562
+ maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1563
+ maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1564
+ maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1199
1565
  targetRoles: [AGENT_ROLE],
1200
1566
  fillMode: "continue"
1201
1567
  };
1568
+ console.log("");
1569
+ console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1202
1570
  const harness = createHarness(form, harnessConfig);
1203
1571
  const agent = createLiveAgent({
1204
1572
  model,
1573
+ provider,
1205
1574
  targetRole: AGENT_ROLE
1206
1575
  });
1207
- console.log("");
1208
1576
  p.log.step(pc.bold("Agent fill in progress..."));
1209
1577
  let stepResult = harness.step();
1210
1578
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1211
- console.log(pc.dim(` Turn ${stepResult.turnNumber}: ${stepResult.issues.length} issue(s) to address`));
1212
- const { patches } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1579
+ console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1580
+ const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1213
1581
  for (const patch of patches) {
1214
1582
  const typeName = formatPatchType(patch);
1215
1583
  const value = formatPatchValue(patch);
1216
- console.log(pc.dim(` ${pc.cyan(patch.fieldId)} (${typeName}) = ${pc.green(value)}`));
1584
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1585
+ if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1586
+ else console.log(` (${typeName}) = ${pc.green(value)}`);
1217
1587
  }
1218
1588
  stepResult = harness.apply(patches, stepResult.issues);
1219
- console.log(pc.dim(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining`));
1589
+ const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1590
+ console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1220
1591
  if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1221
1592
  }
1222
1593
  if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
@@ -1233,18 +1604,24 @@ async function runAgentFill(form, modelId, _outputPath) {
1233
1604
  }
1234
1605
  /**
1235
1606
  * Run the interactive example scaffolding and filling flow.
1607
+ *
1608
+ * @param preselectedId Optional example ID to pre-select
1609
+ * @param formsDirOverride Optional forms directory override from CLI option
1236
1610
  */
1237
- async function runInteractiveFlow(preselectedId) {
1611
+ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1238
1612
  const startTime = Date.now();
1239
1613
  p.intro(pc.bgCyan(pc.black(" markform examples ")));
1614
+ const formsDir = getFormsDir(formsDirOverride);
1615
+ await ensureFormsDir(formsDir);
1240
1616
  showApiStatus();
1241
1617
  let selectedId = preselectedId;
1242
1618
  if (!selectedId) {
1619
+ const examples = getAllExamplesWithMetadata();
1243
1620
  const selection = await p.select({
1244
1621
  message: "Select an example form to scaffold:",
1245
- options: EXAMPLE_DEFINITIONS.map((example$1) => ({
1622
+ options: examples.map((example$1) => ({
1246
1623
  value: example$1.id,
1247
- label: example$1.title,
1624
+ label: example$1.title ?? example$1.id,
1248
1625
  hint: example$1.description
1249
1626
  }))
1250
1627
  });
@@ -1259,9 +1636,9 @@ async function runInteractiveFlow(preselectedId) {
1259
1636
  p.cancel(`Unknown example: ${selectedId}`);
1260
1637
  process.exit(1);
1261
1638
  }
1262
- const defaultFilename = generateVersionedPath(example.filename);
1639
+ const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
1263
1640
  const filenameResult = await p.text({
1264
- message: "Output filename:",
1641
+ message: `Output filename (in ${formatPath(formsDir)}):`,
1265
1642
  initialValue: defaultFilename,
1266
1643
  validate: (value) => {
1267
1644
  if (!value.trim()) return "Filename is required";
@@ -1273,7 +1650,7 @@ async function runInteractiveFlow(preselectedId) {
1273
1650
  process.exit(0);
1274
1651
  }
1275
1652
  const filename = filenameResult;
1276
- const outputPath = join(process.cwd(), filename);
1653
+ const outputPath = join(formsDir, filename);
1277
1654
  if (existsSync(outputPath)) {
1278
1655
  const overwrite = await p.confirm({
1279
1656
  message: `${filename} already exists. Overwrite?`,
@@ -1287,7 +1664,7 @@ async function runInteractiveFlow(preselectedId) {
1287
1664
  let content;
1288
1665
  try {
1289
1666
  content = loadExampleContent(selectedId);
1290
- writeFileSync(outputPath, content, "utf-8");
1667
+ await writeFile(outputPath, content);
1291
1668
  } catch (error) {
1292
1669
  const message = error instanceof Error ? error.message : String(error);
1293
1670
  p.cancel(`Failed to write file: ${message}`);
@@ -1296,6 +1673,7 @@ async function runInteractiveFlow(preselectedId) {
1296
1673
  p.log.success(`Created ${formatPath(outputPath)}`);
1297
1674
  const form = parseForm(content);
1298
1675
  const targetRoles = [USER_ROLE];
1676
+ let userFillOutputs = null;
1299
1677
  const inspectResult = inspect(form, { targetRoles });
1300
1678
  const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
1301
1679
  const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
@@ -1308,7 +1686,7 @@ async function runInteractiveFlow(preselectedId) {
1308
1686
  dryRun: false,
1309
1687
  quiet: false
1310
1688
  }, "Total time", Date.now() - startTime);
1311
- p.outro(pc.dim("Form scaffolded with no fields to fill."));
1689
+ p.outro("Form scaffolded with no fields to fill.");
1312
1690
  return;
1313
1691
  }
1314
1692
  } else {
@@ -1319,13 +1697,13 @@ async function runInteractiveFlow(preselectedId) {
1319
1697
  process.exit(1);
1320
1698
  }
1321
1699
  if (patches.length > 0) applyPatches(form, patches);
1322
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath);
1700
+ userFillOutputs = await exportMultiFormat(form, outputPath);
1323
1701
  showInteractiveOutro(patches.length, false);
1324
1702
  console.log("");
1325
1703
  p.log.success("Outputs:");
1326
- console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
1327
- console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
1328
- console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
1704
+ console.log(` ${formatPath(userFillOutputs.reportPath)} ${pc.dim("(output report)")}`);
1705
+ console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(output values)")}`);
1706
+ console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(filled markform source)")}`);
1329
1707
  logTiming({
1330
1708
  verbose: false,
1331
1709
  format: "console",
@@ -1334,29 +1712,50 @@ async function runInteractiveFlow(preselectedId) {
1334
1712
  }, "Fill time", Date.now() - startTime);
1335
1713
  }
1336
1714
  const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
1715
+ const isResearchExample = example.type === "research";
1337
1716
  if (agentFieldIssues.length > 0) {
1338
1717
  console.log("");
1718
+ const workflowLabel = isResearchExample ? "research" : "agent fill";
1339
1719
  p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
1720
+ const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
1340
1721
  const runAgent = await p.confirm({
1341
- message: "Run agent fill now?",
1722
+ message: confirmMessage,
1342
1723
  initialValue: true
1343
1724
  });
1344
1725
  if (p.isCancel(runAgent) || !runAgent) {
1345
1726
  console.log("");
1346
- console.log(pc.dim("You can run agent fill later with:"));
1347
- console.log(pc.dim(` markform fill ${formatPath(outputPath)} --model=<provider/model>`));
1348
- p.outro(pc.dim("Happy form filling!"));
1727
+ const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
1728
+ console.log(`You can run ${workflowLabel} later with:`);
1729
+ console.log(cliCommand);
1730
+ if (userFillOutputs) await showFileViewerChooser([
1731
+ {
1732
+ path: userFillOutputs.reportPath,
1733
+ label: "Report",
1734
+ hint: "output report"
1735
+ },
1736
+ {
1737
+ path: userFillOutputs.yamlPath,
1738
+ label: "Values",
1739
+ hint: "output values"
1740
+ },
1741
+ {
1742
+ path: userFillOutputs.formPath,
1743
+ label: "Form",
1744
+ hint: "filled markform source"
1745
+ }
1746
+ ]);
1747
+ p.outro("Happy form filling!");
1349
1748
  return;
1350
1749
  }
1351
- const modelId = await promptForModel();
1750
+ const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
1352
1751
  if (!modelId) {
1353
1752
  p.cancel("Cancelled.");
1354
1753
  process.exit(0);
1355
1754
  }
1356
- const agentDefaultFilename = generateVersionedPath(outputPath);
1755
+ const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
1357
1756
  const agentFilenameResult = await p.text({
1358
- message: "Agent output filename:",
1359
- initialValue: basename(agentDefaultFilename),
1757
+ message: `Agent output filename (in ${formatPath(formsDir)}):`,
1758
+ initialValue: agentDefaultFilename,
1360
1759
  validate: (value) => {
1361
1760
  if (!value.trim()) return "Filename is required";
1362
1761
  }
@@ -1365,39 +1764,64 @@ async function runInteractiveFlow(preselectedId) {
1365
1764
  p.cancel("Cancelled.");
1366
1765
  process.exit(0);
1367
1766
  }
1368
- const agentOutputPath = join(process.cwd(), agentFilenameResult);
1767
+ const agentOutputPath = join(formsDir, agentFilenameResult);
1369
1768
  const agentStartTime = Date.now();
1769
+ const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
1770
+ const configOverrides = isResearchExample ? {
1771
+ maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
1772
+ maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
1773
+ } : void 0;
1370
1774
  try {
1371
- const { success, turnCount: _turnCount } = await runAgentFill(form, modelId, agentOutputPath);
1775
+ const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
1372
1776
  logTiming({
1373
1777
  verbose: false,
1374
1778
  format: "console",
1375
1779
  dryRun: false,
1376
1780
  quiet: false
1377
- }, "Agent fill time", Date.now() - agentStartTime);
1378
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, agentOutputPath);
1781
+ }, timingLabel, Date.now() - agentStartTime);
1782
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, agentOutputPath);
1379
1783
  console.log("");
1380
- p.log.success("Agent fill complete. Outputs:");
1381
- console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
1382
- console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
1383
- console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
1784
+ const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
1785
+ p.log.success(successMessage);
1786
+ console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
1787
+ console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
1788
+ console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
1384
1789
  if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
1790
+ await showFileViewerChooser([
1791
+ {
1792
+ path: reportPath,
1793
+ label: "Report",
1794
+ hint: "output report"
1795
+ },
1796
+ {
1797
+ path: yamlPath,
1798
+ label: "Values",
1799
+ hint: "output values"
1800
+ },
1801
+ {
1802
+ path: formPath,
1803
+ label: "Form",
1804
+ hint: "filled markform source"
1805
+ }
1806
+ ]);
1385
1807
  } catch (error) {
1386
1808
  const message = error instanceof Error ? error.message : String(error);
1387
- p.log.error(`Agent fill failed: ${message}`);
1809
+ const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
1810
+ p.log.error(`${failMessage}: ${message}`);
1388
1811
  console.log("");
1389
- console.log(pc.dim("You can try again with:"));
1390
- console.log(pc.dim(` markform fill ${formatPath(outputPath)} --model=${modelId}`));
1812
+ console.log("You can try again with:");
1813
+ const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
1814
+ console.log(retryCommand);
1391
1815
  }
1392
1816
  }
1393
- p.outro(pc.dim("Happy form filling!"));
1817
+ p.outro("Happy form filling!");
1394
1818
  }
1395
1819
  /**
1396
1820
  * Register the examples command.
1397
1821
  */
1398
1822
  function registerExamplesCommand(program) {
1399
- program.command("examples").description("Scaffold an example form and fill it interactively").option("--list", "List available examples without interactive selection").option("--name <example>", "Select example by ID (still prompts for filename)").action(async (options, cmd) => {
1400
- getCommandContext(cmd);
1823
+ program.command("examples").description("Try out some example forms interactively using the console").option("--list", "List available examples without interactive selection").option("--name <example>", "Select example by ID (still prompts for filename)").action(async (options, cmd) => {
1824
+ const ctx = getCommandContext(cmd);
1401
1825
  try {
1402
1826
  if (options.list) {
1403
1827
  printExamplesList();
@@ -1411,7 +1835,7 @@ function registerExamplesCommand(program) {
1411
1835
  process.exit(1);
1412
1836
  }
1413
1837
  }
1414
- await runInteractiveFlow(options.name);
1838
+ await runInteractiveFlow(options.name, ctx.formsDir);
1415
1839
  } catch (error) {
1416
1840
  logError(error instanceof Error ? error.message : String(error));
1417
1841
  process.exit(1);
@@ -1434,7 +1858,7 @@ function registerExportCommand(program) {
1434
1858
  else if (ctx.format === "markform") format = "markform";
1435
1859
  try {
1436
1860
  logVerbose(ctx, `Reading file: ${file}`);
1437
- const content = await readFile(file);
1861
+ const content = await readFile$1(file);
1438
1862
  logVerbose(ctx, "Parsing form...");
1439
1863
  const form = parseForm(content);
1440
1864
  if (format === "markform") {
@@ -1445,26 +1869,30 @@ function registerExportCommand(program) {
1445
1869
  console.log(serializeRawMarkdown(form));
1446
1870
  return;
1447
1871
  }
1448
- const output = {
1449
- schema: {
1450
- id: form.schema.id,
1451
- title: form.schema.title,
1452
- groups: form.schema.groups.map((group) => ({
1453
- id: group.id,
1454
- title: group.title,
1455
- children: group.children.map((field) => ({
1456
- id: field.id,
1457
- kind: field.kind,
1458
- label: field.label,
1459
- required: field.required,
1460
- ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1461
- id: opt.id,
1462
- label: opt.label
1463
- })) } : {}
1464
- }))
1872
+ const schema = {
1873
+ id: form.schema.id,
1874
+ title: form.schema.title,
1875
+ groups: form.schema.groups.map((group) => ({
1876
+ id: group.id,
1877
+ title: group.title,
1878
+ children: group.children.map((field) => ({
1879
+ id: field.id,
1880
+ kind: field.kind,
1881
+ label: field.label,
1882
+ required: field.required,
1883
+ ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1884
+ id: opt.id,
1885
+ label: opt.label
1886
+ })) } : {}
1465
1887
  }))
1466
- },
1467
- values: form.valuesByFieldId,
1888
+ }))
1889
+ };
1890
+ const values = {};
1891
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
1892
+ const output = {
1893
+ schema,
1894
+ values,
1895
+ notes: form.notes,
1468
1896
  markdown: serialize(form)
1469
1897
  };
1470
1898
  if (format === "json") if (options.compact) console.log(JSON.stringify(output));
@@ -1498,7 +1926,7 @@ function formatConsoleSession(transcript, useColors) {
1498
1926
  lines.push(bold("Harness Config:"));
1499
1927
  lines.push(` Max turns: ${transcript.harness.maxTurns}`);
1500
1928
  lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
1501
- lines.push(` Max issues: ${transcript.harness.maxIssues}`);
1929
+ lines.push(` Max issues/turn: ${transcript.harness.maxIssuesPerTurn}`);
1502
1930
  lines.push("");
1503
1931
  lines.push(bold(`Turns (${transcript.turns.length}):`));
1504
1932
  for (const turn of transcript.turns) {
@@ -1517,7 +1945,7 @@ function formatConsoleSession(transcript, useColors) {
1517
1945
  * Register the fill command.
1518
1946
  */
1519
1947
  function registerFillCommand(program) {
1520
- program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-4o)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES})`, String(DEFAULT_MAX_ISSUES)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
1948
+ program.command("fill <file>").description("Run an agent to autonomously fill a form").option("--mock", "Use mock agent (requires --mock-source)").option("--model <id>", "Model ID for live agent (format: provider/model-id, e.g. openai/gpt-5-mini)").option("--mock-source <file>", "Path to completed form for mock agent").option("--record <file>", "Record session transcript to file").option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_MAX_PATCHES_PER_TURN})`, String(DEFAULT_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues shown per turn (default: ${DEFAULT_MAX_ISSUES_PER_TURN})`, String(DEFAULT_MAX_ISSUES_PER_TURN)).option("--max-fields <n>", "Maximum unique fields per turn (applied before --max-issues)").option("--max-groups <n>", "Maximum unique groups per turn (applied before --max-issues)").option("--roles <roles>", "Target roles to fill (comma-separated, or '*' for all; default: 'agent', or 'user' in --interactive mode)").option("--mode <mode>", "Fill mode: continue (skip filled fields) or overwrite (re-fill; default: continue)").option("-o, --output <file>", "Write final form to file").option("--prompt <file>", "Path to custom system prompt file (appends to default)").option("--instructions <text>", "Inline system prompt (appends to default; takes precedence over --prompt)").option("-i, --interactive", "Interactive mode: prompt user for field values (defaults to user role)").action(async (file, options, cmd) => {
1521
1949
  const ctx = getCommandContext(cmd);
1522
1950
  const filePath = resolve(file);
1523
1951
  try {
@@ -1539,7 +1967,7 @@ function registerFillCommand(program) {
1539
1967
  fillMode = options.mode;
1540
1968
  }
1541
1969
  logVerbose(ctx, `Reading form: ${filePath}`);
1542
- const formContent = await readFile(filePath);
1970
+ const formContent = await readFile$1(filePath);
1543
1971
  logVerbose(ctx, "Parsing form...");
1544
1972
  const form = parseForm(formContent);
1545
1973
  if (options.interactive) {
@@ -1567,24 +1995,30 @@ function registerFillCommand(program) {
1567
1995
  }
1568
1996
  if (patches.length > 0) applyPatches(form, patches);
1569
1997
  const durationMs$1 = Date.now() - startTime;
1570
- const outputPath$1 = options.output ? resolve(options.output) : generateVersionedPath(filePath);
1998
+ let outputPath$1;
1999
+ if (options.output) outputPath$1 = resolve(options.output);
2000
+ else {
2001
+ const formsDir = getFormsDir(ctx.formsDir);
2002
+ await ensureFormsDir(formsDir);
2003
+ outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
2004
+ }
1571
2005
  if (ctx.dryRun) {
1572
2006
  logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
1573
2007
  showInteractiveOutro(patches.length, false);
1574
2008
  } else {
1575
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath$1);
2009
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(form, outputPath$1);
1576
2010
  showInteractiveOutro(patches.length, false);
1577
2011
  console.log("");
1578
2012
  p.log.success("Outputs:");
1579
- console.log(` ${formatPath(formPath)} ${pc.dim("(markform)")}`);
1580
- console.log(` ${formatPath(rawPath)} ${pc.dim("(plain markdown)")}`);
1581
- console.log(` ${formatPath(yamlPath)} ${pc.dim("(values as YAML)")}`);
2013
+ console.log(` ${formatPath(reportPath)} ${pc.dim("(output report)")}`);
2014
+ console.log(` ${formatPath(yamlPath)} ${pc.dim("(output values)")}`);
2015
+ console.log(` ${formatPath(formPath)} ${pc.dim("(filled markform source)")}`);
1582
2016
  }
1583
2017
  logTiming(ctx, "Fill time", durationMs$1);
1584
2018
  if (patches.length > 0) {
1585
2019
  console.log("");
1586
- console.log(pc.dim("Next step: fill remaining fields with agent"));
1587
- console.log(pc.dim(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`));
2020
+ console.log("Next step: fill remaining fields with agent");
2021
+ console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
1588
2022
  }
1589
2023
  process.exit(0);
1590
2024
  }
@@ -1599,22 +2033,22 @@ function registerFillCommand(program) {
1599
2033
  process.exit(1);
1600
2034
  }
1601
2035
  if (targetRoles.includes("*")) logWarn(ctx, "Warning: Filling all roles including user-designated fields");
1602
- const harnessConfig = {
1603
- maxTurns: parseInt(options.maxTurns ?? String(DEFAULT_MAX_TURNS), 10),
1604
- maxPatchesPerTurn: parseInt(options.maxPatches ?? String(DEFAULT_MAX_PATCHES_PER_TURN), 10),
1605
- maxIssues: parseInt(options.maxIssues ?? String(DEFAULT_MAX_ISSUES), 10),
1606
- ...options.maxFields && { maxFieldsPerTurn: parseInt(options.maxFields, 10) },
1607
- ...options.maxGroups && { maxGroupsPerTurn: parseInt(options.maxGroups, 10) },
2036
+ const harnessConfig = resolveHarnessConfig(form, {
2037
+ maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2038
+ maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
2039
+ maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
2040
+ maxFieldsPerTurn: options.maxFields ? parseInt(options.maxFields, 10) : void 0,
2041
+ maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
1608
2042
  targetRoles,
1609
2043
  fillMode
1610
- };
2044
+ });
1611
2045
  const harness = createHarness(form, harnessConfig);
1612
2046
  let agent;
1613
2047
  let mockPath;
1614
2048
  if (options.mock) {
1615
2049
  mockPath = resolve(options.mockSource);
1616
2050
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
1617
- agent = createMockAgent(parseForm(await readFile(mockPath)));
2051
+ agent = createMockAgent(parseForm(await readFile$1(mockPath)));
1618
2052
  } else {
1619
2053
  const modelId = options.model;
1620
2054
  logVerbose(ctx, `Resolving model: ${modelId}`);
@@ -1626,7 +2060,7 @@ function registerFillCommand(program) {
1626
2060
  } else if (options.prompt) {
1627
2061
  const promptPath = resolve(options.prompt);
1628
2062
  logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
1629
- systemPrompt = await readFile(promptPath);
2063
+ systemPrompt = await readFile$1(promptPath);
1630
2064
  }
1631
2065
  const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
1632
2066
  const liveAgent = createLiveAgent({
@@ -1643,21 +2077,24 @@ function registerFillCommand(program) {
1643
2077
  logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
1644
2078
  logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
1645
2079
  logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
1646
- logVerbose(ctx, `Max issues per step: ${harnessConfig.maxIssues}`);
2080
+ logVerbose(ctx, `Max issues per turn: ${harnessConfig.maxIssuesPerTurn}`);
1647
2081
  logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
1648
2082
  logVerbose(ctx, `Fill mode: ${fillMode}`);
1649
2083
  let stepResult = harness.step();
1650
- logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
2084
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1651
2085
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1652
2086
  const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1653
- logInfo(ctx, ` ${pc.yellow(String(patches.length))} patches:`);
2087
+ const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2088
+ logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
1654
2089
  for (const patch of patches) {
1655
2090
  const typeName = formatPatchType(patch);
1656
2091
  const value = formatPatchValue(patch);
1657
- logInfo(ctx, ` ${pc.cyan(patch.fieldId)} ${pc.dim(`(${typeName})`)} ${pc.dim("=")} ${pc.green(value)}`);
2092
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2093
+ if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2094
+ else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
1658
2095
  }
1659
2096
  if (stats) {
1660
- logVerbose(ctx, ` Stats: ${stats.inputTokens ?? 0} in / ${stats.outputTokens ?? 0} out tokens`);
2097
+ logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
1661
2098
  if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
1662
2099
  if (stats.prompts) {
1663
2100
  logVerbose(ctx, ``);
@@ -1679,14 +2116,20 @@ function registerFillCommand(program) {
1679
2116
  if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
1680
2117
  else if (!harness.hasReachedMaxTurns()) {
1681
2118
  stepResult = harness.step();
1682
- logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
2119
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1683
2120
  }
1684
2121
  }
1685
2122
  const durationMs = Date.now() - startTime;
1686
2123
  if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
1687
2124
  else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
1688
2125
  logTiming(ctx, "Fill time", durationMs);
1689
- const outputPath = options.output ? resolve(options.output) : generateVersionedPath(filePath);
2126
+ let outputPath;
2127
+ if (options.output) outputPath = resolve(options.output);
2128
+ else {
2129
+ const formsDir = getFormsDir(ctx.formsDir);
2130
+ await ensureFormsDir(formsDir);
2131
+ outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2132
+ }
1690
2133
  const formMarkdown = serialize(harness.getForm());
1691
2134
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
1692
2135
  else {
@@ -1750,6 +2193,18 @@ function formatState$1(state, useColors) {
1750
2193
  return useColors ? colorFn(text) : text;
1751
2194
  }
1752
2195
  /**
2196
+ * Format answer state badge for console output.
2197
+ */
2198
+ function formatAnswerState(state, useColors) {
2199
+ const [text, colorFn] = {
2200
+ answered: ["answered", pc.green],
2201
+ skipped: ["skipped", pc.yellow],
2202
+ aborted: ["aborted", pc.red],
2203
+ unanswered: ["unanswered", pc.dim]
2204
+ }[state] ?? [state, (s) => s];
2205
+ return useColors ? colorFn(text) : text;
2206
+ }
2207
+ /**
1753
2208
  * Format priority badge for console output.
1754
2209
  *
1755
2210
  * Priority tiers and colors:
@@ -1796,6 +2251,8 @@ function formatFieldValue(value, useColors) {
1796
2251
  if (entries.length === 0) return dim("(no entries)");
1797
2252
  return entries.map(([k, v]) => `${k}:${v}`).join(", ");
1798
2253
  }
2254
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
2255
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
1799
2256
  default: return dim("(unknown)");
1800
2257
  }
1801
2258
  }
@@ -1823,13 +2280,11 @@ function formatConsoleReport$1(report, useColors) {
1823
2280
  lines.push(bold("Progress:"));
1824
2281
  lines.push(` Total fields: ${progress.counts.totalFields}`);
1825
2282
  lines.push(` Required: ${progress.counts.requiredFields}`);
1826
- lines.push(` Answered: ${progress.counts.answeredFields}`);
1827
- lines.push(` Skipped: ${progress.counts.skippedFields}`);
1828
- lines.push(` Complete: ${progress.counts.completeFields}`);
1829
- lines.push(` Incomplete: ${progress.counts.incompleteFields}`);
1830
- lines.push(` Invalid: ${progress.counts.invalidFields}`);
1831
- lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
1832
- lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
2283
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
2284
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
2285
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
2286
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
2287
+ lines.push(` Total notes: ${progress.counts.totalNotes}`);
1833
2288
  lines.push("");
1834
2289
  lines.push(bold("Form Content:"));
1835
2290
  for (const group of report.groups) {
@@ -1837,13 +2292,25 @@ function formatConsoleReport$1(report, useColors) {
1837
2292
  for (const field of group.children) {
1838
2293
  const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
1839
2294
  const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
2295
+ const fieldProgress = progress.fields[field.id];
2296
+ const responseStateBadge = fieldProgress ? `[${formatAnswerState(fieldProgress.answerState, useColors)}]` : "";
2297
+ const notesBadge = fieldProgress?.hasNotes ? cyan(`[${fieldProgress.noteCount} note${fieldProgress.noteCount > 1 ? "s" : ""}]`) : "";
1840
2298
  const value = report.values[field.id];
1841
2299
  const valueStr = formatFieldValue(value, useColors);
1842
- lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge}`.trim());
2300
+ lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge} ${responseStateBadge} ${notesBadge}`.trim());
1843
2301
  lines.push(` ${dim("→")} ${valueStr}`);
1844
2302
  }
1845
2303
  }
1846
2304
  lines.push("");
2305
+ if (report.notes.length > 0) {
2306
+ lines.push(bold(`Notes (${report.notes.length}):`));
2307
+ for (const note of report.notes) {
2308
+ const roleBadge = cyan(`[${note.role}]`);
2309
+ const refLabel = dim(`${note.ref}:`);
2310
+ lines.push(` ${note.id} ${roleBadge} ${refLabel} ${note.text}`.trim());
2311
+ }
2312
+ lines.push("");
2313
+ }
1847
2314
  if (report.issues.length > 0) {
1848
2315
  lines.push(bold(`Issues (${report.issues.length}):`));
1849
2316
  for (const issue of report.issues) {
@@ -1871,11 +2338,13 @@ function registerInspectCommand(program) {
1871
2338
  process.exit(1);
1872
2339
  }
1873
2340
  logVerbose(ctx, `Reading file: ${file}`);
1874
- const content = await readFile(file);
2341
+ const content = await readFile$1(file);
1875
2342
  logVerbose(ctx, "Parsing form...");
1876
2343
  const form = parseForm(content);
1877
2344
  logVerbose(ctx, "Running inspection...");
1878
2345
  const result = inspect(form, { targetRoles });
2346
+ const values = {};
2347
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
1879
2348
  const output = formatOutput(ctx, {
1880
2349
  title: form.schema.title,
1881
2350
  structure: result.structureSummary,
@@ -1892,7 +2361,8 @@ function registerInspectCommand(program) {
1892
2361
  role: field.role
1893
2362
  }))
1894
2363
  })),
1895
- values: form.valuesByFieldId,
2364
+ values,
2365
+ notes: form.notes,
1896
2366
  issues: result.issues.map((issue) => ({
1897
2367
  ref: issue.ref,
1898
2368
  scope: issue.scope,
@@ -1912,7 +2382,7 @@ function registerInspectCommand(program) {
1912
2382
  }
1913
2383
 
1914
2384
  //#endregion
1915
- //#region src/cli/commands/instructions.ts
2385
+ //#region src/cli/commands/readme.ts
1916
2386
  /**
1917
2387
  * Get the path to the README.md file.
1918
2388
  * Works both during development and when installed as a package.
@@ -1938,6 +2408,128 @@ function loadReadme() {
1938
2408
  * Apply basic terminal formatting to markdown content.
1939
2409
  * Colorizes headers, code blocks, and other elements for better readability.
1940
2410
  */
2411
+ function formatMarkdown$1(content, useColors) {
2412
+ if (!useColors) return content;
2413
+ const lines = content.split("\n");
2414
+ const formatted = [];
2415
+ let inCodeBlock = false;
2416
+ for (const line of lines) {
2417
+ if (line.startsWith("```")) {
2418
+ inCodeBlock = !inCodeBlock;
2419
+ formatted.push(pc.dim(line));
2420
+ continue;
2421
+ }
2422
+ if (inCodeBlock) {
2423
+ formatted.push(pc.dim(line));
2424
+ continue;
2425
+ }
2426
+ if (line.startsWith("# ")) {
2427
+ formatted.push(pc.bold(pc.cyan(line)));
2428
+ continue;
2429
+ }
2430
+ if (line.startsWith("## ")) {
2431
+ formatted.push(pc.bold(pc.blue(line)));
2432
+ continue;
2433
+ }
2434
+ if (line.startsWith("### ")) {
2435
+ formatted.push(pc.bold(line));
2436
+ continue;
2437
+ }
2438
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
2439
+ return pc.yellow(code);
2440
+ });
2441
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
2442
+ return pc.bold(text);
2443
+ });
2444
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
2445
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
2446
+ });
2447
+ formatted.push(formattedLine);
2448
+ }
2449
+ return formatted.join("\n");
2450
+ }
2451
+ /**
2452
+ * Check if stdout is an interactive terminal.
2453
+ */
2454
+ function isInteractive$1() {
2455
+ return process.stdout.isTTY === true;
2456
+ }
2457
+ /**
2458
+ * Display content. In a future enhancement, could pipe to a pager for long output.
2459
+ */
2460
+ function displayContent$1(content) {
2461
+ console.log(content);
2462
+ }
2463
+ /**
2464
+ * Register the readme command.
2465
+ */
2466
+ function registerReadmeCommand(program) {
2467
+ program.command("readme").description("✨Display README documentation ← START HERE!").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
2468
+ const ctx = getCommandContext(cmd);
2469
+ try {
2470
+ displayContent$1(formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1() && !ctx.quiet));
2471
+ } catch (error) {
2472
+ logError(error instanceof Error ? error.message : String(error));
2473
+ process.exit(1);
2474
+ }
2475
+ });
2476
+ }
2477
+
2478
+ //#endregion
2479
+ //#region src/cli/commands/report.ts
2480
+ /**
2481
+ * Register the report command.
2482
+ */
2483
+ function registerReportCommand(program) {
2484
+ program.command("report <file>").description("Generate filtered markdown report (excludes instructions, report=false elements)").option("-o, --output <file>", "Output file path (default: stdout)").action(async (file, options, cmd) => {
2485
+ const ctx = getCommandContext(cmd);
2486
+ try {
2487
+ logVerbose(ctx, `Reading file: ${file}`);
2488
+ const content = await readFile$1(file);
2489
+ logVerbose(ctx, "Parsing form...");
2490
+ const form = parseForm(content);
2491
+ logVerbose(ctx, "Generating report...");
2492
+ const reportContent = serializeReportMarkdown(form);
2493
+ if (options.output) {
2494
+ let outputPath = options.output;
2495
+ if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
2496
+ await writeFile(outputPath, reportContent);
2497
+ logVerbose(ctx, `Report written to: ${outputPath}`);
2498
+ } else console.log(reportContent);
2499
+ } catch (error) {
2500
+ logError(error instanceof Error ? error.message : String(error));
2501
+ process.exit(1);
2502
+ }
2503
+ });
2504
+ }
2505
+
2506
+ //#endregion
2507
+ //#region src/cli/commands/spec.ts
2508
+ /**
2509
+ * Get the path to the SPEC.md file.
2510
+ * Works both during development and when installed as a package.
2511
+ */
2512
+ function getSpecPath() {
2513
+ const thisDir = dirname(fileURLToPath(import.meta.url));
2514
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
2515
+ return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
2516
+ }
2517
+ /**
2518
+ * Load the spec content.
2519
+ */
2520
+ function loadSpec() {
2521
+ const specPath = getSpecPath();
2522
+ try {
2523
+ return readFileSync(specPath, "utf-8");
2524
+ } catch (error) {
2525
+ const message = error instanceof Error ? error.message : String(error);
2526
+ throw new Error(`Failed to load SPEC from ${specPath}: ${message}`);
2527
+ }
2528
+ }
2529
+ /**
2530
+ * Apply basic terminal formatting to markdown content.
2531
+ * Colorizes headers, code blocks, and other elements for better readability.
2532
+ */
1941
2533
  function formatMarkdown(content, useColors) {
1942
2534
  if (!useColors) return content;
1943
2535
  const lines = content.split("\n");
@@ -1991,13 +2583,13 @@ function displayContent(content) {
1991
2583
  console.log(content);
1992
2584
  }
1993
2585
  /**
1994
- * Register the instructions command.
2586
+ * Register the spec command.
1995
2587
  */
1996
- function registerInstructionsCommand(program) {
1997
- program.command("instructions").alias("readme").alias("docs").description("Display usage instructions and documentation").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
2588
+ function registerSpecCommand(program) {
2589
+ program.command("spec").description("Display the Markform specification").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
1998
2590
  const ctx = getCommandContext(cmd);
1999
2591
  try {
2000
- displayContent(formatMarkdown(loadReadme(), !options.raw && isInteractive() && !ctx.quiet));
2592
+ displayContent(formatMarkdown(loadSpec(), !options.raw && isInteractive() && !ctx.quiet));
2001
2593
  } catch (error) {
2002
2594
  logError(error instanceof Error ? error.message : String(error));
2003
2595
  process.exit(1);
@@ -2077,15 +2669,18 @@ function openBrowser(url) {
2077
2669
  * Register the serve command.
2078
2670
  */
2079
2671
  function registerServeCommand(program) {
2080
- program.command("serve <file>").description("Serve a form as a web page for browsing").option("-p, --port <port>", "Port to serve on", String(DEFAULT_PORT)).option("--no-open", "Don't open browser automatically").action(async (file, options, cmd) => {
2672
+ program.command("serve <file>").description("Serve a file as a web page (forms are interactive, others are read-only)").option("-p, --port <port>", "Port to serve on", String(DEFAULT_PORT)).option("--no-open", "Don't open browser automatically").action(async (file, options, cmd) => {
2081
2673
  const ctx = getCommandContext(cmd);
2082
2674
  const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
2083
2675
  const filePath = resolve(file);
2676
+ const fileType = detectFileType(filePath);
2084
2677
  try {
2085
2678
  logVerbose(ctx, `Reading file: ${filePath}`);
2086
- let form = parseForm(await readFile(filePath));
2679
+ const content = await readFile$1(filePath);
2680
+ let form = null;
2681
+ if (fileType === "form") form = parseForm(content);
2087
2682
  const server = createServer((req, res) => {
2088
- handleRequest(req, res, form, filePath, ctx, (updatedForm) => {
2683
+ handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
2089
2684
  form = updatedForm;
2090
2685
  }).catch((err) => {
2091
2686
  console.error("Request error:", err);
@@ -2095,7 +2690,8 @@ function registerServeCommand(program) {
2095
2690
  });
2096
2691
  server.listen(port, () => {
2097
2692
  const url = `http://localhost:${port}`;
2098
- logInfo(ctx, pc.green(`\n✓ Form server running at ${pc.bold(url)}\n`));
2693
+ const typeLabel = fileType === "form" ? "Form" : fileType === "unknown" ? "File" : fileType.toUpperCase();
2694
+ logInfo(ctx, pc.green(`\n✓ ${typeLabel} server running at ${pc.bold(url)}\n`));
2099
2695
  logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
2100
2696
  if (options.open !== false) openBrowser(url);
2101
2697
  });
@@ -2112,15 +2708,33 @@ function registerServeCommand(program) {
2112
2708
  }
2113
2709
  /**
2114
2710
  * Handle HTTP requests.
2711
+ * Dispatches to appropriate renderer based on file type.
2115
2712
  */
2116
- async function handleRequest(req, res, form, filePath, ctx, updateForm) {
2713
+ async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
2117
2714
  const url = req.url ?? "/";
2118
- if (req.method === "GET" && url === "/") {
2715
+ if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
2119
2716
  const html = renderFormHtml(form);
2120
2717
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2121
2718
  res.end(html);
2122
- } else if (req.method === "POST" && url === "/save") await handleSave(req, res, form, filePath, ctx, updateForm);
2123
- else if (url === "/api/form") {
2719
+ } else if (fileType === "raw" || fileType === "report") {
2720
+ const html = renderMarkdownHtml(await readFile(filePath, "utf-8"), basename(filePath));
2721
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2722
+ res.end(html);
2723
+ } else if (fileType === "yaml") {
2724
+ const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
2725
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2726
+ res.end(html);
2727
+ } else if (fileType === "json") {
2728
+ const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
2729
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2730
+ res.end(html);
2731
+ } else {
2732
+ const html = renderPlainTextHtml(await readFile(filePath, "utf-8"), basename(filePath));
2733
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2734
+ res.end(html);
2735
+ }
2736
+ else if (req.method === "POST" && url === "/save" && fileType === "form" && form) await handleSave(req, res, form, filePath, ctx, updateForm);
2737
+ else if (url === "/api/form" && form) {
2124
2738
  res.writeHead(200, { "Content-Type": "application/json" });
2125
2739
  res.end(JSON.stringify({ schema: form.schema }));
2126
2740
  } else {
@@ -2153,7 +2767,8 @@ function formDataToPatches(formData, form) {
2153
2767
  if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
2154
2768
  patches.push({
2155
2769
  op: "skip_field",
2156
- fieldId
2770
+ fieldId,
2771
+ role: "user"
2157
2772
  });
2158
2773
  continue;
2159
2774
  }
@@ -2334,9 +2949,9 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
2334
2949
  * @public Exported for testing.
2335
2950
  */
2336
2951
  function renderFormHtml(form) {
2337
- const { schema, valuesByFieldId, skipsByFieldId } = form;
2952
+ const { schema, responsesByFieldId } = form;
2338
2953
  const formTitle = schema.title ?? schema.id;
2339
- const groupsHtml = schema.groups.map((group) => renderGroup(group, valuesByFieldId, skipsByFieldId ?? {})).join("\n");
2954
+ const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
2340
2955
  return `<!DOCTYPE html>
2341
2956
  <html lang="en">
2342
2957
  <head>
@@ -2583,9 +3198,12 @@ function renderFormHtml(form) {
2583
3198
  /**
2584
3199
  * Render a field group as HTML.
2585
3200
  */
2586
- function renderGroup(group, values, skips) {
3201
+ function renderGroup(group, responses) {
2587
3202
  const groupTitle = group.title ?? group.id;
2588
- const fieldsHtml = group.children.map((field) => renderFieldHtml(field, values[field.id], skips[field.id])).join("\n");
3203
+ const fieldsHtml = group.children.map((field) => {
3204
+ const response = responses[field.id];
3205
+ return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
3206
+ }).join("\n");
2589
3207
  return `
2590
3208
  <div class="group">
2591
3209
  <h2>${escapeHtml(groupTitle)}</h2>
@@ -2596,13 +3214,13 @@ function renderGroup(group, values, skips) {
2596
3214
  * Render a field as HTML.
2597
3215
  * @public Exported for testing.
2598
3216
  */
2599
- function renderFieldHtml(field, value, skipInfo) {
2600
- const isSkipped = skipInfo?.skipped === true;
3217
+ function renderFieldHtml(field, value, isSkipped) {
3218
+ const skipped = isSkipped === true;
2601
3219
  const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
2602
3220
  const typeLabel = `<span class="type-badge">${field.kind}</span>`;
2603
- const skippedBadge = isSkipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
2604
- const fieldClass = isSkipped ? "field field-skipped" : "field";
2605
- const disabledAttr = isSkipped ? " disabled" : "";
3221
+ const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
3222
+ const fieldClass = skipped ? "field field-skipped" : "field";
3223
+ const disabledAttr = skipped ? " disabled" : "";
2606
3224
  let inputHtml;
2607
3225
  switch (field.kind) {
2608
3226
  case "string":
@@ -2631,7 +3249,7 @@ function renderFieldHtml(field, value, skipInfo) {
2631
3249
  break;
2632
3250
  default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
2633
3251
  }
2634
- const skipButton = !field.required && !isSkipped ? `<div class="field-actions">
3252
+ const skipButton = !field.required && !skipped ? `<div class="field-actions">
2635
3253
  <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
2636
3254
  </div>` : "";
2637
3255
  return `
@@ -2775,6 +3393,182 @@ function renderCheckboxesInput(field, value, disabledAttr) {
2775
3393
  function escapeHtml(str) {
2776
3394
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2777
3395
  }
3396
+ /** Common styles for read-only viewers */
3397
+ const READ_ONLY_STYLES = `
3398
+ * { box-sizing: border-box; }
3399
+ body {
3400
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3401
+ line-height: 1.6;
3402
+ max-width: 900px;
3403
+ margin: 0 auto;
3404
+ padding: 2rem;
3405
+ background: #f8f9fa;
3406
+ color: #212529;
3407
+ }
3408
+ h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; font-size: 1.5rem; }
3409
+ .content {
3410
+ background: white;
3411
+ border-radius: 8px;
3412
+ padding: 1.5rem;
3413
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
3414
+ }
3415
+ pre {
3416
+ background: #1e1e1e;
3417
+ color: #d4d4d4;
3418
+ padding: 1rem;
3419
+ border-radius: 6px;
3420
+ overflow-x: auto;
3421
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
3422
+ font-size: 0.9rem;
3423
+ line-height: 1.5;
3424
+ }
3425
+ .badge {
3426
+ font-size: 0.75rem;
3427
+ padding: 0.2rem 0.5rem;
3428
+ background: #e9ecef;
3429
+ border-radius: 4px;
3430
+ color: #6c757d;
3431
+ margin-left: 0.75rem;
3432
+ font-weight: normal;
3433
+ }
3434
+ `;
3435
+ /**
3436
+ * Render markdown content as read-only HTML.
3437
+ * Simple rendering without full markdown parsing.
3438
+ */
3439
+ function renderMarkdownHtml(content, filename) {
3440
+ const lines = content.split("\n");
3441
+ let html = "";
3442
+ let inParagraph = false;
3443
+ for (const line of lines) {
3444
+ const trimmed = line.trim();
3445
+ if (trimmed.startsWith("# ")) {
3446
+ if (inParagraph) {
3447
+ html += "</p>";
3448
+ inParagraph = false;
3449
+ }
3450
+ html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
3451
+ } else if (trimmed.startsWith("## ")) {
3452
+ if (inParagraph) {
3453
+ html += "</p>";
3454
+ inParagraph = false;
3455
+ }
3456
+ html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
3457
+ } else if (trimmed.startsWith("### ")) {
3458
+ if (inParagraph) {
3459
+ html += "</p>";
3460
+ inParagraph = false;
3461
+ }
3462
+ html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
3463
+ } else if (trimmed === "") {
3464
+ if (inParagraph) {
3465
+ html += "</p>";
3466
+ inParagraph = false;
3467
+ }
3468
+ } else {
3469
+ if (!inParagraph) {
3470
+ html += "<p>";
3471
+ inParagraph = true;
3472
+ } else html += "<br>";
3473
+ html += escapeHtml(trimmed);
3474
+ }
3475
+ }
3476
+ if (inParagraph) html += "</p>";
3477
+ return `<!DOCTYPE html>
3478
+ <html lang="en">
3479
+ <head>
3480
+ <meta charset="UTF-8">
3481
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3482
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3483
+ <style>${READ_ONLY_STYLES}
3484
+ h2 { color: #495057; font-size: 1.3rem; margin-top: 1.5rem; }
3485
+ h3 { color: #6c757d; font-size: 1.1rem; margin-top: 1.25rem; }
3486
+ h4 { color: #6c757d; font-size: 1rem; margin-top: 1rem; }
3487
+ p { margin: 0.75rem 0; }
3488
+ </style>
3489
+ </head>
3490
+ <body>
3491
+ <h1>${escapeHtml(filename)}<span class="badge">Markdown</span></h1>
3492
+ <div class="content">
3493
+ ${html}
3494
+ </div>
3495
+ </body>
3496
+ </html>`;
3497
+ }
3498
+ /**
3499
+ * Render YAML content with syntax highlighting.
3500
+ */
3501
+ function renderYamlHtml(content, filename) {
3502
+ const highlighted = content.split("\n").map((line) => {
3503
+ const colonIndex = line.indexOf(":");
3504
+ if (colonIndex > 0 && !line.trim().startsWith("#") && !line.trim().startsWith("-")) return `<span style="color:#9cdcfe">${escapeHtml(line.slice(0, colonIndex))}</span>${escapeHtml(line.slice(colonIndex))}`;
3505
+ if (line.trim().startsWith("#")) return `<span style="color:#6a9955">${escapeHtml(line)}</span>`;
3506
+ return escapeHtml(line);
3507
+ }).join("\n");
3508
+ return `<!DOCTYPE html>
3509
+ <html lang="en">
3510
+ <head>
3511
+ <meta charset="UTF-8">
3512
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3513
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3514
+ <style>${READ_ONLY_STYLES}</style>
3515
+ </head>
3516
+ <body>
3517
+ <h1>${escapeHtml(filename)}<span class="badge">YAML</span></h1>
3518
+ <div class="content">
3519
+ <pre>${highlighted}</pre>
3520
+ </div>
3521
+ </body>
3522
+ </html>`;
3523
+ }
3524
+ /**
3525
+ * Render JSON content with syntax highlighting and formatting.
3526
+ */
3527
+ function renderJsonHtml(content, filename) {
3528
+ let formatted;
3529
+ try {
3530
+ const parsed = JSON.parse(content);
3531
+ formatted = JSON.stringify(parsed, null, 2);
3532
+ } catch {
3533
+ formatted = content;
3534
+ }
3535
+ const highlighted = formatted.replace(/"([^"]+)":/g, "<span style=\"color:#9cdcfe\">\"$1\"</span>:").replace(/: "([^"]+)"/g, ": <span style=\"color:#ce9178\">\"$1\"</span>").replace(/: (\d+\.?\d*)/g, ": <span style=\"color:#b5cea8\">$1</span>").replace(/: (true|false|null)/g, ": <span style=\"color:#569cd6\">$1</span>");
3536
+ return `<!DOCTYPE html>
3537
+ <html lang="en">
3538
+ <head>
3539
+ <meta charset="UTF-8">
3540
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3541
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3542
+ <style>${READ_ONLY_STYLES}</style>
3543
+ </head>
3544
+ <body>
3545
+ <h1>${escapeHtml(filename)}<span class="badge">JSON</span></h1>
3546
+ <div class="content">
3547
+ <pre>${highlighted}</pre>
3548
+ </div>
3549
+ </body>
3550
+ </html>`;
3551
+ }
3552
+ /**
3553
+ * Render plain text content.
3554
+ */
3555
+ function renderPlainTextHtml(content, filename) {
3556
+ return `<!DOCTYPE html>
3557
+ <html lang="en">
3558
+ <head>
3559
+ <meta charset="UTF-8">
3560
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3561
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3562
+ <style>${READ_ONLY_STYLES}</style>
3563
+ </head>
3564
+ <body>
3565
+ <h1>${escapeHtml(filename)}<span class="badge">Text</span></h1>
3566
+ <div class="content">
3567
+ <pre>${escapeHtml(content)}</pre>
3568
+ </div>
3569
+ </body>
3570
+ </html>`;
3571
+ }
2778
3572
 
2779
3573
  //#endregion
2780
3574
  //#region src/cli/commands/render.ts
@@ -2798,7 +3592,7 @@ function registerRenderCommand(program) {
2798
3592
  const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
2799
3593
  try {
2800
3594
  logVerbose(ctx, `Reading file: ${filePath}`);
2801
- const content = await readFile(filePath);
3595
+ const content = await readFile$1(filePath);
2802
3596
  logVerbose(ctx, "Parsing form...");
2803
3597
  const form = parseForm(content);
2804
3598
  logVerbose(ctx, "Rendering HTML...");
@@ -2816,6 +3610,156 @@ function registerRenderCommand(program) {
2816
3610
  });
2817
3611
  }
2818
3612
 
3613
+ //#endregion
3614
+ //#region src/cli/lib/initialValues.ts
3615
+ /**
3616
+ * Parse initial value inputs from CLI flags.
3617
+ *
3618
+ * Supports formats:
3619
+ * - fieldId=value (string values)
3620
+ * - fieldId:number=123 (explicit number)
3621
+ * - fieldId:list=a,b,c (comma-separated list)
3622
+ *
3623
+ * @param inputs Array of input strings in "fieldId=value" format
3624
+ * @returns Array of patches to apply as initial values
3625
+ */
3626
+ function parseInitialValues(inputs) {
3627
+ const patches = [];
3628
+ for (const input of inputs) {
3629
+ const equalsIndex = input.indexOf("=");
3630
+ if (equalsIndex === -1) throw new Error(`Invalid input format: "${input}" (expected "fieldId=value")`);
3631
+ const fieldSpec = input.slice(0, equalsIndex);
3632
+ const value = input.slice(equalsIndex + 1);
3633
+ const colonIndex = fieldSpec.indexOf(":");
3634
+ let fieldId;
3635
+ let type = null;
3636
+ if (colonIndex !== -1) {
3637
+ fieldId = fieldSpec.slice(0, colonIndex);
3638
+ type = fieldSpec.slice(colonIndex + 1).toLowerCase();
3639
+ } else fieldId = fieldSpec;
3640
+ if (type === "number") {
3641
+ const numValue = parseFloat(value);
3642
+ if (isNaN(numValue)) throw new Error(`Invalid number value for "${fieldId}": "${value}"`);
3643
+ patches.push({
3644
+ op: "set_number",
3645
+ fieldId,
3646
+ value: numValue
3647
+ });
3648
+ } else if (type === "list") {
3649
+ const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3650
+ patches.push({
3651
+ op: "set_string_list",
3652
+ fieldId,
3653
+ items
3654
+ });
3655
+ } else patches.push({
3656
+ op: "set_string",
3657
+ fieldId,
3658
+ value
3659
+ });
3660
+ }
3661
+ return patches;
3662
+ }
3663
+ /**
3664
+ * Validate that all initial values reference valid field IDs.
3665
+ *
3666
+ * @param patches Patches to validate
3667
+ * @param validFieldIds Set of valid field IDs from the form
3668
+ * @returns Array of invalid field IDs (empty if all valid)
3669
+ */
3670
+ function validateInitialValueFields(patches, validFieldIds) {
3671
+ const invalid = [];
3672
+ for (const patch of patches) if ("fieldId" in patch && patch.fieldId && !validFieldIds.has(patch.fieldId)) invalid.push(patch.fieldId);
3673
+ return invalid;
3674
+ }
3675
+
3676
+ //#endregion
3677
+ //#region src/cli/commands/research.ts
3678
+ /**
3679
+ * Register the research command.
3680
+ */
3681
+ function registerResearchCommand(program) {
3682
+ program.command("research <input>").description("Fill a form using a web-search-enabled model").option("--model <provider/model>", "LLM model to use (e.g., google/gemini-2.5-flash). Required.").option("--output <path>", "Output path for filled form (default: auto-generated in forms directory)").option("--input <fieldId=value>", "Set initial field value (can be used multiple times)", (value, previous) => previous.concat([value]), []).option("--max-turns <n>", `Maximum turns (default: ${DEFAULT_MAX_TURNS})`, String(DEFAULT_MAX_TURNS)).option("--max-patches <n>", `Maximum patches per turn (default: ${DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN)).option("--max-issues <n>", `Maximum issues per turn (default: ${DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN})`, String(DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN)).option("--transcript", "Save session transcript").action(async (input, options, cmd) => {
3683
+ const ctx = getCommandContext(cmd);
3684
+ const startTime = Date.now();
3685
+ try {
3686
+ if (!options.model) {
3687
+ logError("--model is required");
3688
+ console.log("");
3689
+ console.log(formatSuggestedLlms());
3690
+ process.exit(1);
3691
+ }
3692
+ const modelId = options.model;
3693
+ if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3694
+ const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3695
+ logError(`Model "${modelId}" does not support web search.`);
3696
+ console.log("");
3697
+ console.log(pc.yellow("Research forms require web search capabilities."));
3698
+ console.log(`Use a model from: ${webSearchProviders.join(", ")}`);
3699
+ console.log("");
3700
+ console.log("Examples:");
3701
+ console.log(" --model openai/gpt-5-mini");
3702
+ console.log(" --model anthropic/claude-sonnet-4-5");
3703
+ console.log(" --model google/gemini-2.5-flash");
3704
+ console.log(" --model xai/grok-4");
3705
+ process.exit(1);
3706
+ }
3707
+ const inputPath = resolve(input);
3708
+ logVerbose(ctx, `Input: ${inputPath}`);
3709
+ const form = parseForm(await readFile$1(inputPath));
3710
+ logVerbose(ctx, `Parsed form: ${form.schema.id}`);
3711
+ const initialInputs = options.input ?? [];
3712
+ if (initialInputs.length > 0) {
3713
+ const patches = parseInitialValues(initialInputs);
3714
+ const invalidFields = validateInitialValueFields(patches, new Set(form.orderIndex));
3715
+ if (invalidFields.length > 0) logWarn(ctx, `Unknown field IDs: ${invalidFields.join(", ")}`);
3716
+ applyPatches(form, patches);
3717
+ logInfo(ctx, `Applied ${patches.length} initial value(s)`);
3718
+ }
3719
+ const formsDir = getFormsDir(ctx.formsDir);
3720
+ let outputPath;
3721
+ if (options.output) outputPath = resolve(options.output);
3722
+ else outputPath = generateVersionedPathInFormsDir(inputPath, formsDir);
3723
+ logVerbose(ctx, `Output: ${outputPath}`);
3724
+ const maxTurns = parseInt(options.maxTurns, 10);
3725
+ const maxPatchesPerTurn = parseInt(options.maxPatches, 10);
3726
+ const maxIssuesPerTurn = parseInt(options.maxIssues, 10);
3727
+ logInfo(ctx, `Research fill with model: ${modelId}`);
3728
+ logVerbose(ctx, `Max turns: ${maxTurns}`);
3729
+ logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3730
+ logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
3731
+ const result = await runResearch(form, {
3732
+ model: modelId,
3733
+ maxTurns,
3734
+ maxPatchesPerTurn,
3735
+ maxIssuesPerTurn,
3736
+ targetRoles: [AGENT_ROLE],
3737
+ fillMode: "continue"
3738
+ });
3739
+ if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3740
+ logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3741
+ logInfo(ctx, `Turns: ${result.totalTurns}`);
3742
+ if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
3743
+ const { reportPath, yamlPath, formPath } = await exportMultiFormat(result.form, outputPath);
3744
+ logSuccess(ctx, "Outputs:");
3745
+ console.log(` ${reportPath} ${pc.dim("(output report)")}`);
3746
+ console.log(` ${yamlPath} ${pc.dim("(output values)")}`);
3747
+ console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
3748
+ if (options.transcript && result.transcript) {
3749
+ const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
3750
+ const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3751
+ const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
3752
+ await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3753
+ logInfo(ctx, `Transcript: ${transcriptPath}`);
3754
+ }
3755
+ logTiming(ctx, "Research fill", Date.now() - startTime);
3756
+ } catch (error) {
3757
+ logError(error instanceof Error ? error.message : String(error));
3758
+ process.exit(1);
3759
+ }
3760
+ });
3761
+ }
3762
+
2819
3763
  //#endregion
2820
3764
  //#region src/cli/commands/validate.ts
2821
3765
  /**
@@ -2875,12 +3819,10 @@ function formatConsoleReport(report, useColors) {
2875
3819
  lines.push(bold("Progress:"));
2876
3820
  lines.push(` Total fields: ${progress.counts.totalFields}`);
2877
3821
  lines.push(` Required: ${progress.counts.requiredFields}`);
2878
- lines.push(` Submitted: ${progress.counts.submittedFields}`);
2879
- lines.push(` Complete: ${progress.counts.completeFields}`);
2880
- lines.push(` Incomplete: ${progress.counts.incompleteFields}`);
2881
- lines.push(` Invalid: ${progress.counts.invalidFields}`);
2882
- lines.push(` Empty (required): ${progress.counts.emptyRequiredFields}`);
2883
- lines.push(` Empty (optional): ${progress.counts.emptyOptionalFields}`);
3822
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
3823
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
3824
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
3825
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
2884
3826
  lines.push("");
2885
3827
  if (report.issues.length > 0) {
2886
3828
  lines.push(bold(`Issues (${report.issues.length}):`));
@@ -2900,7 +3842,7 @@ function registerValidateCommand(program) {
2900
3842
  const ctx = getCommandContext(cmd);
2901
3843
  try {
2902
3844
  logVerbose(ctx, `Reading file: ${file}`);
2903
- const content = await readFile(file);
3845
+ const content = await readFile$1(file);
2904
3846
  logVerbose(ctx, "Parsing form...");
2905
3847
  const form = parseForm(content);
2906
3848
  logVerbose(ctx, "Running validation...");
@@ -2952,18 +3894,22 @@ function withColoredHelp(cmd) {
2952
3894
  */
2953
3895
  function createProgram() {
2954
3896
  const program = withColoredHelp(new Command());
2955
- program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console");
2956
- registerInspectCommand(program);
2957
- registerValidateCommand(program);
3897
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(VERSION).showHelpAfterError().option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--dry-run", "Show what would be done without making changes").option("--format <format>", `Output format: ${OUTPUT_FORMATS.join(", ")}`, "console").option("--forms-dir <dir>", `Directory for form output (default: ${DEFAULT_FORMS_DIR})`);
3898
+ registerReadmeCommand(program);
3899
+ registerDocsCommand(program);
3900
+ registerSpecCommand(program);
2958
3901
  registerApplyCommand(program);
2959
- registerExportCommand(program);
2960
3902
  registerDumpCommand(program);
2961
- registerRenderCommand(program);
2962
- registerServeCommand(program);
3903
+ registerExamplesCommand(program);
3904
+ registerExportCommand(program);
2963
3905
  registerFillCommand(program);
3906
+ registerInspectCommand(program);
2964
3907
  registerModelsCommand(program);
2965
- registerExamplesCommand(program);
2966
- registerInstructionsCommand(program);
3908
+ registerRenderCommand(program);
3909
+ registerReportCommand(program);
3910
+ registerResearchCommand(program);
3911
+ registerServeCommand(program);
3912
+ registerValidateCommand(program);
2967
3913
  return program;
2968
3914
  }
2969
3915
  /**