markform 0.1.1 → 0.1.2

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 +338 -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-BfAGTHMh.mjs} +837 -730
  7. package/dist/bin.mjs +6 -3
  8. package/dist/{cli-pjOiHgCW.mjs → cli-B3NVm6zL.mjs} +1349 -422
  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-BXRkGFpG.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 +73 -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 +9 -5
  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 SUGGESTED_LLMS, D as detectFileType, E as deriveExportPath, M as formatSuggestedLlms, O as getFormsDir, P as hasWebSearchSupport, 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 WEB_SEARCH_CONFIG, k as parseRolesFlag, 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-BfAGTHMh.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-BXRkGFpG.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,46 +248,100 @@ 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.
421
336
  *
422
337
  * @param basePath - Path to the .form.md file
423
338
  * @returns Object with paths for all export formats
424
339
  */
425
340
  function deriveExportPaths(basePath) {
426
341
  return {
427
- formPath: basePath,
428
- rawPath: basePath.replace(/\.form\.md$/, ".raw.md"),
429
- yamlPath: basePath.replace(/\.form\.md$/, ".yml")
342
+ formPath: deriveExportPath(basePath, "form"),
343
+ rawPath: deriveExportPath(basePath, "raw"),
344
+ yamlPath: deriveExportPath(basePath, "yaml")
430
345
  };
431
346
  }
432
347
  /**
@@ -435,24 +350,101 @@ function deriveExportPaths(basePath) {
435
350
  * Writes:
436
351
  * - Markform format (.form.md) - canonical form with directives
437
352
  * - Raw markdown (.raw.md) - plain readable markdown (no directives)
438
- * - YAML values (.yml) - extracted field values only
353
+ * - YAML values (.yml) - structured format with state and notes (markform-218, markform-219)
439
354
  *
440
355
  * @param form - The parsed form to export
441
356
  * @param basePath - Base path for the .form.md file (other paths are derived)
442
357
  * @returns Paths to all exported files
443
358
  */
444
- function exportMultiFormat(form, basePath) {
359
+ async function exportMultiFormat(form, basePath) {
445
360
  const paths = deriveExportPaths(basePath);
446
361
  const formContent = serialize(form);
447
- writeFileSync(paths.formPath, formContent, "utf-8");
362
+ await writeFile(paths.formPath, formContent);
448
363
  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");
364
+ await writeFile(paths.rawPath, rawContent);
365
+ const values = toStructuredValues(form);
366
+ const notes = toNotesArray(form);
367
+ const exportData = {
368
+ values,
369
+ ...notes.length > 0 && { notes }
370
+ };
371
+ const yamlContent = YAML.stringify(exportData);
372
+ await writeFile(paths.yamlPath, yamlContent);
453
373
  return paths;
454
374
  }
455
375
 
376
+ //#endregion
377
+ //#region src/cli/commands/dump.ts
378
+ /**
379
+ * Format a field response for console display, including state information.
380
+ */
381
+ function formatFieldResponse(response, useColors) {
382
+ const dim = useColors ? pc.dim : (s) => s;
383
+ const green = useColors ? pc.green : (s) => s;
384
+ const yellow = useColors ? pc.yellow : (s) => s;
385
+ if (response.state === "unanswered") return dim("(unanswered)");
386
+ if (response.state === "skipped") return yellow(`[skipped]${response.reason ? ` ${response.reason}` : ""}`);
387
+ if (response.state === "aborted") return yellow(`[aborted]${response.reason ? ` ${response.reason}` : ""}`);
388
+ const value = response.value;
389
+ if (!value) return dim("(empty)");
390
+ switch (value.kind) {
391
+ case "string": return value.value ? green(`"${value.value}"`) : dim("(empty)");
392
+ case "number": return value.value !== null ? green(String(value.value)) : dim("(empty)");
393
+ case "string_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
394
+ case "single_select": return value.selected ? green(value.selected) : dim("(none selected)");
395
+ case "multi_select": return value.selected.length > 0 ? green(`[${value.selected.join(", ")}]`) : dim("(none selected)");
396
+ case "checkboxes": {
397
+ const entries = Object.entries(value.values);
398
+ if (entries.length === 0) return dim("(no entries)");
399
+ return entries.map(([k, v]) => `${k}:${v}`).join(", ");
400
+ }
401
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
402
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.join(", ")}]`) : dim("(empty)");
403
+ default: return dim("(unknown)");
404
+ }
405
+ }
406
+ /**
407
+ * Format form responses for console output, showing all fields with their states.
408
+ */
409
+ function formatConsoleResponses(form, useColors) {
410
+ const lines = [];
411
+ const bold = useColors ? pc.bold : (s) => s;
412
+ const dim = useColors ? pc.dim : (s) => s;
413
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) {
414
+ const valueStr = formatFieldResponse(response, useColors);
415
+ lines.push(`${bold(fieldId)}: ${valueStr}`);
416
+ }
417
+ if (lines.length === 0) lines.push(dim("(no fields)"));
418
+ return lines.join("\n");
419
+ }
420
+ /**
421
+ * Register the dump command.
422
+ */
423
+ function registerDumpCommand(program) {
424
+ program.command("dump <file>").description("Extract and display form values with state (lightweight inspect)").action(async (file, _options, cmd) => {
425
+ const ctx = getCommandContext(cmd);
426
+ try {
427
+ logVerbose(ctx, `Reading file: ${file}`);
428
+ const content = await readFile$1(file);
429
+ logVerbose(ctx, "Parsing form...");
430
+ const form = parseForm(content);
431
+ if (ctx.format === "json" || ctx.format === "yaml") {
432
+ const output = formatOutput(ctx, {
433
+ values: toStructuredValues(form),
434
+ ...form.notes.length > 0 && { notes: toNotesArray(form) }
435
+ }, () => "");
436
+ console.log(output);
437
+ } else {
438
+ const output = formatOutput(ctx, form, (data, useColors) => formatConsoleResponses(data, useColors));
439
+ console.log(output);
440
+ }
441
+ } catch (error) {
442
+ logError(error instanceof Error ? error.message : String(error));
443
+ process.exit(1);
444
+ }
445
+ });
446
+ }
447
+
456
448
  //#endregion
457
449
  //#region src/cli/lib/patchFormat.ts
458
450
  /** Maximum characters for a patch value display before truncation */
@@ -477,8 +469,13 @@ function formatPatchValue(patch) {
477
469
  case "set_checkboxes": return truncate(Object.entries(patch.values).map(([k, v]) => `${k}:${v}`).join(", "));
478
470
  case "clear_field": return "(cleared)";
479
471
  case "skip_field": return patch.reason ? truncate(`(skipped: ${patch.reason})`) : "(skipped)";
472
+ case "abort_field": return patch.reason ? truncate(`(aborted: ${patch.reason})`) : "(aborted)";
480
473
  case "set_url": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
481
474
  case "set_url_list": return patch.items.length > 0 ? truncate(`[${patch.items.join(", ")}]`) : "(empty)";
475
+ case "set_date": return patch.value ? truncate(`"${patch.value}"`) : "(empty)";
476
+ case "set_year": return patch.value !== null ? String(patch.value) : "(empty)";
477
+ case "add_note": return truncate(`note: ${patch.text}`);
478
+ case "remove_note": return `(remove note ${patch.noteId})`;
482
479
  }
483
480
  }
484
481
  /**
@@ -494,46 +491,103 @@ function formatPatchType(patch) {
494
491
  case "set_checkboxes": return "checkboxes";
495
492
  case "clear_field": return "clear";
496
493
  case "skip_field": return "skip";
494
+ case "abort_field": return "abort";
497
495
  case "set_url": return "url";
498
496
  case "set_url_list": return "url_list";
497
+ case "set_date": return "date";
498
+ case "set_year": return "year";
499
+ case "add_note": return "note";
500
+ case "remove_note": return "remove_note";
499
501
  }
500
502
  }
501
503
 
504
+ //#endregion
505
+ //#region src/cli/lib/formatting.ts
506
+ /**
507
+ * Get a short status word from an issue reason.
508
+ */
509
+ function issueReasonToStatus(reason) {
510
+ switch (reason) {
511
+ case "required_missing": return "missing";
512
+ case "validation_error": return "invalid";
513
+ case "checkbox_incomplete": return "incomplete";
514
+ case "min_items_not_met": return "too-few";
515
+ case "optional_empty": return "empty";
516
+ default: return "issue";
517
+ }
518
+ }
519
+ /**
520
+ * Format a single issue as "fieldId (status)".
521
+ */
522
+ function formatIssueBrief(issue) {
523
+ const status = issueReasonToStatus(issue.reason);
524
+ return `${issue.ref} (${status})`;
525
+ }
526
+ /**
527
+ * Format issues for turn logging - shows count and brief field list.
528
+ * Example: "5 issue(s): company_name (missing), revenue (invalid), ..."
529
+ */
530
+ function formatTurnIssues(issues, maxShow = 5) {
531
+ const count = issues.length;
532
+ if (count === 0) return "0 issues";
533
+ return `${count} issue(s): ${issues.slice(0, maxShow).map(formatIssueBrief).join(", ")}${count > maxShow ? `, +${count - maxShow} more` : ""}`;
534
+ }
535
+
502
536
  //#endregion
503
537
  //#region src/cli/examples/exampleRegistry.ts
504
538
  /**
505
539
  * Example form registry.
506
540
  * Provides form content from the examples directory for the examples CLI command.
541
+ *
542
+ * Metadata (title, description) is loaded dynamically from the form's YAML frontmatter
543
+ * rather than being duplicated here, following the single source of truth principle.
544
+ */
545
+ /**
546
+ * Example definitions without content or metadata.
547
+ * Title and description are loaded dynamically from frontmatter.
507
548
  */
508
- /** Example definitions without content - content is loaded lazily. */
509
549
  const EXAMPLE_DEFINITIONS = [
510
550
  {
511
551
  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
552
  filename: "simple.form.md",
515
- path: "simple/simple.form.md"
553
+ path: "simple/simple.form.md",
554
+ type: "fill"
516
555
  },
517
556
  {
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"
557
+ id: "movie-research-minimal",
558
+ filename: "movie-research-minimal.form.md",
559
+ path: "movie-research/movie-research-minimal.form.md",
560
+ type: "research"
561
+ },
562
+ {
563
+ id: "movie-research-basic",
564
+ filename: "movie-research-basic.form.md",
565
+ path: "movie-research/movie-research-basic.form.md",
566
+ type: "research"
567
+ },
568
+ {
569
+ id: "movie-research-deep",
570
+ filename: "movie-research-deep.form.md",
571
+ path: "movie-research/movie-research-deep.form.md",
572
+ type: "research"
523
573
  },
524
574
  {
525
575
  id: "earnings-analysis",
526
- title: "Company Quarterly Analysis",
527
- description: "Financial analysis with one user field (company) and agent-filled quarterly analysis sections.",
528
576
  filename: "earnings-analysis.form.md",
529
- path: "earnings-analysis/earnings-analysis.form.md"
577
+ path: "earnings-analysis/earnings-analysis.form.md",
578
+ type: "research"
530
579
  },
531
580
  {
532
581
  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
582
  filename: "startup-deep-research.form.md",
536
- path: "startup-deep-research/startup-deep-research.form.md"
583
+ path: "startup-deep-research/startup-deep-research.form.md",
584
+ type: "research"
585
+ },
586
+ {
587
+ id: "celebrity-deep-research",
588
+ filename: "celebrity-deep-research.form.md",
589
+ path: "celebrity-deep-research/celebrity-deep-research.form.md",
590
+ type: "research"
537
591
  }
538
592
  ];
539
593
  /**
@@ -547,7 +601,7 @@ function getExamplesDir() {
547
601
  }
548
602
  /**
549
603
  * Load the content of an example form.
550
- * @param exampleId - The example ID (e.g., 'simple', 'political-research')
604
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
551
605
  * @returns The form content as a string
552
606
  * @throws Error if the example is not found
553
607
  */
@@ -569,7 +623,7 @@ function getExampleById(id) {
569
623
  }
570
624
  /**
571
625
  * Get the absolute path to an example's source file.
572
- * @param exampleId - The example ID (e.g., 'simple', 'political-research')
626
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
573
627
  * @returns The absolute path to the example form file
574
628
  * @throws Error if the example is not found
575
629
  */
@@ -578,6 +632,48 @@ function getExamplePath(exampleId) {
578
632
  if (!example) throw new Error(`Unknown example: ${exampleId}`);
579
633
  return join(getExamplesDir(), example.path);
580
634
  }
635
+ /**
636
+ * Extract YAML frontmatter from a markdown file content.
637
+ * @param content - The markdown file content
638
+ * @returns The parsed frontmatter object or null if no frontmatter found
639
+ */
640
+ function extractFrontmatter(content) {
641
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
642
+ if (!frontmatterMatch || !frontmatterMatch[1]) return null;
643
+ try {
644
+ return YAML.parse(frontmatterMatch[1]);
645
+ } catch {
646
+ return null;
647
+ }
648
+ }
649
+ /**
650
+ * Load metadata (title, description) from an example's YAML frontmatter.
651
+ * @param exampleId - The example ID (e.g., 'simple', 'celebrity-deep-research')
652
+ * @returns Object with title and description from frontmatter
653
+ */
654
+ function loadExampleMetadata(exampleId) {
655
+ const frontmatter = extractFrontmatter(loadExampleContent(exampleId));
656
+ if (!frontmatter || !frontmatter.markform) return {};
657
+ const markform = frontmatter.markform;
658
+ return {
659
+ title: typeof markform.title === "string" ? markform.title : void 0,
660
+ description: typeof markform.description === "string" ? markform.description : void 0
661
+ };
662
+ }
663
+ /**
664
+ * Get all example definitions with metadata loaded from frontmatter.
665
+ * @returns Array of ExampleDefinition with title and description populated
666
+ */
667
+ function getAllExamplesWithMetadata() {
668
+ return EXAMPLE_DEFINITIONS.map((example) => {
669
+ const metadata = loadExampleMetadata(example.id);
670
+ return {
671
+ ...example,
672
+ title: metadata.title,
673
+ description: metadata.description
674
+ };
675
+ });
676
+ }
581
677
 
582
678
  //#endregion
583
679
  //#region src/cli/lib/versioning.ts
@@ -654,6 +750,29 @@ function generateVersionedPath(filePath) {
654
750
  }
655
751
  return candidate;
656
752
  }
753
+ /**
754
+ * Generate a versioned filename within the forms directory.
755
+ *
756
+ * Derives the base name from the input path and creates a versioned
757
+ * output path within the specified forms directory.
758
+ *
759
+ * @param inputPath - Original input file path (used to derive basename)
760
+ * @param formsDir - Absolute path to the forms directory
761
+ * @returns Absolute path to a non-existent versioned file in formsDir
762
+ */
763
+ function generateVersionedPathInFormsDir(inputPath, formsDir) {
764
+ const inputFilename = basename(inputPath);
765
+ const parsed = parseVersionedPath(inputFilename);
766
+ const baseName = parsed?.base ?? inputFilename.replace(/\.form\.md$/i, "");
767
+ const extension = parsed?.extension ?? ".form.md";
768
+ let version = 1;
769
+ let candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
770
+ while (existsSync(candidate)) {
771
+ version++;
772
+ candidate = join(formsDir, `${baseName}-filled${version}${extension}`);
773
+ }
774
+ return candidate;
775
+ }
657
776
 
658
777
  //#endregion
659
778
  //#region src/cli/lib/interactivePrompts.ts
@@ -683,7 +802,7 @@ function getFieldById(form, fieldId) {
683
802
  */
684
803
  function formatFieldLabel(ctx) {
685
804
  const required = ctx.field.required ? pc.red("*") : "";
686
- const progress = pc.dim(`(${ctx.index} of ${ctx.total})`);
805
+ const progress = `(${ctx.index} of ${ctx.total})`;
687
806
  return `${ctx.field.label}${required} ${progress}`;
688
807
  }
689
808
  /**
@@ -693,6 +812,7 @@ function createSkipPatch(field) {
693
812
  return {
694
813
  op: "skip_field",
695
814
  fieldId: field.id,
815
+ role: "user",
696
816
  reason: "User skipped in console"
697
817
  };
698
818
  }
@@ -1057,9 +1177,10 @@ async function runInteractiveFill(form, issues) {
1057
1177
  const field = getFieldById(form, issue.ref);
1058
1178
  if (!field) continue;
1059
1179
  index++;
1180
+ const response = form.responsesByFieldId[field.id];
1060
1181
  const patch = await promptForField({
1061
1182
  field,
1062
- currentValue: form.valuesByFieldId[field.id],
1183
+ currentValue: response?.state === "answered" ? response.value : void 0,
1063
1184
  description: getFieldDescription(form, field.id),
1064
1185
  index,
1065
1186
  total: uniqueFieldIssues.length
@@ -1110,6 +1231,183 @@ function showInteractiveOutro(patchCount, cancelled) {
1110
1231
  p.outro(`✓ ${patchCount} field(s) updated.`);
1111
1232
  }
1112
1233
 
1234
+ //#endregion
1235
+ //#region src/cli/lib/fileViewer.ts
1236
+ /**
1237
+ * File viewer utility for displaying files with colorization and pagination.
1238
+ *
1239
+ * Provides a modern console experience:
1240
+ * - Syntax highlighting for markdown and YAML
1241
+ * - Pagination using system pager (less) when available
1242
+ * - Fallback to console output when not interactive
1243
+ */
1244
+ /**
1245
+ * Check if stdout is an interactive terminal.
1246
+ */
1247
+ function isInteractive$2() {
1248
+ return process.stdout.isTTY === true;
1249
+ }
1250
+ /**
1251
+ * Apply terminal formatting to markdown content.
1252
+ * Colorizes headers, code blocks, and other elements.
1253
+ */
1254
+ function formatMarkdown$2(content) {
1255
+ const lines = content.split("\n");
1256
+ const formatted = [];
1257
+ let inCodeBlock = false;
1258
+ for (const line of lines) {
1259
+ if (line.startsWith("```")) {
1260
+ inCodeBlock = !inCodeBlock;
1261
+ formatted.push(pc.dim(line));
1262
+ continue;
1263
+ }
1264
+ if (inCodeBlock) {
1265
+ formatted.push(pc.cyan(line));
1266
+ continue;
1267
+ }
1268
+ if (line.startsWith("# ")) {
1269
+ formatted.push(pc.bold(pc.magenta(line)));
1270
+ continue;
1271
+ }
1272
+ if (line.startsWith("## ")) {
1273
+ formatted.push(pc.bold(pc.blue(line)));
1274
+ continue;
1275
+ }
1276
+ if (line.startsWith("### ")) {
1277
+ formatted.push(pc.bold(pc.cyan(line)));
1278
+ continue;
1279
+ }
1280
+ if (line.startsWith("#### ")) {
1281
+ formatted.push(pc.bold(line));
1282
+ continue;
1283
+ }
1284
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
1285
+ return pc.yellow(code);
1286
+ });
1287
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
1288
+ return pc.bold(text);
1289
+ });
1290
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
1291
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
1292
+ });
1293
+ formattedLine = formattedLine.replace(/\{%\s*(\w+)\s*([^%]*)\s*%\}/g, (_match, tag, attrs) => {
1294
+ return `${pc.dim("{% ")}${pc.green(tag)}${pc.dim(attrs)} ${pc.dim("%}")}`;
1295
+ });
1296
+ formatted.push(formattedLine);
1297
+ }
1298
+ return formatted.join("\n");
1299
+ }
1300
+ /**
1301
+ * Apply terminal formatting to YAML content.
1302
+ */
1303
+ function formatYaml(content) {
1304
+ const lines = content.split("\n");
1305
+ const formatted = [];
1306
+ for (const line of lines) {
1307
+ if (line.trim().startsWith("#")) {
1308
+ formatted.push(pc.dim(line));
1309
+ continue;
1310
+ }
1311
+ const match = /^(\s*)([^:]+)(:)(.*)$/.exec(line);
1312
+ if (match) {
1313
+ const [, indent, key, colon, value] = match;
1314
+ formatted.push(`${indent}${pc.cyan(key)}${pc.dim(colon)}${pc.yellow(value)}`);
1315
+ continue;
1316
+ }
1317
+ if (line.trim().startsWith("-")) {
1318
+ formatted.push(pc.green(line));
1319
+ continue;
1320
+ }
1321
+ formatted.push(line);
1322
+ }
1323
+ return formatted.join("\n");
1324
+ }
1325
+ /**
1326
+ * Format file content based on extension.
1327
+ */
1328
+ function formatContent(content, filename) {
1329
+ if (filename.endsWith(".yml") || filename.endsWith(".yaml")) return formatYaml(content);
1330
+ if (filename.endsWith(".md")) return formatMarkdown$2(content);
1331
+ return content;
1332
+ }
1333
+ /**
1334
+ * Display content using system pager (less) if available.
1335
+ * Falls back to console.log if not interactive or pager unavailable.
1336
+ *
1337
+ * @returns Promise that resolves when viewing is complete
1338
+ */
1339
+ async function displayWithPager(content, title) {
1340
+ if (!isInteractive$2()) {
1341
+ console.log(content);
1342
+ return;
1343
+ }
1344
+ const header = `${pc.bgCyan(pc.black(` ${title} `))}`;
1345
+ return new Promise((resolve$1) => {
1346
+ const pager = spawn("less", [
1347
+ "-R",
1348
+ "-S",
1349
+ "-X",
1350
+ "-F",
1351
+ "-K"
1352
+ ], { stdio: [
1353
+ "pipe",
1354
+ "inherit",
1355
+ "inherit"
1356
+ ] });
1357
+ pager.on("error", () => {
1358
+ console.log(header);
1359
+ console.log("");
1360
+ console.log(content);
1361
+ console.log("");
1362
+ resolve$1();
1363
+ });
1364
+ pager.on("close", () => {
1365
+ resolve$1();
1366
+ });
1367
+ pager.stdin.write(header + "\n\n");
1368
+ pager.stdin.write(content);
1369
+ pager.stdin.end();
1370
+ });
1371
+ }
1372
+ /**
1373
+ * Load and display a file with formatting and pagination.
1374
+ */
1375
+ async function viewFile(filePath) {
1376
+ const content = readFileSync(filePath, "utf-8");
1377
+ const filename = basename(filePath);
1378
+ await displayWithPager(formatContent(content, filename), filename);
1379
+ }
1380
+ /**
1381
+ * Show an interactive file viewer chooser.
1382
+ *
1383
+ * Presents a list of files to view with descriptions, plus a Skip option.
1384
+ * Loops until the user selects Skip.
1385
+ *
1386
+ * @param files Array of file options to display
1387
+ */
1388
+ async function showFileViewerChooser(files) {
1389
+ if (!isInteractive$2()) return;
1390
+ console.log("");
1391
+ while (true) {
1392
+ const options = [...files.map((file) => ({
1393
+ value: file.path,
1394
+ label: pc.green(basename(file.path)),
1395
+ hint: file.hint ?? ""
1396
+ })), {
1397
+ value: "skip",
1398
+ label: "Done viewing",
1399
+ hint: "exit file viewer"
1400
+ }];
1401
+ const selection = await p.select({
1402
+ message: "View an output file?",
1403
+ options
1404
+ });
1405
+ if (p.isCancel(selection) || selection === "skip") break;
1406
+ await viewFile(selection);
1407
+ console.log("");
1408
+ }
1409
+ }
1410
+
1113
1411
  //#endregion
1114
1412
  //#region src/cli/commands/examples.ts
1115
1413
  /**
@@ -1117,10 +1415,12 @@ function showInteractiveOutro(patchCount, cancelled) {
1117
1415
  */
1118
1416
  function printExamplesList() {
1119
1417
  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)}`);
1418
+ const examples = getAllExamplesWithMetadata();
1419
+ for (const example of examples) {
1420
+ const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
1421
+ console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
1422
+ console.log(` ${pc.bold(example.title ?? example.id)}`);
1423
+ console.log(` ${example.description ?? "No description"}`);
1124
1424
  console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
1125
1425
  console.log("");
1126
1426
  }
@@ -1129,12 +1429,12 @@ function printExamplesList() {
1129
1429
  * Display API availability status at startup.
1130
1430
  */
1131
1431
  function showApiStatus() {
1132
- console.log(pc.dim("API Status:"));
1432
+ console.log("API Status:");
1133
1433
  for (const [provider, _models] of Object.entries(SUGGESTED_LLMS)) {
1134
1434
  const info = getProviderInfo(provider);
1135
1435
  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);
1436
+ const status = hasKey ? pc.green("✓") : "○";
1437
+ const envVar = hasKey ? info.envVar : pc.yellow(info.envVar);
1138
1438
  console.log(` ${status} ${provider} (${envVar})`);
1139
1439
  }
1140
1440
  console.log("");
@@ -1146,7 +1446,7 @@ function buildModelOptions() {
1146
1446
  const options = [];
1147
1447
  for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1148
1448
  const info = getProviderInfo(provider);
1149
- const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : pc.dim("○");
1449
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1150
1450
  for (const model of models) options.push({
1151
1451
  value: `${provider}/${model}`,
1152
1452
  label: `${provider}/${model}`,
@@ -1184,39 +1484,91 @@ async function promptForModel() {
1184
1484
  return selection;
1185
1485
  }
1186
1486
  /**
1487
+ * Build model options filtered to providers with web search support.
1488
+ */
1489
+ function buildWebSearchModelOptions() {
1490
+ const options = [];
1491
+ for (const [provider, models] of Object.entries(SUGGESTED_LLMS)) {
1492
+ if (!hasWebSearchSupport(provider)) continue;
1493
+ const info = getProviderInfo(provider);
1494
+ const keyStatus = !!process.env[info.envVar] ? pc.green("✓") : "○";
1495
+ for (const model of models) options.push({
1496
+ value: `${provider}/${model}`,
1497
+ label: `${provider}/${model}`,
1498
+ hint: `${keyStatus} ${info.envVar}`
1499
+ });
1500
+ }
1501
+ options.push({
1502
+ value: "custom",
1503
+ label: "Enter custom model ID...",
1504
+ hint: "provider/model-id format"
1505
+ });
1506
+ return options;
1507
+ }
1508
+ /**
1509
+ * Prompt user to select a model with web search capability for research workflow.
1510
+ */
1511
+ async function promptForWebSearchModel() {
1512
+ const modelOptions = buildWebSearchModelOptions();
1513
+ if (modelOptions.length === 1) p.log.warn("No web-search-capable providers found. OpenAI, Google, or xAI API key required.");
1514
+ const selection = await p.select({
1515
+ message: "Select LLM model (web search required):",
1516
+ options: modelOptions
1517
+ });
1518
+ if (p.isCancel(selection)) return null;
1519
+ if (selection === "custom") {
1520
+ const customModel = await p.text({
1521
+ message: "Model ID (provider/model-id):",
1522
+ placeholder: "openai/gpt-5-mini",
1523
+ validate: (value) => {
1524
+ if (!value.includes("/")) return "Format: provider/model-id (e.g., openai/gpt-5-mini)";
1525
+ }
1526
+ });
1527
+ if (p.isCancel(customModel)) return null;
1528
+ return customModel;
1529
+ }
1530
+ return selection;
1531
+ }
1532
+ /**
1187
1533
  * Run the agent fill workflow.
1534
+ * Accepts optional harness config overrides - research uses different defaults.
1188
1535
  */
1189
- async function runAgentFill(form, modelId, _outputPath) {
1536
+ async function runAgentFill(form, modelId, _outputPath, configOverrides) {
1190
1537
  const spinner = p.spinner();
1191
1538
  try {
1192
1539
  spinner.start(`Resolving model: ${modelId}`);
1193
- const { model } = await resolveModel(modelId);
1540
+ const { model, provider } = await resolveModel(modelId);
1194
1541
  spinner.stop(`Model resolved: ${modelId}`);
1195
1542
  const harnessConfig = {
1196
- maxTurns: DEFAULT_MAX_TURNS,
1197
- maxPatchesPerTurn: DEFAULT_MAX_PATCHES_PER_TURN,
1198
- maxIssues: DEFAULT_MAX_ISSUES,
1543
+ maxTurns: configOverrides?.maxTurns ?? DEFAULT_MAX_TURNS,
1544
+ maxPatchesPerTurn: configOverrides?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
1545
+ maxIssuesPerTurn: configOverrides?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
1199
1546
  targetRoles: [AGENT_ROLE],
1200
1547
  fillMode: "continue"
1201
1548
  };
1549
+ console.log("");
1550
+ console.log(`Config: max_turns=${harnessConfig.maxTurns}, max_issues_per_turn=${harnessConfig.maxIssuesPerTurn}, max_patches_per_turn=${harnessConfig.maxPatchesPerTurn}`);
1202
1551
  const harness = createHarness(form, harnessConfig);
1203
1552
  const agent = createLiveAgent({
1204
1553
  model,
1554
+ provider,
1205
1555
  targetRole: AGENT_ROLE
1206
1556
  });
1207
- console.log("");
1208
1557
  p.log.step(pc.bold("Agent fill in progress..."));
1209
1558
  let stepResult = harness.step();
1210
1559
  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);
1560
+ console.log(` ${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1561
+ const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1213
1562
  for (const patch of patches) {
1214
1563
  const typeName = formatPatchType(patch);
1215
1564
  const value = formatPatchValue(patch);
1216
- console.log(pc.dim(` ${pc.cyan(patch.fieldId)} (${typeName}) = ${pc.green(value)}`));
1565
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
1566
+ if (fieldId) console.log(` ${pc.cyan(fieldId)} (${typeName}) = ${pc.green(value)}`);
1567
+ else console.log(` (${typeName}) = ${pc.green(value)}`);
1217
1568
  }
1218
1569
  stepResult = harness.apply(patches, stepResult.issues);
1219
- console.log(pc.dim(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining`));
1570
+ const tokenInfo = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
1571
+ console.log(` ${patches.length} patch(es) applied, ${stepResult.issues.length} remaining${tokenInfo}`);
1220
1572
  if (!stepResult.isComplete && !harness.hasReachedMaxTurns()) stepResult = harness.step();
1221
1573
  }
1222
1574
  if (stepResult.isComplete) p.log.success(pc.green(`Form completed in ${harness.getTurnNumber()} turn(s)`));
@@ -1233,18 +1585,24 @@ async function runAgentFill(form, modelId, _outputPath) {
1233
1585
  }
1234
1586
  /**
1235
1587
  * Run the interactive example scaffolding and filling flow.
1588
+ *
1589
+ * @param preselectedId Optional example ID to pre-select
1590
+ * @param formsDirOverride Optional forms directory override from CLI option
1236
1591
  */
1237
- async function runInteractiveFlow(preselectedId) {
1592
+ async function runInteractiveFlow(preselectedId, formsDirOverride) {
1238
1593
  const startTime = Date.now();
1239
1594
  p.intro(pc.bgCyan(pc.black(" markform examples ")));
1595
+ const formsDir = getFormsDir(formsDirOverride);
1596
+ await ensureFormsDir(formsDir);
1240
1597
  showApiStatus();
1241
1598
  let selectedId = preselectedId;
1242
1599
  if (!selectedId) {
1600
+ const examples = getAllExamplesWithMetadata();
1243
1601
  const selection = await p.select({
1244
1602
  message: "Select an example form to scaffold:",
1245
- options: EXAMPLE_DEFINITIONS.map((example$1) => ({
1603
+ options: examples.map((example$1) => ({
1246
1604
  value: example$1.id,
1247
- label: example$1.title,
1605
+ label: example$1.title ?? example$1.id,
1248
1606
  hint: example$1.description
1249
1607
  }))
1250
1608
  });
@@ -1259,9 +1617,9 @@ async function runInteractiveFlow(preselectedId) {
1259
1617
  p.cancel(`Unknown example: ${selectedId}`);
1260
1618
  process.exit(1);
1261
1619
  }
1262
- const defaultFilename = generateVersionedPath(example.filename);
1620
+ const defaultFilename = basename(generateVersionedPathInFormsDir(example.filename, formsDir));
1263
1621
  const filenameResult = await p.text({
1264
- message: "Output filename:",
1622
+ message: `Output filename (in ${formatPath(formsDir)}):`,
1265
1623
  initialValue: defaultFilename,
1266
1624
  validate: (value) => {
1267
1625
  if (!value.trim()) return "Filename is required";
@@ -1273,7 +1631,7 @@ async function runInteractiveFlow(preselectedId) {
1273
1631
  process.exit(0);
1274
1632
  }
1275
1633
  const filename = filenameResult;
1276
- const outputPath = join(process.cwd(), filename);
1634
+ const outputPath = join(formsDir, filename);
1277
1635
  if (existsSync(outputPath)) {
1278
1636
  const overwrite = await p.confirm({
1279
1637
  message: `${filename} already exists. Overwrite?`,
@@ -1287,7 +1645,7 @@ async function runInteractiveFlow(preselectedId) {
1287
1645
  let content;
1288
1646
  try {
1289
1647
  content = loadExampleContent(selectedId);
1290
- writeFileSync(outputPath, content, "utf-8");
1648
+ await writeFile(outputPath, content);
1291
1649
  } catch (error) {
1292
1650
  const message = error instanceof Error ? error.message : String(error);
1293
1651
  p.cancel(`Failed to write file: ${message}`);
@@ -1296,6 +1654,7 @@ async function runInteractiveFlow(preselectedId) {
1296
1654
  p.log.success(`Created ${formatPath(outputPath)}`);
1297
1655
  const form = parseForm(content);
1298
1656
  const targetRoles = [USER_ROLE];
1657
+ let userFillOutputs = null;
1299
1658
  const inspectResult = inspect(form, { targetRoles });
1300
1659
  const fieldIssues = inspectResult.issues.filter((i) => i.scope === "field");
1301
1660
  const uniqueFieldIds = new Set(fieldIssues.map((i) => i.ref));
@@ -1308,7 +1667,7 @@ async function runInteractiveFlow(preselectedId) {
1308
1667
  dryRun: false,
1309
1668
  quiet: false
1310
1669
  }, "Total time", Date.now() - startTime);
1311
- p.outro(pc.dim("Form scaffolded with no fields to fill."));
1670
+ p.outro("Form scaffolded with no fields to fill.");
1312
1671
  return;
1313
1672
  }
1314
1673
  } else {
@@ -1319,13 +1678,13 @@ async function runInteractiveFlow(preselectedId) {
1319
1678
  process.exit(1);
1320
1679
  }
1321
1680
  if (patches.length > 0) applyPatches(form, patches);
1322
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath);
1681
+ userFillOutputs = await exportMultiFormat(form, outputPath);
1323
1682
  showInteractiveOutro(patches.length, false);
1324
1683
  console.log("");
1325
1684
  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)")}`);
1685
+ console.log(` ${formatPath(userFillOutputs.formPath)} ${pc.dim("(markform)")}`);
1686
+ console.log(` ${formatPath(userFillOutputs.rawPath)} ${pc.dim("(plain markdown)")}`);
1687
+ console.log(` ${formatPath(userFillOutputs.yamlPath)} ${pc.dim("(values as YAML)")}`);
1329
1688
  logTiming({
1330
1689
  verbose: false,
1331
1690
  format: "console",
@@ -1334,29 +1693,50 @@ async function runInteractiveFlow(preselectedId) {
1334
1693
  }, "Fill time", Date.now() - startTime);
1335
1694
  }
1336
1695
  const agentFieldIssues = inspect(form, { targetRoles: [AGENT_ROLE] }).issues.filter((i) => i.scope === "field");
1696
+ const isResearchExample = example.type === "research";
1337
1697
  if (agentFieldIssues.length > 0) {
1338
1698
  console.log("");
1699
+ const workflowLabel = isResearchExample ? "research" : "agent fill";
1339
1700
  p.log.info(`This form has ${agentFieldIssues.length} agent-role field(s) remaining.`);
1701
+ const confirmMessage = isResearchExample ? "Run research now? (requires web search)" : "Run agent fill now?";
1340
1702
  const runAgent = await p.confirm({
1341
- message: "Run agent fill now?",
1703
+ message: confirmMessage,
1342
1704
  initialValue: true
1343
1705
  });
1344
1706
  if (p.isCancel(runAgent) || !runAgent) {
1345
1707
  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!"));
1708
+ const cliCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=<provider/model>` : ` markform fill ${formatPath(outputPath)} --model=<provider/model>`;
1709
+ console.log(`You can run ${workflowLabel} later with:`);
1710
+ console.log(cliCommand);
1711
+ if (userFillOutputs) await showFileViewerChooser([
1712
+ {
1713
+ path: userFillOutputs.formPath,
1714
+ label: "Markform",
1715
+ hint: "form with tags"
1716
+ },
1717
+ {
1718
+ path: userFillOutputs.rawPath,
1719
+ label: "Plain Markdown",
1720
+ hint: "rendered output"
1721
+ },
1722
+ {
1723
+ path: userFillOutputs.yamlPath,
1724
+ label: "YAML",
1725
+ hint: "extracted values"
1726
+ }
1727
+ ]);
1728
+ p.outro("Happy form filling!");
1349
1729
  return;
1350
1730
  }
1351
- const modelId = await promptForModel();
1731
+ const modelId = isResearchExample ? await promptForWebSearchModel() : await promptForModel();
1352
1732
  if (!modelId) {
1353
1733
  p.cancel("Cancelled.");
1354
1734
  process.exit(0);
1355
1735
  }
1356
- const agentDefaultFilename = generateVersionedPath(outputPath);
1736
+ const agentDefaultFilename = basename(generateVersionedPathInFormsDir(outputPath, formsDir));
1357
1737
  const agentFilenameResult = await p.text({
1358
- message: "Agent output filename:",
1359
- initialValue: basename(agentDefaultFilename),
1738
+ message: `Agent output filename (in ${formatPath(formsDir)}):`,
1739
+ initialValue: agentDefaultFilename,
1360
1740
  validate: (value) => {
1361
1741
  if (!value.trim()) return "Filename is required";
1362
1742
  }
@@ -1365,39 +1745,64 @@ async function runInteractiveFlow(preselectedId) {
1365
1745
  p.cancel("Cancelled.");
1366
1746
  process.exit(0);
1367
1747
  }
1368
- const agentOutputPath = join(process.cwd(), agentFilenameResult);
1748
+ const agentOutputPath = join(formsDir, agentFilenameResult);
1369
1749
  const agentStartTime = Date.now();
1750
+ const timingLabel = isResearchExample ? "Research time" : "Agent fill time";
1751
+ const configOverrides = isResearchExample ? {
1752
+ maxIssuesPerTurn: DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN,
1753
+ maxPatchesPerTurn: DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN
1754
+ } : void 0;
1370
1755
  try {
1371
- const { success, turnCount: _turnCount } = await runAgentFill(form, modelId, agentOutputPath);
1756
+ const { success } = await runAgentFill(form, modelId, agentOutputPath, configOverrides);
1372
1757
  logTiming({
1373
1758
  verbose: false,
1374
1759
  format: "console",
1375
1760
  dryRun: false,
1376
1761
  quiet: false
1377
- }, "Agent fill time", Date.now() - agentStartTime);
1378
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, agentOutputPath);
1762
+ }, timingLabel, Date.now() - agentStartTime);
1763
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, agentOutputPath);
1379
1764
  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)")}`);
1765
+ const successMessage = isResearchExample ? "Research complete. Outputs:" : "Agent fill complete. Outputs:";
1766
+ p.log.success(successMessage);
1767
+ console.log(` ${formatPath(formPath)} (markform)`);
1768
+ console.log(` ${formatPath(rawPath)} (plain markdown)`);
1769
+ console.log(` ${formatPath(yamlPath)} (values as YAML)`);
1384
1770
  if (!success) p.log.warn("Agent did not complete all fields. You may need to run it again.");
1771
+ await showFileViewerChooser([
1772
+ {
1773
+ path: formPath,
1774
+ label: "Markform",
1775
+ hint: "form with tags"
1776
+ },
1777
+ {
1778
+ path: rawPath,
1779
+ label: "Plain Markdown",
1780
+ hint: "rendered output"
1781
+ },
1782
+ {
1783
+ path: yamlPath,
1784
+ label: "YAML",
1785
+ hint: "extracted values"
1786
+ }
1787
+ ]);
1385
1788
  } catch (error) {
1386
1789
  const message = error instanceof Error ? error.message : String(error);
1387
- p.log.error(`Agent fill failed: ${message}`);
1790
+ const failMessage = isResearchExample ? "Research failed" : "Agent fill failed";
1791
+ p.log.error(`${failMessage}: ${message}`);
1388
1792
  console.log("");
1389
- console.log(pc.dim("You can try again with:"));
1390
- console.log(pc.dim(` markform fill ${formatPath(outputPath)} --model=${modelId}`));
1793
+ console.log("You can try again with:");
1794
+ const retryCommand = isResearchExample ? ` markform research ${formatPath(outputPath)} --model=${modelId}` : ` markform fill ${formatPath(outputPath)} --model=${modelId}`;
1795
+ console.log(retryCommand);
1391
1796
  }
1392
1797
  }
1393
- p.outro(pc.dim("Happy form filling!"));
1798
+ p.outro("Happy form filling!");
1394
1799
  }
1395
1800
  /**
1396
1801
  * Register the examples command.
1397
1802
  */
1398
1803
  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);
1804
+ 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) => {
1805
+ const ctx = getCommandContext(cmd);
1401
1806
  try {
1402
1807
  if (options.list) {
1403
1808
  printExamplesList();
@@ -1411,7 +1816,7 @@ function registerExamplesCommand(program) {
1411
1816
  process.exit(1);
1412
1817
  }
1413
1818
  }
1414
- await runInteractiveFlow(options.name);
1819
+ await runInteractiveFlow(options.name, ctx.formsDir);
1415
1820
  } catch (error) {
1416
1821
  logError(error instanceof Error ? error.message : String(error));
1417
1822
  process.exit(1);
@@ -1434,7 +1839,7 @@ function registerExportCommand(program) {
1434
1839
  else if (ctx.format === "markform") format = "markform";
1435
1840
  try {
1436
1841
  logVerbose(ctx, `Reading file: ${file}`);
1437
- const content = await readFile(file);
1842
+ const content = await readFile$1(file);
1438
1843
  logVerbose(ctx, "Parsing form...");
1439
1844
  const form = parseForm(content);
1440
1845
  if (format === "markform") {
@@ -1445,26 +1850,30 @@ function registerExportCommand(program) {
1445
1850
  console.log(serializeRawMarkdown(form));
1446
1851
  return;
1447
1852
  }
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
- }))
1853
+ const schema = {
1854
+ id: form.schema.id,
1855
+ title: form.schema.title,
1856
+ groups: form.schema.groups.map((group) => ({
1857
+ id: group.id,
1858
+ title: group.title,
1859
+ children: group.children.map((field) => ({
1860
+ id: field.id,
1861
+ kind: field.kind,
1862
+ label: field.label,
1863
+ required: field.required,
1864
+ ...field.kind === "single_select" || field.kind === "multi_select" || field.kind === "checkboxes" ? { options: field.options.map((opt) => ({
1865
+ id: opt.id,
1866
+ label: opt.label
1867
+ })) } : {}
1465
1868
  }))
1466
- },
1467
- values: form.valuesByFieldId,
1869
+ }))
1870
+ };
1871
+ const values = {};
1872
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
1873
+ const output = {
1874
+ schema,
1875
+ values,
1876
+ notes: form.notes,
1468
1877
  markdown: serialize(form)
1469
1878
  };
1470
1879
  if (format === "json") if (options.compact) console.log(JSON.stringify(output));
@@ -1498,7 +1907,7 @@ function formatConsoleSession(transcript, useColors) {
1498
1907
  lines.push(bold("Harness Config:"));
1499
1908
  lines.push(` Max turns: ${transcript.harness.maxTurns}`);
1500
1909
  lines.push(` Max patches/turn: ${transcript.harness.maxPatchesPerTurn}`);
1501
- lines.push(` Max issues: ${transcript.harness.maxIssues}`);
1910
+ lines.push(` Max issues/turn: ${transcript.harness.maxIssuesPerTurn}`);
1502
1911
  lines.push("");
1503
1912
  lines.push(bold(`Turns (${transcript.turns.length}):`));
1504
1913
  for (const turn of transcript.turns) {
@@ -1517,7 +1926,7 @@ function formatConsoleSession(transcript, useColors) {
1517
1926
  * Register the fill command.
1518
1927
  */
1519
1928
  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) => {
1929
+ 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
1930
  const ctx = getCommandContext(cmd);
1522
1931
  const filePath = resolve(file);
1523
1932
  try {
@@ -1539,7 +1948,7 @@ function registerFillCommand(program) {
1539
1948
  fillMode = options.mode;
1540
1949
  }
1541
1950
  logVerbose(ctx, `Reading form: ${filePath}`);
1542
- const formContent = await readFile(filePath);
1951
+ const formContent = await readFile$1(filePath);
1543
1952
  logVerbose(ctx, "Parsing form...");
1544
1953
  const form = parseForm(formContent);
1545
1954
  if (options.interactive) {
@@ -1567,12 +1976,18 @@ function registerFillCommand(program) {
1567
1976
  }
1568
1977
  if (patches.length > 0) applyPatches(form, patches);
1569
1978
  const durationMs$1 = Date.now() - startTime;
1570
- const outputPath$1 = options.output ? resolve(options.output) : generateVersionedPath(filePath);
1979
+ let outputPath$1;
1980
+ if (options.output) outputPath$1 = resolve(options.output);
1981
+ else {
1982
+ const formsDir = getFormsDir(ctx.formsDir);
1983
+ await ensureFormsDir(formsDir);
1984
+ outputPath$1 = generateVersionedPathInFormsDir(filePath, formsDir);
1985
+ }
1571
1986
  if (ctx.dryRun) {
1572
1987
  logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath$1}`);
1573
1988
  showInteractiveOutro(patches.length, false);
1574
1989
  } else {
1575
- const { formPath, rawPath, yamlPath } = exportMultiFormat(form, outputPath$1);
1990
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(form, outputPath$1);
1576
1991
  showInteractiveOutro(patches.length, false);
1577
1992
  console.log("");
1578
1993
  p.log.success("Outputs:");
@@ -1583,8 +1998,8 @@ function registerFillCommand(program) {
1583
1998
  logTiming(ctx, "Fill time", durationMs$1);
1584
1999
  if (patches.length > 0) {
1585
2000
  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>`));
2001
+ console.log("Next step: fill remaining fields with agent");
2002
+ console.log(` markform fill ${formatPath(outputPath$1)} --model=<provider/model>`);
1588
2003
  }
1589
2004
  process.exit(0);
1590
2005
  }
@@ -1599,22 +2014,22 @@ function registerFillCommand(program) {
1599
2014
  process.exit(1);
1600
2015
  }
1601
2016
  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) },
2017
+ const harnessConfig = resolveHarnessConfig(form, {
2018
+ maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : void 0,
2019
+ maxPatchesPerTurn: options.maxPatches ? parseInt(options.maxPatches, 10) : void 0,
2020
+ maxIssuesPerTurn: options.maxIssues ? parseInt(options.maxIssues, 10) : void 0,
2021
+ maxFieldsPerTurn: options.maxFields ? parseInt(options.maxFields, 10) : void 0,
2022
+ maxGroupsPerTurn: options.maxGroups ? parseInt(options.maxGroups, 10) : void 0,
1608
2023
  targetRoles,
1609
2024
  fillMode
1610
- };
2025
+ });
1611
2026
  const harness = createHarness(form, harnessConfig);
1612
2027
  let agent;
1613
2028
  let mockPath;
1614
2029
  if (options.mock) {
1615
2030
  mockPath = resolve(options.mockSource);
1616
2031
  logVerbose(ctx, `Reading mock source: ${mockPath}`);
1617
- agent = createMockAgent(parseForm(await readFile(mockPath)));
2032
+ agent = createMockAgent(parseForm(await readFile$1(mockPath)));
1618
2033
  } else {
1619
2034
  const modelId = options.model;
1620
2035
  logVerbose(ctx, `Resolving model: ${modelId}`);
@@ -1626,7 +2041,7 @@ function registerFillCommand(program) {
1626
2041
  } else if (options.prompt) {
1627
2042
  const promptPath = resolve(options.prompt);
1628
2043
  logVerbose(ctx, `Reading system prompt from: ${promptPath}`);
1629
- systemPrompt = await readFile(promptPath);
2044
+ systemPrompt = await readFile$1(promptPath);
1630
2045
  }
1631
2046
  const primaryRole = targetRoles[0] === "*" ? AGENT_ROLE : targetRoles[0];
1632
2047
  const liveAgent = createLiveAgent({
@@ -1643,21 +2058,24 @@ function registerFillCommand(program) {
1643
2058
  logInfo(ctx, `Agent: ${options.mock ? "mock" : "live"}${options.model ? ` (${options.model})` : ""}`);
1644
2059
  logVerbose(ctx, `Max turns: ${harnessConfig.maxTurns}`);
1645
2060
  logVerbose(ctx, `Max patches per turn: ${harnessConfig.maxPatchesPerTurn}`);
1646
- logVerbose(ctx, `Max issues per step: ${harnessConfig.maxIssues}`);
2061
+ logVerbose(ctx, `Max issues per turn: ${harnessConfig.maxIssuesPerTurn}`);
1647
2062
  logVerbose(ctx, `Target roles: ${targetRoles.includes("*") ? "*" : targetRoles.join(", ")}`);
1648
2063
  logVerbose(ctx, `Fill mode: ${fillMode}`);
1649
2064
  let stepResult = harness.step();
1650
- logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
2065
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1651
2066
  while (!stepResult.isComplete && !harness.hasReachedMaxTurns()) {
1652
2067
  const { patches, stats } = await agent.generatePatches(stepResult.issues, harness.getForm(), harnessConfig.maxPatchesPerTurn);
1653
- logInfo(ctx, ` ${pc.yellow(String(patches.length))} patches:`);
2068
+ const tokenSuffix = stats ? ` ${pc.dim(`(tokens: ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0})`)}` : "";
2069
+ logInfo(ctx, ` → ${pc.yellow(String(patches.length))} patches${tokenSuffix}:`);
1654
2070
  for (const patch of patches) {
1655
2071
  const typeName = formatPatchType(patch);
1656
2072
  const value = formatPatchValue(patch);
1657
- logInfo(ctx, ` ${pc.cyan(patch.fieldId)} ${pc.dim(`(${typeName})`)} ${pc.dim("=")} ${pc.green(value)}`);
2073
+ const fieldId = "fieldId" in patch ? patch.fieldId : patch.op === "add_note" ? patch.ref : "";
2074
+ if (fieldId) logInfo(ctx, ` ${pc.cyan(fieldId)} ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
2075
+ else logInfo(ctx, ` ${pc.dim(`(${typeName})`)} = ${pc.green(value)}`);
1658
2076
  }
1659
2077
  if (stats) {
1660
- logVerbose(ctx, ` Stats: ${stats.inputTokens ?? 0} in / ${stats.outputTokens ?? 0} out tokens`);
2078
+ logVerbose(ctx, ` Stats: tokens ↓${stats.inputTokens ?? 0} ↑${stats.outputTokens ?? 0}`);
1661
2079
  if (stats.toolCalls.length > 0) logVerbose(ctx, ` Tools: ${stats.toolCalls.map((t) => `${t.name}(${t.count})`).join(", ")}`);
1662
2080
  if (stats.prompts) {
1663
2081
  logVerbose(ctx, ``);
@@ -1679,14 +2097,20 @@ function registerFillCommand(program) {
1679
2097
  if (stepResult.isComplete) logInfo(ctx, pc.green(` ✓ Complete`));
1680
2098
  else if (!harness.hasReachedMaxTurns()) {
1681
2099
  stepResult = harness.step();
1682
- logInfo(ctx, `Turn ${pc.bold(String(stepResult.turnNumber))}: ${pc.yellow(String(stepResult.issues.length))} issues`);
2100
+ logInfo(ctx, `${pc.bold(`Turn ${stepResult.turnNumber}:`)} ${formatTurnIssues(stepResult.issues)}`);
1683
2101
  }
1684
2102
  }
1685
2103
  const durationMs = Date.now() - startTime;
1686
2104
  if (stepResult.isComplete) logSuccess(ctx, `Form completed in ${harness.getTurnNumber()} turn(s)`);
1687
2105
  else if (harness.hasReachedMaxTurns()) logWarn(ctx, `Max turns reached (${harnessConfig.maxTurns})`);
1688
2106
  logTiming(ctx, "Fill time", durationMs);
1689
- const outputPath = options.output ? resolve(options.output) : generateVersionedPath(filePath);
2107
+ let outputPath;
2108
+ if (options.output) outputPath = resolve(options.output);
2109
+ else {
2110
+ const formsDir = getFormsDir(ctx.formsDir);
2111
+ await ensureFormsDir(formsDir);
2112
+ outputPath = generateVersionedPathInFormsDir(filePath, formsDir);
2113
+ }
1690
2114
  const formMarkdown = serialize(harness.getForm());
1691
2115
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
1692
2116
  else {
@@ -1750,6 +2174,18 @@ function formatState$1(state, useColors) {
1750
2174
  return useColors ? colorFn(text) : text;
1751
2175
  }
1752
2176
  /**
2177
+ * Format answer state badge for console output.
2178
+ */
2179
+ function formatAnswerState(state, useColors) {
2180
+ const [text, colorFn] = {
2181
+ answered: ["answered", pc.green],
2182
+ skipped: ["skipped", pc.yellow],
2183
+ aborted: ["aborted", pc.red],
2184
+ unanswered: ["unanswered", pc.dim]
2185
+ }[state] ?? [state, (s) => s];
2186
+ return useColors ? colorFn(text) : text;
2187
+ }
2188
+ /**
1753
2189
  * Format priority badge for console output.
1754
2190
  *
1755
2191
  * Priority tiers and colors:
@@ -1796,6 +2232,8 @@ function formatFieldValue(value, useColors) {
1796
2232
  if (entries.length === 0) return dim("(no entries)");
1797
2233
  return entries.map(([k, v]) => `${k}:${v}`).join(", ");
1798
2234
  }
2235
+ case "url": return value.value ? green(`"${value.value}"`) : dim("(empty)");
2236
+ case "url_list": return value.items.length > 0 ? green(`[${value.items.map((i) => `"${i}"`).join(", ")}]`) : dim("(empty)");
1799
2237
  default: return dim("(unknown)");
1800
2238
  }
1801
2239
  }
@@ -1823,13 +2261,11 @@ function formatConsoleReport$1(report, useColors) {
1823
2261
  lines.push(bold("Progress:"));
1824
2262
  lines.push(` Total fields: ${progress.counts.totalFields}`);
1825
2263
  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}`);
2264
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
2265
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
2266
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
2267
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
2268
+ lines.push(` Total notes: ${progress.counts.totalNotes}`);
1833
2269
  lines.push("");
1834
2270
  lines.push(bold("Form Content:"));
1835
2271
  for (const group of report.groups) {
@@ -1837,13 +2273,25 @@ function formatConsoleReport$1(report, useColors) {
1837
2273
  for (const field of group.children) {
1838
2274
  const reqBadge = field.required ? yellow("[required]") : dim("[optional]");
1839
2275
  const roleBadge = field.role !== "agent" ? cyan(`[${field.role}]`) : "";
2276
+ const fieldProgress = progress.fields[field.id];
2277
+ const responseStateBadge = fieldProgress ? `[${formatAnswerState(fieldProgress.answerState, useColors)}]` : "";
2278
+ const notesBadge = fieldProgress?.hasNotes ? cyan(`[${fieldProgress.noteCount} note${fieldProgress.noteCount > 1 ? "s" : ""}]`) : "";
1840
2279
  const value = report.values[field.id];
1841
2280
  const valueStr = formatFieldValue(value, useColors);
1842
- lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge}`.trim());
2281
+ lines.push(` ${field.label} ${dim(`(${field.kind})`)} ${reqBadge} ${roleBadge} ${responseStateBadge} ${notesBadge}`.trim());
1843
2282
  lines.push(` ${dim("→")} ${valueStr}`);
1844
2283
  }
1845
2284
  }
1846
2285
  lines.push("");
2286
+ if (report.notes.length > 0) {
2287
+ lines.push(bold(`Notes (${report.notes.length}):`));
2288
+ for (const note of report.notes) {
2289
+ const roleBadge = cyan(`[${note.role}]`);
2290
+ const refLabel = dim(`${note.ref}:`);
2291
+ lines.push(` ${note.id} ${roleBadge} ${refLabel} ${note.text}`.trim());
2292
+ }
2293
+ lines.push("");
2294
+ }
1847
2295
  if (report.issues.length > 0) {
1848
2296
  lines.push(bold(`Issues (${report.issues.length}):`));
1849
2297
  for (const issue of report.issues) {
@@ -1871,11 +2319,13 @@ function registerInspectCommand(program) {
1871
2319
  process.exit(1);
1872
2320
  }
1873
2321
  logVerbose(ctx, `Reading file: ${file}`);
1874
- const content = await readFile(file);
2322
+ const content = await readFile$1(file);
1875
2323
  logVerbose(ctx, "Parsing form...");
1876
2324
  const form = parseForm(content);
1877
2325
  logVerbose(ctx, "Running inspection...");
1878
2326
  const result = inspect(form, { targetRoles });
2327
+ const values = {};
2328
+ for (const [fieldId, response] of Object.entries(form.responsesByFieldId)) if (response.state === "answered" && response.value) values[fieldId] = response.value;
1879
2329
  const output = formatOutput(ctx, {
1880
2330
  title: form.schema.title,
1881
2331
  structure: result.structureSummary,
@@ -1892,7 +2342,8 @@ function registerInspectCommand(program) {
1892
2342
  role: field.role
1893
2343
  }))
1894
2344
  })),
1895
- values: form.valuesByFieldId,
2345
+ values,
2346
+ notes: form.notes,
1896
2347
  issues: result.issues.map((issue) => ({
1897
2348
  ref: issue.ref,
1898
2349
  scope: issue.scope,
@@ -1912,7 +2363,7 @@ function registerInspectCommand(program) {
1912
2363
  }
1913
2364
 
1914
2365
  //#endregion
1915
- //#region src/cli/commands/instructions.ts
2366
+ //#region src/cli/commands/readme.ts
1916
2367
  /**
1917
2368
  * Get the path to the README.md file.
1918
2369
  * Works both during development and when installed as a package.
@@ -1938,6 +2389,128 @@ function loadReadme() {
1938
2389
  * Apply basic terminal formatting to markdown content.
1939
2390
  * Colorizes headers, code blocks, and other elements for better readability.
1940
2391
  */
2392
+ function formatMarkdown$1(content, useColors) {
2393
+ if (!useColors) return content;
2394
+ const lines = content.split("\n");
2395
+ const formatted = [];
2396
+ let inCodeBlock = false;
2397
+ for (const line of lines) {
2398
+ if (line.startsWith("```")) {
2399
+ inCodeBlock = !inCodeBlock;
2400
+ formatted.push(pc.dim(line));
2401
+ continue;
2402
+ }
2403
+ if (inCodeBlock) {
2404
+ formatted.push(pc.dim(line));
2405
+ continue;
2406
+ }
2407
+ if (line.startsWith("# ")) {
2408
+ formatted.push(pc.bold(pc.cyan(line)));
2409
+ continue;
2410
+ }
2411
+ if (line.startsWith("## ")) {
2412
+ formatted.push(pc.bold(pc.blue(line)));
2413
+ continue;
2414
+ }
2415
+ if (line.startsWith("### ")) {
2416
+ formatted.push(pc.bold(line));
2417
+ continue;
2418
+ }
2419
+ let formattedLine = line.replace(/`([^`]+)`/g, (_match, code) => {
2420
+ return pc.yellow(code);
2421
+ });
2422
+ formattedLine = formattedLine.replace(/\*\*([^*]+)\*\*/g, (_match, text) => {
2423
+ return pc.bold(text);
2424
+ });
2425
+ formattedLine = formattedLine.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
2426
+ return `${pc.cyan(text)} ${pc.dim(`(${url})`)}`;
2427
+ });
2428
+ formatted.push(formattedLine);
2429
+ }
2430
+ return formatted.join("\n");
2431
+ }
2432
+ /**
2433
+ * Check if stdout is an interactive terminal.
2434
+ */
2435
+ function isInteractive$1() {
2436
+ return process.stdout.isTTY === true;
2437
+ }
2438
+ /**
2439
+ * Display content. In a future enhancement, could pipe to a pager for long output.
2440
+ */
2441
+ function displayContent$1(content) {
2442
+ console.log(content);
2443
+ }
2444
+ /**
2445
+ * Register the readme command.
2446
+ */
2447
+ function registerReadmeCommand(program) {
2448
+ program.command("readme").description("✨Display README documentation ← START HERE!").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
2449
+ const ctx = getCommandContext(cmd);
2450
+ try {
2451
+ displayContent$1(formatMarkdown$1(loadReadme(), !options.raw && isInteractive$1() && !ctx.quiet));
2452
+ } catch (error) {
2453
+ logError(error instanceof Error ? error.message : String(error));
2454
+ process.exit(1);
2455
+ }
2456
+ });
2457
+ }
2458
+
2459
+ //#endregion
2460
+ //#region src/cli/commands/report.ts
2461
+ /**
2462
+ * Register the report command.
2463
+ */
2464
+ function registerReportCommand(program) {
2465
+ 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) => {
2466
+ const ctx = getCommandContext(cmd);
2467
+ try {
2468
+ logVerbose(ctx, `Reading file: ${file}`);
2469
+ const content = await readFile$1(file);
2470
+ logVerbose(ctx, "Parsing form...");
2471
+ const form = parseForm(content);
2472
+ logVerbose(ctx, "Generating report...");
2473
+ const reportContent = serializeReportMarkdown(form);
2474
+ if (options.output) {
2475
+ let outputPath = options.output;
2476
+ if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
2477
+ await writeFile(outputPath, reportContent);
2478
+ logVerbose(ctx, `Report written to: ${outputPath}`);
2479
+ } else console.log(reportContent);
2480
+ } catch (error) {
2481
+ logError(error instanceof Error ? error.message : String(error));
2482
+ process.exit(1);
2483
+ }
2484
+ });
2485
+ }
2486
+
2487
+ //#endregion
2488
+ //#region src/cli/commands/spec.ts
2489
+ /**
2490
+ * Get the path to the SPEC.md file.
2491
+ * Works both during development and when installed as a package.
2492
+ */
2493
+ function getSpecPath() {
2494
+ const thisDir = dirname(fileURLToPath(import.meta.url));
2495
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "SPEC.md");
2496
+ return join(dirname(dirname(dirname(thisDir))), "SPEC.md");
2497
+ }
2498
+ /**
2499
+ * Load the spec content.
2500
+ */
2501
+ function loadSpec() {
2502
+ const specPath = getSpecPath();
2503
+ try {
2504
+ return readFileSync(specPath, "utf-8");
2505
+ } catch (error) {
2506
+ const message = error instanceof Error ? error.message : String(error);
2507
+ throw new Error(`Failed to load SPEC from ${specPath}: ${message}`);
2508
+ }
2509
+ }
2510
+ /**
2511
+ * Apply basic terminal formatting to markdown content.
2512
+ * Colorizes headers, code blocks, and other elements for better readability.
2513
+ */
1941
2514
  function formatMarkdown(content, useColors) {
1942
2515
  if (!useColors) return content;
1943
2516
  const lines = content.split("\n");
@@ -1991,13 +2564,13 @@ function displayContent(content) {
1991
2564
  console.log(content);
1992
2565
  }
1993
2566
  /**
1994
- * Register the instructions command.
2567
+ * Register the spec command.
1995
2568
  */
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) => {
2569
+ function registerSpecCommand(program) {
2570
+ program.command("spec").description("Display the Markform specification").option("--raw", "Output raw markdown without formatting").action((options, cmd) => {
1998
2571
  const ctx = getCommandContext(cmd);
1999
2572
  try {
2000
- displayContent(formatMarkdown(loadReadme(), !options.raw && isInteractive() && !ctx.quiet));
2573
+ displayContent(formatMarkdown(loadSpec(), !options.raw && isInteractive() && !ctx.quiet));
2001
2574
  } catch (error) {
2002
2575
  logError(error instanceof Error ? error.message : String(error));
2003
2576
  process.exit(1);
@@ -2077,15 +2650,18 @@ function openBrowser(url) {
2077
2650
  * Register the serve command.
2078
2651
  */
2079
2652
  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) => {
2653
+ 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
2654
  const ctx = getCommandContext(cmd);
2082
2655
  const port = parseInt(options.port ?? String(DEFAULT_PORT), 10);
2083
2656
  const filePath = resolve(file);
2657
+ const fileType = detectFileType(filePath);
2084
2658
  try {
2085
2659
  logVerbose(ctx, `Reading file: ${filePath}`);
2086
- let form = parseForm(await readFile(filePath));
2660
+ const content = await readFile$1(filePath);
2661
+ let form = null;
2662
+ if (fileType === "form") form = parseForm(content);
2087
2663
  const server = createServer((req, res) => {
2088
- handleRequest(req, res, form, filePath, ctx, (updatedForm) => {
2664
+ handleRequest(req, res, filePath, fileType, form, ctx, (updatedForm) => {
2089
2665
  form = updatedForm;
2090
2666
  }).catch((err) => {
2091
2667
  console.error("Request error:", err);
@@ -2095,7 +2671,8 @@ function registerServeCommand(program) {
2095
2671
  });
2096
2672
  server.listen(port, () => {
2097
2673
  const url = `http://localhost:${port}`;
2098
- logInfo(ctx, pc.green(`\n✓ Form server running at ${pc.bold(url)}\n`));
2674
+ const typeLabel = fileType === "form" ? "Form" : fileType === "unknown" ? "File" : fileType.toUpperCase();
2675
+ logInfo(ctx, pc.green(`\n✓ ${typeLabel} server running at ${pc.bold(url)}\n`));
2099
2676
  logInfo(ctx, pc.dim("Press Ctrl+C to stop\n"));
2100
2677
  if (options.open !== false) openBrowser(url);
2101
2678
  });
@@ -2112,15 +2689,33 @@ function registerServeCommand(program) {
2112
2689
  }
2113
2690
  /**
2114
2691
  * Handle HTTP requests.
2692
+ * Dispatches to appropriate renderer based on file type.
2115
2693
  */
2116
- async function handleRequest(req, res, form, filePath, ctx, updateForm) {
2694
+ async function handleRequest(req, res, filePath, fileType, form, ctx, updateForm) {
2117
2695
  const url = req.url ?? "/";
2118
- if (req.method === "GET" && url === "/") {
2696
+ if (req.method === "GET" && url === "/") if (fileType === "form" && form) {
2119
2697
  const html = renderFormHtml(form);
2120
2698
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2121
2699
  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") {
2700
+ } else if (fileType === "raw" || fileType === "report") {
2701
+ const html = renderMarkdownHtml(await readFile(filePath, "utf-8"), basename(filePath));
2702
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2703
+ res.end(html);
2704
+ } else if (fileType === "yaml") {
2705
+ const html = renderYamlHtml(await readFile(filePath, "utf-8"), basename(filePath));
2706
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2707
+ res.end(html);
2708
+ } else if (fileType === "json") {
2709
+ const html = renderJsonHtml(await readFile(filePath, "utf-8"), basename(filePath));
2710
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2711
+ res.end(html);
2712
+ } else {
2713
+ const html = renderPlainTextHtml(await readFile(filePath, "utf-8"), basename(filePath));
2714
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2715
+ res.end(html);
2716
+ }
2717
+ else if (req.method === "POST" && url === "/save" && fileType === "form" && form) await handleSave(req, res, form, filePath, ctx, updateForm);
2718
+ else if (url === "/api/form" && form) {
2124
2719
  res.writeHead(200, { "Content-Type": "application/json" });
2125
2720
  res.end(JSON.stringify({ schema: form.schema }));
2126
2721
  } else {
@@ -2153,7 +2748,8 @@ function formDataToPatches(formData, form) {
2153
2748
  if (formData[`__skip__${fieldId}`] === "1" && !field.required) {
2154
2749
  patches.push({
2155
2750
  op: "skip_field",
2156
- fieldId
2751
+ fieldId,
2752
+ role: "user"
2157
2753
  });
2158
2754
  continue;
2159
2755
  }
@@ -2334,9 +2930,9 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
2334
2930
  * @public Exported for testing.
2335
2931
  */
2336
2932
  function renderFormHtml(form) {
2337
- const { schema, valuesByFieldId, skipsByFieldId } = form;
2933
+ const { schema, responsesByFieldId } = form;
2338
2934
  const formTitle = schema.title ?? schema.id;
2339
- const groupsHtml = schema.groups.map((group) => renderGroup(group, valuesByFieldId, skipsByFieldId ?? {})).join("\n");
2935
+ const groupsHtml = schema.groups.map((group) => renderGroup(group, responsesByFieldId)).join("\n");
2340
2936
  return `<!DOCTYPE html>
2341
2937
  <html lang="en">
2342
2938
  <head>
@@ -2583,9 +3179,12 @@ function renderFormHtml(form) {
2583
3179
  /**
2584
3180
  * Render a field group as HTML.
2585
3181
  */
2586
- function renderGroup(group, values, skips) {
3182
+ function renderGroup(group, responses) {
2587
3183
  const groupTitle = group.title ?? group.id;
2588
- const fieldsHtml = group.children.map((field) => renderFieldHtml(field, values[field.id], skips[field.id])).join("\n");
3184
+ const fieldsHtml = group.children.map((field) => {
3185
+ const response = responses[field.id];
3186
+ return renderFieldHtml(field, response?.state === "answered" ? response.value : void 0, response?.state === "skipped");
3187
+ }).join("\n");
2589
3188
  return `
2590
3189
  <div class="group">
2591
3190
  <h2>${escapeHtml(groupTitle)}</h2>
@@ -2596,13 +3195,13 @@ function renderGroup(group, values, skips) {
2596
3195
  * Render a field as HTML.
2597
3196
  * @public Exported for testing.
2598
3197
  */
2599
- function renderFieldHtml(field, value, skipInfo) {
2600
- const isSkipped = skipInfo?.skipped === true;
3198
+ function renderFieldHtml(field, value, isSkipped) {
3199
+ const skipped = isSkipped === true;
2601
3200
  const requiredMark = field.required ? "<span class=\"required\">*</span>" : "";
2602
3201
  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" : "";
3202
+ const skippedBadge = skipped ? "<span class=\"skipped-badge\">Skipped</span>" : "";
3203
+ const fieldClass = skipped ? "field field-skipped" : "field";
3204
+ const disabledAttr = skipped ? " disabled" : "";
2606
3205
  let inputHtml;
2607
3206
  switch (field.kind) {
2608
3207
  case "string":
@@ -2631,7 +3230,7 @@ function renderFieldHtml(field, value, skipInfo) {
2631
3230
  break;
2632
3231
  default: inputHtml = "<div class=\"field-help\">(unknown field type)</div>";
2633
3232
  }
2634
- const skipButton = !field.required && !isSkipped ? `<div class="field-actions">
3233
+ const skipButton = !field.required && !skipped ? `<div class="field-actions">
2635
3234
  <button type="button" class="btn-skip" data-skip-field="${field.id}">Skip</button>
2636
3235
  </div>` : "";
2637
3236
  return `
@@ -2775,6 +3374,182 @@ function renderCheckboxesInput(field, value, disabledAttr) {
2775
3374
  function escapeHtml(str) {
2776
3375
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2777
3376
  }
3377
+ /** Common styles for read-only viewers */
3378
+ const READ_ONLY_STYLES = `
3379
+ * { box-sizing: border-box; }
3380
+ body {
3381
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3382
+ line-height: 1.6;
3383
+ max-width: 900px;
3384
+ margin: 0 auto;
3385
+ padding: 2rem;
3386
+ background: #f8f9fa;
3387
+ color: #212529;
3388
+ }
3389
+ h1 { color: #495057; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5rem; font-size: 1.5rem; }
3390
+ .content {
3391
+ background: white;
3392
+ border-radius: 8px;
3393
+ padding: 1.5rem;
3394
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
3395
+ }
3396
+ pre {
3397
+ background: #1e1e1e;
3398
+ color: #d4d4d4;
3399
+ padding: 1rem;
3400
+ border-radius: 6px;
3401
+ overflow-x: auto;
3402
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
3403
+ font-size: 0.9rem;
3404
+ line-height: 1.5;
3405
+ }
3406
+ .badge {
3407
+ font-size: 0.75rem;
3408
+ padding: 0.2rem 0.5rem;
3409
+ background: #e9ecef;
3410
+ border-radius: 4px;
3411
+ color: #6c757d;
3412
+ margin-left: 0.75rem;
3413
+ font-weight: normal;
3414
+ }
3415
+ `;
3416
+ /**
3417
+ * Render markdown content as read-only HTML.
3418
+ * Simple rendering without full markdown parsing.
3419
+ */
3420
+ function renderMarkdownHtml(content, filename) {
3421
+ const lines = content.split("\n");
3422
+ let html = "";
3423
+ let inParagraph = false;
3424
+ for (const line of lines) {
3425
+ const trimmed = line.trim();
3426
+ if (trimmed.startsWith("# ")) {
3427
+ if (inParagraph) {
3428
+ html += "</p>";
3429
+ inParagraph = false;
3430
+ }
3431
+ html += `<h2>${escapeHtml(trimmed.slice(2))}</h2>`;
3432
+ } else if (trimmed.startsWith("## ")) {
3433
+ if (inParagraph) {
3434
+ html += "</p>";
3435
+ inParagraph = false;
3436
+ }
3437
+ html += `<h3>${escapeHtml(trimmed.slice(3))}</h3>`;
3438
+ } else if (trimmed.startsWith("### ")) {
3439
+ if (inParagraph) {
3440
+ html += "</p>";
3441
+ inParagraph = false;
3442
+ }
3443
+ html += `<h4>${escapeHtml(trimmed.slice(4))}</h4>`;
3444
+ } else if (trimmed === "") {
3445
+ if (inParagraph) {
3446
+ html += "</p>";
3447
+ inParagraph = false;
3448
+ }
3449
+ } else {
3450
+ if (!inParagraph) {
3451
+ html += "<p>";
3452
+ inParagraph = true;
3453
+ } else html += "<br>";
3454
+ html += escapeHtml(trimmed);
3455
+ }
3456
+ }
3457
+ if (inParagraph) html += "</p>";
3458
+ return `<!DOCTYPE html>
3459
+ <html lang="en">
3460
+ <head>
3461
+ <meta charset="UTF-8">
3462
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3463
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3464
+ <style>${READ_ONLY_STYLES}
3465
+ h2 { color: #495057; font-size: 1.3rem; margin-top: 1.5rem; }
3466
+ h3 { color: #6c757d; font-size: 1.1rem; margin-top: 1.25rem; }
3467
+ h4 { color: #6c757d; font-size: 1rem; margin-top: 1rem; }
3468
+ p { margin: 0.75rem 0; }
3469
+ </style>
3470
+ </head>
3471
+ <body>
3472
+ <h1>${escapeHtml(filename)}<span class="badge">Markdown</span></h1>
3473
+ <div class="content">
3474
+ ${html}
3475
+ </div>
3476
+ </body>
3477
+ </html>`;
3478
+ }
3479
+ /**
3480
+ * Render YAML content with syntax highlighting.
3481
+ */
3482
+ function renderYamlHtml(content, filename) {
3483
+ const highlighted = content.split("\n").map((line) => {
3484
+ const colonIndex = line.indexOf(":");
3485
+ if (colonIndex > 0 && !line.trim().startsWith("#") && !line.trim().startsWith("-")) return `<span style="color:#9cdcfe">${escapeHtml(line.slice(0, colonIndex))}</span>${escapeHtml(line.slice(colonIndex))}`;
3486
+ if (line.trim().startsWith("#")) return `<span style="color:#6a9955">${escapeHtml(line)}</span>`;
3487
+ return escapeHtml(line);
3488
+ }).join("\n");
3489
+ return `<!DOCTYPE html>
3490
+ <html lang="en">
3491
+ <head>
3492
+ <meta charset="UTF-8">
3493
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3494
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3495
+ <style>${READ_ONLY_STYLES}</style>
3496
+ </head>
3497
+ <body>
3498
+ <h1>${escapeHtml(filename)}<span class="badge">YAML</span></h1>
3499
+ <div class="content">
3500
+ <pre>${highlighted}</pre>
3501
+ </div>
3502
+ </body>
3503
+ </html>`;
3504
+ }
3505
+ /**
3506
+ * Render JSON content with syntax highlighting and formatting.
3507
+ */
3508
+ function renderJsonHtml(content, filename) {
3509
+ let formatted;
3510
+ try {
3511
+ const parsed = JSON.parse(content);
3512
+ formatted = JSON.stringify(parsed, null, 2);
3513
+ } catch {
3514
+ formatted = content;
3515
+ }
3516
+ 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>");
3517
+ return `<!DOCTYPE html>
3518
+ <html lang="en">
3519
+ <head>
3520
+ <meta charset="UTF-8">
3521
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3522
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3523
+ <style>${READ_ONLY_STYLES}</style>
3524
+ </head>
3525
+ <body>
3526
+ <h1>${escapeHtml(filename)}<span class="badge">JSON</span></h1>
3527
+ <div class="content">
3528
+ <pre>${highlighted}</pre>
3529
+ </div>
3530
+ </body>
3531
+ </html>`;
3532
+ }
3533
+ /**
3534
+ * Render plain text content.
3535
+ */
3536
+ function renderPlainTextHtml(content, filename) {
3537
+ return `<!DOCTYPE html>
3538
+ <html lang="en">
3539
+ <head>
3540
+ <meta charset="UTF-8">
3541
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3542
+ <title>${escapeHtml(filename)} - Markform Viewer</title>
3543
+ <style>${READ_ONLY_STYLES}</style>
3544
+ </head>
3545
+ <body>
3546
+ <h1>${escapeHtml(filename)}<span class="badge">Text</span></h1>
3547
+ <div class="content">
3548
+ <pre>${escapeHtml(content)}</pre>
3549
+ </div>
3550
+ </body>
3551
+ </html>`;
3552
+ }
2778
3553
 
2779
3554
  //#endregion
2780
3555
  //#region src/cli/commands/render.ts
@@ -2798,7 +3573,7 @@ function registerRenderCommand(program) {
2798
3573
  const outputPath = options.output ? resolve(options.output) : getDefaultOutputPath(filePath);
2799
3574
  try {
2800
3575
  logVerbose(ctx, `Reading file: ${filePath}`);
2801
- const content = await readFile(filePath);
3576
+ const content = await readFile$1(filePath);
2802
3577
  logVerbose(ctx, "Parsing form...");
2803
3578
  const form = parseForm(content);
2804
3579
  logVerbose(ctx, "Rendering HTML...");
@@ -2816,6 +3591,156 @@ function registerRenderCommand(program) {
2816
3591
  });
2817
3592
  }
2818
3593
 
3594
+ //#endregion
3595
+ //#region src/cli/lib/initialValues.ts
3596
+ /**
3597
+ * Parse initial value inputs from CLI flags.
3598
+ *
3599
+ * Supports formats:
3600
+ * - fieldId=value (string values)
3601
+ * - fieldId:number=123 (explicit number)
3602
+ * - fieldId:list=a,b,c (comma-separated list)
3603
+ *
3604
+ * @param inputs Array of input strings in "fieldId=value" format
3605
+ * @returns Array of patches to apply as initial values
3606
+ */
3607
+ function parseInitialValues(inputs) {
3608
+ const patches = [];
3609
+ for (const input of inputs) {
3610
+ const equalsIndex = input.indexOf("=");
3611
+ if (equalsIndex === -1) throw new Error(`Invalid input format: "${input}" (expected "fieldId=value")`);
3612
+ const fieldSpec = input.slice(0, equalsIndex);
3613
+ const value = input.slice(equalsIndex + 1);
3614
+ const colonIndex = fieldSpec.indexOf(":");
3615
+ let fieldId;
3616
+ let type = null;
3617
+ if (colonIndex !== -1) {
3618
+ fieldId = fieldSpec.slice(0, colonIndex);
3619
+ type = fieldSpec.slice(colonIndex + 1).toLowerCase();
3620
+ } else fieldId = fieldSpec;
3621
+ if (type === "number") {
3622
+ const numValue = parseFloat(value);
3623
+ if (isNaN(numValue)) throw new Error(`Invalid number value for "${fieldId}": "${value}"`);
3624
+ patches.push({
3625
+ op: "set_number",
3626
+ fieldId,
3627
+ value: numValue
3628
+ });
3629
+ } else if (type === "list") {
3630
+ const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3631
+ patches.push({
3632
+ op: "set_string_list",
3633
+ fieldId,
3634
+ items
3635
+ });
3636
+ } else patches.push({
3637
+ op: "set_string",
3638
+ fieldId,
3639
+ value
3640
+ });
3641
+ }
3642
+ return patches;
3643
+ }
3644
+ /**
3645
+ * Validate that all initial values reference valid field IDs.
3646
+ *
3647
+ * @param patches Patches to validate
3648
+ * @param validFieldIds Set of valid field IDs from the form
3649
+ * @returns Array of invalid field IDs (empty if all valid)
3650
+ */
3651
+ function validateInitialValueFields(patches, validFieldIds) {
3652
+ const invalid = [];
3653
+ for (const patch of patches) if ("fieldId" in patch && patch.fieldId && !validFieldIds.has(patch.fieldId)) invalid.push(patch.fieldId);
3654
+ return invalid;
3655
+ }
3656
+
3657
+ //#endregion
3658
+ //#region src/cli/commands/research.ts
3659
+ /**
3660
+ * Register the research command.
3661
+ */
3662
+ function registerResearchCommand(program) {
3663
+ 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) => {
3664
+ const ctx = getCommandContext(cmd);
3665
+ const startTime = Date.now();
3666
+ try {
3667
+ if (!options.model) {
3668
+ logError("--model is required");
3669
+ console.log("");
3670
+ console.log(formatSuggestedLlms());
3671
+ process.exit(1);
3672
+ }
3673
+ const modelId = options.model;
3674
+ if (!hasWebSearchSupport(/^([^/]+)\//.exec(modelId)?.[1] ?? modelId)) {
3675
+ const webSearchProviders = Object.entries(WEB_SEARCH_CONFIG).filter(([, config]) => config.supported).map(([p$1]) => p$1);
3676
+ logError(`Model "${modelId}" does not support web search.`);
3677
+ console.log("");
3678
+ console.log(pc.yellow("Research forms require web search capabilities."));
3679
+ console.log(`Use a model from: ${webSearchProviders.join(", ")}`);
3680
+ console.log("");
3681
+ console.log("Examples:");
3682
+ console.log(" --model openai/gpt-5-mini");
3683
+ console.log(" --model anthropic/claude-sonnet-4-5");
3684
+ console.log(" --model google/gemini-2.5-flash");
3685
+ console.log(" --model xai/grok-4");
3686
+ process.exit(1);
3687
+ }
3688
+ const inputPath = resolve(input);
3689
+ logVerbose(ctx, `Input: ${inputPath}`);
3690
+ const form = parseForm(await readFile$1(inputPath));
3691
+ logVerbose(ctx, `Parsed form: ${form.schema.id}`);
3692
+ const initialInputs = options.input ?? [];
3693
+ if (initialInputs.length > 0) {
3694
+ const patches = parseInitialValues(initialInputs);
3695
+ const invalidFields = validateInitialValueFields(patches, new Set(form.orderIndex));
3696
+ if (invalidFields.length > 0) logWarn(ctx, `Unknown field IDs: ${invalidFields.join(", ")}`);
3697
+ applyPatches(form, patches);
3698
+ logInfo(ctx, `Applied ${patches.length} initial value(s)`);
3699
+ }
3700
+ const formsDir = getFormsDir(ctx.formsDir);
3701
+ let outputPath;
3702
+ if (options.output) outputPath = resolve(options.output);
3703
+ else outputPath = generateVersionedPathInFormsDir(inputPath, formsDir);
3704
+ logVerbose(ctx, `Output: ${outputPath}`);
3705
+ const maxTurns = parseInt(options.maxTurns, 10);
3706
+ const maxPatchesPerTurn = parseInt(options.maxPatches, 10);
3707
+ const maxIssuesPerTurn = parseInt(options.maxIssues, 10);
3708
+ logInfo(ctx, `Research fill with model: ${modelId}`);
3709
+ logVerbose(ctx, `Max turns: ${maxTurns}`);
3710
+ logVerbose(ctx, `Max patches/turn: ${maxPatchesPerTurn}`);
3711
+ logVerbose(ctx, `Max issues/turn: ${maxIssuesPerTurn}`);
3712
+ const result = await runResearch(form, {
3713
+ model: modelId,
3714
+ maxTurns,
3715
+ maxPatchesPerTurn,
3716
+ maxIssuesPerTurn,
3717
+ targetRoles: [AGENT_ROLE],
3718
+ fillMode: "continue"
3719
+ });
3720
+ if (result.availableTools) logInfo(ctx, `Tools: ${result.availableTools.join(", ")}`);
3721
+ logInfo(ctx, `Status: ${(result.status === "completed" ? pc.green : result.status === "max_turns_reached" ? pc.yellow : pc.red)(result.status)}`);
3722
+ logInfo(ctx, `Turns: ${result.totalTurns}`);
3723
+ if (result.inputTokens || result.outputTokens) logVerbose(ctx, `Tokens: ${result.inputTokens ?? 0} in, ${result.outputTokens ?? 0} out`);
3724
+ const { formPath, rawPath, yamlPath } = await exportMultiFormat(result.form, outputPath);
3725
+ logSuccess(ctx, "Outputs:");
3726
+ console.log(` ${formPath} ${pc.dim("(markform)")}`);
3727
+ console.log(` ${rawPath} ${pc.dim("(plain markdown)")}`);
3728
+ console.log(` ${yamlPath} ${pc.dim("(values as YAML)")}`);
3729
+ if (options.transcript && result.transcript) {
3730
+ const { serializeSession: serializeSession$1 } = await import("./session-DdAtY2Ni.mjs");
3731
+ const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
3732
+ const { writeFile: writeFile$1 } = await import("./shared-D7gf27Tr.mjs");
3733
+ await writeFile$1(transcriptPath, serializeSession$1(result.transcript));
3734
+ logInfo(ctx, `Transcript: ${transcriptPath}`);
3735
+ }
3736
+ logTiming(ctx, "Research fill", Date.now() - startTime);
3737
+ } catch (error) {
3738
+ logError(error instanceof Error ? error.message : String(error));
3739
+ process.exit(1);
3740
+ }
3741
+ });
3742
+ }
3743
+
2819
3744
  //#endregion
2820
3745
  //#region src/cli/commands/validate.ts
2821
3746
  /**
@@ -2875,12 +3800,10 @@ function formatConsoleReport(report, useColors) {
2875
3800
  lines.push(bold("Progress:"));
2876
3801
  lines.push(` Total fields: ${progress.counts.totalFields}`);
2877
3802
  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}`);
3803
+ lines.push(` AnswerState: answered=${progress.counts.answeredFields}, skipped=${progress.counts.skippedFields}, aborted=${progress.counts.abortedFields}, unanswered=${progress.counts.unansweredFields}`);
3804
+ lines.push(` Validity: valid=${progress.counts.validFields}, invalid=${progress.counts.invalidFields}`);
3805
+ lines.push(` Value: filled=${progress.counts.filledFields}, empty=${progress.counts.emptyFields}`);
3806
+ lines.push(` Empty required: ${progress.counts.emptyRequiredFields}`);
2884
3807
  lines.push("");
2885
3808
  if (report.issues.length > 0) {
2886
3809
  lines.push(bold(`Issues (${report.issues.length}):`));
@@ -2900,7 +3823,7 @@ function registerValidateCommand(program) {
2900
3823
  const ctx = getCommandContext(cmd);
2901
3824
  try {
2902
3825
  logVerbose(ctx, `Reading file: ${file}`);
2903
- const content = await readFile(file);
3826
+ const content = await readFile$1(file);
2904
3827
  logVerbose(ctx, "Parsing form...");
2905
3828
  const form = parseForm(content);
2906
3829
  logVerbose(ctx, "Running validation...");
@@ -2952,18 +3875,22 @@ function withColoredHelp(cmd) {
2952
3875
  */
2953
3876
  function createProgram() {
2954
3877
  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);
3878
+ 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})`);
3879
+ registerReadmeCommand(program);
3880
+ registerDocsCommand(program);
3881
+ registerSpecCommand(program);
2958
3882
  registerApplyCommand(program);
2959
- registerExportCommand(program);
2960
3883
  registerDumpCommand(program);
2961
- registerRenderCommand(program);
2962
- registerServeCommand(program);
3884
+ registerExamplesCommand(program);
3885
+ registerExportCommand(program);
2963
3886
  registerFillCommand(program);
3887
+ registerInspectCommand(program);
2964
3888
  registerModelsCommand(program);
2965
- registerExamplesCommand(program);
2966
- registerInstructionsCommand(program);
3889
+ registerRenderCommand(program);
3890
+ registerReportCommand(program);
3891
+ registerResearchCommand(program);
3892
+ registerServeCommand(program);
3893
+ registerValidateCommand(program);
2967
3894
  return program;
2968
3895
  }
2969
3896
  /**