markform 0.1.23 → 0.1.25

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 (50) hide show
  1. package/README.md +69 -29
  2. package/dist/ai-sdk.d.mts +1 -1
  3. package/dist/ai-sdk.mjs +48 -6
  4. package/dist/ai-sdk.mjs.map +1 -1
  5. package/dist/bin.mjs +1 -1
  6. package/dist/{cli-ZcOC47KK.mjs → cli-B1T8kMFt.mjs} +709 -125
  7. package/dist/cli-B1T8kMFt.mjs.map +1 -0
  8. package/dist/cli.d.mts +1 -1
  9. package/dist/cli.mjs +1 -1
  10. package/dist/{coreTypes-BlsJkU1w.d.mts → coreTypes-CxpqKpBA.d.mts} +181 -4
  11. package/dist/{coreTypes-CTLr-NGd.mjs → coreTypes-DIv9Aabl.mjs} +56 -6
  12. package/dist/coreTypes-DIv9Aabl.mjs.map +1 -0
  13. package/dist/{fillRecord-DTl5lnK0.d.mts → fillRecord-V3vlyobd.d.mts} +29 -1
  14. package/dist/{fillRecordRenderer-VBQ2vwPV.mjs → fillRecordRenderer-BqRPHPmE.mjs} +47 -15
  15. package/dist/fillRecordRenderer-BqRPHPmE.mjs.map +1 -0
  16. package/dist/index.d.mts +34 -30
  17. package/dist/index.mjs +4 -4
  18. package/dist/{apply-KzQztrDV.mjs → prompts-DaPKumGY.mjs} +1081 -17
  19. package/dist/prompts-DaPKumGY.mjs.map +1 -0
  20. package/dist/render.d.mts +2 -2
  21. package/dist/render.mjs +1 -1
  22. package/dist/{session-BCcltrLA.mjs → session-BW9jtYNV.mjs} +2 -2
  23. package/dist/{session-BCcltrLA.mjs.map → session-BW9jtYNV.mjs.map} +1 -1
  24. package/dist/{session-VeSkVrck.mjs → session-DHyTMP67.mjs} +1 -1
  25. package/dist/{shared-fb0nkzQi.mjs → shared-BLh342F5.mjs} +1 -1
  26. package/dist/{shared-CsdT2T7k.mjs → shared-BszoSkAO.mjs} +10 -10
  27. package/dist/shared-BszoSkAO.mjs.map +1 -0
  28. package/dist/{src-B2uFvGli.mjs → src-DrXmaOWl.mjs} +179 -840
  29. package/dist/src-DrXmaOWl.mjs.map +1 -0
  30. package/docs/markform-apis.md +25 -7
  31. package/docs/markform-reference.md +273 -179
  32. package/docs/markform-spec.md +92 -36
  33. package/docs/skill/SKILL.md +161 -0
  34. package/examples/markform-demo-playbook.md +342 -0
  35. package/examples/parallel/parallel-research.form.md +2 -6
  36. package/examples/rejection-test/rejection-test.session.yaml +52 -0
  37. package/examples/simple/simple-mock-filled.report.md +2 -2
  38. package/examples/simple/simple-skipped-filled.report.md +2 -2
  39. package/examples/simple/simple-with-skips.session.yaml +78 -0
  40. package/examples/simple/simple.session.yaml +78 -0
  41. package/examples/twitter-thread/twitter-thread.form.md +5 -5
  42. package/package.json +2 -2
  43. package/dist/apply-KzQztrDV.mjs.map +0 -1
  44. package/dist/cli-ZcOC47KK.mjs.map +0 -1
  45. package/dist/coreTypes-CTLr-NGd.mjs.map +0 -1
  46. package/dist/fillRecordRenderer-VBQ2vwPV.mjs.map +0 -1
  47. package/dist/shared-CsdT2T7k.mjs.map +0 -1
  48. package/dist/src-B2uFvGli.mjs.map +0 -1
  49. package/examples/startup-research/startup-research-mock-filled.form.md +0 -297
  50. package/examples/startup-research/startup-research.form.md +0 -181
@@ -1,19 +1,19 @@
1
1
 
2
- import { R as PatchSchema } from "./coreTypes-CTLr-NGd.mjs";
3
- import { $ as WEB_SEARCH_CONFIG, A as DEFAULT_FORMS_DIR, F as DEFAULT_MAX_TURNS, G as deriveExportPath, H as MAX_FORMS_IN_MENU, I as DEFAULT_PORT, J as deriveSchemaPath, K as deriveFillRecordPath, N as DEFAULT_MAX_PATCHES_PER_TURN, Q as SUGGESTED_LLMS, R as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, U as REPORT_EXTENSION, W as USER_ROLE, X as parseRolesFlag, Y as detectFileType, c as computeProgressSummary, d as serializeForm, et as formatSuggestedLlms, f as serializeRawMarkdown, g as validateSyntaxConsistency, i as inspect, j as DEFAULT_MAX_ISSUES_PER_TURN, k as AGENT_ROLE, l as computeStructureSummary, n as getAllFields, nt as hasWebSearchSupport, p as serializeReport, q as deriveReportPath, rt as parseModelIdForDisplay, t as applyPatches, z as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN } from "./apply-KzQztrDV.mjs";
4
- import { A as createMockAgent, C as getProviderNames, D as FillRecordCollector, E as createLiveAgent, M as createHarness, O as computeExecutionPlan, S as getProviderInfo, T as buildMockWireFormat, U as formToJsonSchema, W as parseForm, _ as fillForm, g as resolveHarnessConfig, h as formatFillRecordSummary, i as runResearch, m as stripUnstableFillRecordFields, n as isResearchForm, t as VERSION, w as resolveModel } from "./src-B2uFvGli.mjs";
5
- import { n as serializeSession } from "./session-BCcltrLA.mjs";
6
- import { _ as writeFile, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-CsdT2T7k.mjs";
7
- import { a as renderJsonContent, c as renderViewContent, i as highlightYamlValue, l as renderYamlContent, o as renderMarkdownContent, r as renderFillRecordContent, s as renderSourceContent, u as escapeHtml } from "./fillRecordRenderer-VBQ2vwPV.mjs";
2
+ import { R as PatchSchema } from "./coreTypes-DIv9Aabl.mjs";
3
+ import { $ as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, E as serializeReport, H as AGENT_ROLE, K as DEFAULT_MAX_PATCHES_PER_TURN, Q as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, S as computeStructureSummary, T as serializeRawMarkdown, U as DEFAULT_FORMS_DIR, W as DEFAULT_MAX_ISSUES_PER_TURN, X as DEFAULT_PORT, Y as DEFAULT_MAX_TURNS, _ as inspect, _t as parseModelIdForDisplay, at as deriveExportPath, c as filterIssuesByOrder, ct as deriveSchemaPath, d as coerceInputContext, f as coerceToFieldPatch, ft as SUGGESTED_LLMS, gt as hasWebSearchSupport, h as getAllFields, it as USER_ROLE, k as validateSyntaxConsistency, l as filterIssuesByScope, lt as detectFileType, m as applyPatches, mt as formatSuggestedLlms, nt as MAX_FORMS_IN_MENU, ot as deriveFillRecordPath, p as findFieldById, pt as WEB_SEARCH_CONFIG, rt as REPORT_EXTENSION, st as deriveReportPath, u as getFieldIdFromRef, ut as parseRolesFlag, w as serializeForm, x as computeProgressSummary } from "./prompts-DaPKumGY.mjs";
4
+ import { C as getProviderInfo, D as createLiveAgent, E as buildMockWireFormat, H as parseForm, N as createHarness, O as FillRecordCollector, T as resolveModel, V as formToJsonSchema, _ as resolveHarnessConfig, g as formatFillRecordSummary, h as stripUnstableFillRecordFields, i as runResearch, j as createMockAgent, k as computeExecutionPlan, m as isEmptyFillRecord, n as isResearchForm, t as VERSION, v as fillForm, w as getProviderNames } from "./src-DrXmaOWl.mjs";
5
+ import { n as serializeSession } from "./session-BW9jtYNV.mjs";
6
+ import { _ as writeFile$1, a as formatPath, c as logError, d as logTiming, f as logVerbose, g as stripHtmlComments, h as shouldUseColors, i as formatOutput, l as logInfo, m as readFile$1, n as createSpinner, o as getCommandContext, p as logWarn, r as ensureFormsDir, s as logDryRun, t as OUTPUT_FORMATS, u as logSuccess } from "./shared-BszoSkAO.mjs";
7
+ import { a as renderJsonContent, c as renderViewContent, i as highlightYamlValue, l as renderYamlContent, o as renderMarkdownContent, r as renderFillRecordContent, s as renderSourceContent, u as escapeHtml } from "./fillRecordRenderer-BqRPHPmE.mjs";
8
8
  import Markdoc from "@markdoc/markdoc";
9
9
  import YAML from "yaml";
10
10
  import { Command } from "commander";
11
11
  import pc from "picocolors";
12
12
  import { exec, execSync, spawn } from "node:child_process";
13
13
  import { basename, dirname, join, resolve } from "node:path";
14
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
15
14
  import { fileURLToPath } from "node:url";
16
- import { readFile } from "node:fs/promises";
15
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
16
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
17
17
  import * as p from "@clack/prompts";
18
18
  import { createServer } from "node:http";
19
19
 
@@ -72,6 +72,21 @@ const CLI_VERSION = VERSION === "development" ? getGitVersion() : VERSION;
72
72
  * library to remain Node.js-free.
73
73
  */
74
74
  /**
75
+ * Resolve a path relative to the package root, handling both dev and dist modes.
76
+ *
77
+ * In dist mode, commands run from `<pkg>/dist/` (1 level below root).
78
+ * In dev mode, commands run from `<pkg>/src/cli/commands/` or `<pkg>/src/cli/examples/`
79
+ * (3 levels below root).
80
+ *
81
+ * @param callerMetaUrl - The `import.meta.url` of the calling module
82
+ * @param relativePath - Path relative to the package root (e.g. 'docs/markform-reference.md')
83
+ */
84
+ function resolvePackagePath(callerMetaUrl, relativePath) {
85
+ const thisDir = dirname(fileURLToPath(callerMetaUrl));
86
+ if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), relativePath);
87
+ return join(dirname(dirname(dirname(thisDir))), relativePath);
88
+ }
89
+ /**
75
90
  * Resolve the forms directory path to an absolute path.
76
91
  * Uses the provided override or falls back to DEFAULT_FORMS_DIR.
77
92
  *
@@ -86,19 +101,10 @@ function getFormsDir(override, cwd = process.cwd()) {
86
101
  //#endregion
87
102
  //#region src/cli/commands/apis.ts
88
103
  /**
89
- * Get the path to the markform-apis.md file.
90
- * Works both during development and when installed as a package.
91
- */
92
- function getApisPath() {
93
- const thisDir = dirname(fileURLToPath(import.meta.url));
94
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-apis.md");
95
- return join(dirname(dirname(dirname(thisDir))), "docs", "markform-apis.md");
96
- }
97
- /**
98
104
  * Load the APIs documentation content.
99
105
  */
100
106
  function loadApis() {
101
- const apisPath = getApisPath();
107
+ const apisPath = resolvePackagePath(import.meta.url, "docs/markform-apis.md");
102
108
  try {
103
109
  return readFileSync(apisPath, "utf-8");
104
110
  } catch (error) {
@@ -178,7 +184,7 @@ function registerApisCommand(program) {
178
184
  }
179
185
 
180
186
  //#endregion
181
- //#region src/cli/commands/apply.ts
187
+ //#region src/cli/commands/patch.ts
182
188
  /**
183
189
  * Format state badge for console output.
184
190
  */
@@ -192,16 +198,16 @@ function formatState$2(state, useColors) {
192
198
  return useColors ? colorFn(text) : text;
193
199
  }
194
200
  /**
195
- * Format apply report for console output.
201
+ * Format patch report for console output.
196
202
  */
197
- function formatConsoleReport$3(report, useColors) {
203
+ function formatConsoleReport$5(report, useColors) {
198
204
  const lines = [];
199
205
  const bold = useColors ? pc.bold : (s) => s;
200
206
  const dim = useColors ? pc.dim : (s) => s;
201
207
  const cyan = useColors ? pc.cyan : (s) => s;
202
208
  const green = useColors ? pc.green : (s) => s;
203
209
  const red = useColors ? pc.red : (s) => s;
204
- lines.push(bold(cyan("Apply Result")));
210
+ lines.push(bold(cyan("Patch Result")));
205
211
  lines.push("");
206
212
  const statusColor = report.apply_status === "applied" ? green : red;
207
213
  lines.push(`${bold("Status:")} ${statusColor(report.apply_status)}`);
@@ -225,16 +231,12 @@ function formatConsoleReport$3(report, useColors) {
225
231
  return lines.join("\n");
226
232
  }
227
233
  /**
228
- * Register the apply command.
234
+ * Register the patch command.
229
235
  */
230
- function registerApplyCommand(program) {
231
- program.command("apply <file>").description("Apply patches to a form").option("--patch <json>", "JSON array of patches to apply").option("-o, --output <file>", "Output file (defaults to stdout)").option("--report", "Output apply result report instead of modified form").option("--normalize", "Regenerate form without preserving external content").action(async (file, options, cmd) => {
236
+ function registerPatchCommand(program) {
237
+ program.command("patch <file> <json>").description("Apply raw typed patches to a form").option("-o, --output <file>", "Output file (defaults to stdout)").option("--report", "Output patch result report instead of modified form").option("--normalize", "Regenerate form without preserving external content").action(async (file, json, options, cmd) => {
232
238
  const ctx = getCommandContext(cmd);
233
239
  try {
234
- if (!options.patch) {
235
- logError("--patch option is required");
236
- process.exit(1);
237
- }
238
240
  logVerbose(ctx, `Reading file: ${file}`);
239
241
  const content = await readFile$1(file);
240
242
  logVerbose(ctx, "Parsing form...");
@@ -242,13 +244,13 @@ function registerApplyCommand(program) {
242
244
  logVerbose(ctx, "Parsing patches...");
243
245
  let parsedJson;
244
246
  try {
245
- parsedJson = JSON.parse(options.patch);
247
+ parsedJson = JSON.parse(json);
246
248
  } catch {
247
- logError("Invalid JSON in --patch option");
249
+ logError("Invalid JSON in patch argument");
248
250
  process.exit(1);
249
251
  }
250
252
  if (!Array.isArray(parsedJson)) {
251
- logError("--patch must be a JSON array");
253
+ logError("Patch argument must be a JSON array");
252
254
  process.exit(1);
253
255
  }
254
256
  const patches = parsedJson;
@@ -276,7 +278,7 @@ function registerApplyCommand(program) {
276
278
  structure: result.structureSummary,
277
279
  progress: result.progressSummary,
278
280
  issues: result.issues
279
- }, (data, useColors) => formatConsoleReport$3(data, useColors));
281
+ }, (data, useColors) => formatConsoleReport$5(data, useColors));
280
282
  console.error(output);
281
283
  process.exit(1);
282
284
  }
@@ -288,15 +290,15 @@ function registerApplyCommand(program) {
288
290
  structure: result.structureSummary,
289
291
  progress: result.progressSummary,
290
292
  issues: result.issues
291
- }, (data, useColors) => formatConsoleReport$3(data, useColors));
293
+ }, (data, useColors) => formatConsoleReport$5(data, useColors));
292
294
  if (options.output) {
293
- await writeFile(options.output, output);
295
+ await writeFile$1(options.output, output);
294
296
  logSuccess(ctx, `Report written to ${options.output}`);
295
297
  } else console.log(output);
296
298
  } else {
297
299
  const output = serializeForm(form, { preserveContent: !options.normalize });
298
300
  if (options.output) {
299
- await writeFile(options.output, output);
301
+ await writeFile$1(options.output, output);
300
302
  logSuccess(ctx, `Modified form written to ${options.output}`);
301
303
  } else console.log(output);
302
304
  }
@@ -670,19 +672,10 @@ function registerBrowseCommand(program) {
670
672
  //#endregion
671
673
  //#region src/cli/commands/docs.ts
672
674
  /**
673
- * Get the path to the markform-reference.md file.
674
- * Works both during development and when installed as a package.
675
- */
676
- function getDocsPath() {
677
- const thisDir = dirname(fileURLToPath(import.meta.url));
678
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-reference.md");
679
- return join(dirname(dirname(dirname(thisDir))), "docs", "markform-reference.md");
680
- }
681
- /**
682
675
  * Load the docs content.
683
676
  */
684
677
  function loadDocs() {
685
- const docsPath = getDocsPath();
678
+ const docsPath = resolvePackagePath(import.meta.url, "docs/markform-reference.md");
686
679
  try {
687
680
  return readFileSync(docsPath, "utf-8");
688
681
  } catch (error) {
@@ -909,7 +902,7 @@ function deriveExportPaths(basePath) {
909
902
  async function exportMultiFormat(form, basePath) {
910
903
  const paths = deriveExportPaths(basePath);
911
904
  const reportContent = serializeReport(form);
912
- await writeFile(paths.reportPath, reportContent);
905
+ await writeFile$1(paths.reportPath, reportContent);
913
906
  const values = toStructuredValues(form);
914
907
  const notes = toNotesArray(form);
915
908
  const exportData = {
@@ -917,12 +910,12 @@ async function exportMultiFormat(form, basePath) {
917
910
  ...notes.length > 0 && { notes }
918
911
  };
919
912
  const yamlContent = YAML.stringify(exportData);
920
- await writeFile(paths.yamlPath, yamlContent);
913
+ await writeFile$1(paths.yamlPath, yamlContent);
921
914
  const formContent = serializeForm(form);
922
- await writeFile(paths.formPath, formContent);
915
+ await writeFile$1(paths.formPath, formContent);
923
916
  const schemaResult = formToJsonSchema(form);
924
917
  const schemaContent = JSON.stringify(schemaResult.schema, null, 2) + "\n";
925
- await writeFile(paths.schemaPath, schemaContent);
918
+ await writeFile$1(paths.schemaPath, schemaContent);
926
919
  return paths;
927
920
  }
928
921
 
@@ -1033,6 +1026,12 @@ const EXAMPLE_DEFINITIONS = [
1033
1026
  path: "simple/simple.form.md",
1034
1027
  type: "fill"
1035
1028
  },
1029
+ {
1030
+ id: "twitter-thread",
1031
+ filename: "twitter-thread.form.md",
1032
+ path: "twitter-thread/twitter-thread.form.md",
1033
+ type: "fill"
1034
+ },
1036
1035
  {
1037
1036
  id: "movie-deep-research",
1038
1037
  filename: "movie-deep-research.form.md",
@@ -1061,9 +1060,7 @@ function getExampleOrder(filename) {
1061
1060
  * Works both during development and when installed as a package.
1062
1061
  */
1063
1062
  function getExamplesDir() {
1064
- const thisDir = dirname(fileURLToPath(import.meta.url));
1065
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "examples");
1066
- return join(dirname(dirname(dirname(thisDir))), "examples");
1063
+ return resolvePackagePath(import.meta.url, "examples");
1067
1064
  }
1068
1065
  /**
1069
1066
  * Load the content of an example form.
@@ -1967,6 +1964,12 @@ function formatPatchValue(patch) {
1967
1964
  const rowCount = patch.value?.length ?? 0;
1968
1965
  return rowCount > 0 ? truncate(`[${rowCount} rows]`) : "(empty)";
1969
1966
  }
1967
+ case "append_table": return truncate(`+[${patch.value?.length ?? 0} rows]`);
1968
+ case "delete_table": return `(delete row ${patch.value})`;
1969
+ case "append_string_list": return truncate(`+[${patch.value.join(", ")}]`);
1970
+ case "delete_string_list": return `(delete item ${patch.value})`;
1971
+ case "append_url_list": return truncate(`+[${patch.value.join(", ")}]`);
1972
+ case "delete_url_list": return `(delete item ${patch.value})`;
1970
1973
  case "add_note": return truncate(`note: ${patch.text}`);
1971
1974
  case "remove_note": return `(remove note ${patch.noteId})`;
1972
1975
  }
@@ -1990,6 +1993,12 @@ function formatPatchType(patch) {
1990
1993
  case "set_date": return "date";
1991
1994
  case "set_year": return "year";
1992
1995
  case "set_table": return "table";
1996
+ case "append_table": return "append_table";
1997
+ case "delete_table": return "delete_table";
1998
+ case "append_string_list": return "append_string_list";
1999
+ case "delete_string_list": return "delete_string_list";
2000
+ case "append_url_list": return "append_url_list";
2001
+ case "delete_url_list": return "delete_url_list";
1993
2002
  case "add_note": return "note";
1994
2003
  case "remove_note": return "remove_note";
1995
2004
  }
@@ -2433,18 +2442,38 @@ function registerRunCommand(program) {
2433
2442
  */
2434
2443
  /**
2435
2444
  * Print non-interactive list of examples.
2445
+ * Supports --format=json and --format=yaml via formatOutput().
2436
2446
  */
2437
- function printExamplesList() {
2438
- console.log(pc.bold("Available examples:\n"));
2447
+ function printExamplesList(ctx) {
2439
2448
  const examples = getAllExamplesWithMetadata();
2440
- for (const example of examples) {
2441
- const typeLabel = example.type === "research" ? pc.magenta("[research]") : pc.blue("[fill]");
2442
- console.log(` ${pc.cyan(example.id)} ${typeLabel}`);
2443
- console.log(` ${pc.bold(example.title ?? example.id)}`);
2444
- console.log(` ${example.description ?? "No description"}`);
2445
- console.log(` Source: ${formatPath(getExamplePath(example.id))}`);
2446
- console.log("");
2447
- }
2449
+ const output = formatOutput(ctx, examples.map((example) => ({
2450
+ id: example.id,
2451
+ filename: example.filename,
2452
+ type: example.type,
2453
+ title: example.title ?? example.id,
2454
+ description: example.description ?? ""
2455
+ })), (_data, useColors) => {
2456
+ const c = useColors ? pc : {
2457
+ bold: (s) => s,
2458
+ cyan: (s) => s,
2459
+ magenta: (s) => s,
2460
+ blue: (s) => s,
2461
+ dim: (s) => s
2462
+ };
2463
+ const lines = [c.bold("Available examples:"), ""];
2464
+ for (const example of examples) {
2465
+ const typeLabel = example.type === "research" ? c.magenta("[research]") : c.blue("[fill]");
2466
+ lines.push(` ${c.cyan(example.id)} ${typeLabel}`);
2467
+ lines.push(` ${c.bold(example.title ?? example.id)}`);
2468
+ lines.push(` ${example.description ?? "No description"}`);
2469
+ lines.push(` Source: ${formatPath(getExamplePath(example.id))}`);
2470
+ lines.push("");
2471
+ }
2472
+ lines.push(c.dim("Tip: For a comprehensive end-to-end walkthrough, ask your coding agent"));
2473
+ lines.push(c.dim("to run the Markform QA playbook (tests/qa/markform-full-walkthrough.qa.md)."));
2474
+ return lines.join("\n");
2475
+ });
2476
+ console.log(output);
2448
2477
  }
2449
2478
  /**
2450
2479
  * Copy an example form to the forms directory.
@@ -2462,7 +2491,7 @@ async function copyExample(exampleId, formsDir, overwrite, _quiet) {
2462
2491
  path: outputPath
2463
2492
  };
2464
2493
  }
2465
- await writeFile(outputPath, loadExampleContent(exampleId));
2494
+ await writeFile$1(outputPath, loadExampleContent(exampleId));
2466
2495
  return {
2467
2496
  copied: true,
2468
2497
  skipped: false,
@@ -2585,7 +2614,7 @@ function registerExamplesCommand(program) {
2585
2614
  const ctx = getCommandContext(cmd);
2586
2615
  try {
2587
2616
  if (options.list) {
2588
- printExamplesList();
2617
+ printExamplesList(ctx);
2589
2618
  return;
2590
2619
  }
2591
2620
  const formsDir = getFormsDir(ctx.formsDir);
@@ -2920,7 +2949,7 @@ function registerFillCommand(program) {
2920
2949
  logTiming(ctx, "Fill time", durationMs);
2921
2950
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
2922
2951
  else {
2923
- await writeFile(outputPath, result.markdown);
2952
+ await writeFile$1(outputPath, result.markdown);
2924
2953
  logSuccess(ctx, `Form written to: ${outputPath}`);
2925
2954
  }
2926
2955
  if (result.record && !ctx.quiet) {
@@ -2928,7 +2957,8 @@ function registerFillCommand(program) {
2928
2957
  const summary = formatFillRecordSummary(result.record, { verbose: ctx.verbose });
2929
2958
  console.error(summary);
2930
2959
  }
2931
- if ((options.recordFill || options.recordFillStable) && result.record) {
2960
+ if ((options.recordFill || options.recordFillStable) && result.record) if (isEmptyFillRecord(result.record)) logVerbose(ctx, "Skipping fill record: no turns were executed");
2961
+ else {
2932
2962
  const sidecarPath = deriveFillRecordPath(outputPath);
2933
2963
  const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(result.record) : result.record;
2934
2964
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write fill record to: ${sidecarPath}`);
@@ -3142,6 +3172,7 @@ function registerFillCommand(program) {
3142
3172
  requiredIssuesRemaining: stepResult.issues.filter((i) => i.severity === "required").length,
3143
3173
  isComplete: stepResult.isComplete,
3144
3174
  rejectedPatches,
3175
+ coercionWarnings: stepResult.coercionWarnings,
3145
3176
  issues: stepResult.issues,
3146
3177
  patches
3147
3178
  });
@@ -3172,7 +3203,7 @@ function registerFillCommand(program) {
3172
3203
  const formMarkdown = serializeForm(harness.getForm(), { preserveContent: !options.normalize });
3173
3204
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write form to: ${outputPath}`);
3174
3205
  else {
3175
- await writeFile(outputPath, formMarkdown);
3206
+ await writeFile$1(outputPath, formMarkdown);
3176
3207
  logSuccess(ctx, `Form written to: ${outputPath}`);
3177
3208
  }
3178
3209
  const finalInspect = inspect(harness.getForm(), { targetRoles });
@@ -3184,7 +3215,8 @@ function registerFillCommand(program) {
3184
3215
  const summary = formatFillRecordSummary(fillRecord, { verbose: ctx.verbose });
3185
3216
  console.error(summary);
3186
3217
  }
3187
- if (options.recordFill || options.recordFillStable) {
3218
+ if (options.recordFill || options.recordFillStable) if (isEmptyFillRecord(fillRecord)) logVerbose(ctx, "Skipping fill record: no turns were executed");
3219
+ else {
3188
3220
  const sidecarPath = deriveFillRecordPath(outputPath);
3189
3221
  const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(fillRecord) : fillRecord;
3190
3222
  if (ctx.dryRun) logInfo(ctx, `[DRY RUN] Would write fill record to: ${sidecarPath}`);
@@ -3201,7 +3233,7 @@ function registerFillCommand(program) {
3201
3233
  logInfo(ctx, `[DRY RUN] Would write session to: ${recordPath}`);
3202
3234
  console.log(yaml);
3203
3235
  } else {
3204
- await writeFile(recordPath, yaml);
3236
+ await writeFile$1(recordPath, yaml);
3205
3237
  logSuccess(ctx, `Session recorded to: ${recordPath}`);
3206
3238
  }
3207
3239
  } else if (!ctx.quiet) {
@@ -3218,10 +3250,12 @@ function registerFillCommand(program) {
3218
3250
  const progressSummary = computeProgressSummary(form.schema, currentForm.responsesByFieldId, currentForm.notes, finalInspect.issues);
3219
3251
  collector.setStatus("failed", message);
3220
3252
  const fillRecord = collector.getRecord(progressSummary.counts);
3221
- const sidecarPath = deriveFillRecordPath(resolve(options.output));
3222
- const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(fillRecord) : fillRecord;
3223
- writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
3224
- logWarn(ctx, `Partial fill record written to: ${sidecarPath}`);
3253
+ if (!isEmptyFillRecord(fillRecord)) {
3254
+ const sidecarPath = deriveFillRecordPath(resolve(options.output));
3255
+ const recordToWrite = options.recordFillStable ? stripUnstableFillRecordFields(fillRecord) : fillRecord;
3256
+ writeFileSync(sidecarPath, JSON.stringify(recordToWrite, null, 2));
3257
+ logWarn(ctx, `Partial fill record written to: ${sidecarPath}`);
3258
+ }
3225
3259
  } catch {}
3226
3260
  process.exit(1);
3227
3261
  }
@@ -3336,7 +3370,7 @@ function formatFieldValue(value, useColors) {
3336
3370
  /**
3337
3371
  * Format inspect report for console output.
3338
3372
  */
3339
- function formatConsoleReport$2(report, useColors) {
3373
+ function formatConsoleReport$4(report, useColors) {
3340
3374
  const lines = [];
3341
3375
  const bold = useColors ? pc.bold : (s) => s;
3342
3376
  const dim = useColors ? pc.dim : (s) => s;
@@ -3449,7 +3483,7 @@ function registerInspectCommand(program) {
3449
3483
  severity: issue.severity,
3450
3484
  blockedBy: issue.blockedBy
3451
3485
  }))
3452
- }, (data, useColors) => formatConsoleReport$2(data, useColors));
3486
+ }, (data, useColors) => formatConsoleReport$4(data, useColors));
3453
3487
  console.log(output);
3454
3488
  } catch (error) {
3455
3489
  logError(error instanceof Error ? error.message : String(error));
@@ -3461,19 +3495,10 @@ function registerInspectCommand(program) {
3461
3495
  //#endregion
3462
3496
  //#region src/cli/commands/readme.ts
3463
3497
  /**
3464
- * Get the path to the README.md file.
3465
- * Works both during development and when installed as a package.
3466
- */
3467
- function getReadmePath() {
3468
- const thisDir = dirname(fileURLToPath(import.meta.url));
3469
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "README.md");
3470
- return join(dirname(dirname(dirname(thisDir))), "README.md");
3471
- }
3472
- /**
3473
3498
  * Load the README content.
3474
3499
  */
3475
3500
  function loadReadme() {
3476
- const readmePath = getReadmePath();
3501
+ const readmePath = resolvePackagePath(import.meta.url, "README.md");
3477
3502
  try {
3478
3503
  return readFileSync(readmePath, "utf-8");
3479
3504
  } catch (error) {
@@ -3570,7 +3595,7 @@ function registerReportCommand(program) {
3570
3595
  if (options.output) {
3571
3596
  let outputPath = options.output;
3572
3597
  if (!outputPath.endsWith(REPORT_EXTENSION) && !outputPath.endsWith(".md")) outputPath = outputPath + REPORT_EXTENSION;
3573
- await writeFile(outputPath, reportContent);
3598
+ await writeFile$1(outputPath, reportContent);
3574
3599
  logVerbose(ctx, `Report written to: ${outputPath}`);
3575
3600
  } else console.log(reportContent);
3576
3601
  } catch (error) {
@@ -3583,19 +3608,10 @@ function registerReportCommand(program) {
3583
3608
  //#endregion
3584
3609
  //#region src/cli/commands/spec.ts
3585
3610
  /**
3586
- * Get the path to the markform-spec.md file.
3587
- * Works both during development and when installed as a package.
3588
- */
3589
- function getSpecPath() {
3590
- const thisDir = dirname(fileURLToPath(import.meta.url));
3591
- if (thisDir.split(/[/\\]/).pop() === "dist") return join(dirname(thisDir), "docs", "markform-spec.md");
3592
- return join(dirname(dirname(dirname(thisDir))), "docs", "markform-spec.md");
3593
- }
3594
- /**
3595
3611
  * Load the spec content.
3596
3612
  */
3597
3613
  function loadSpec() {
3598
- const specPath = getSpecPath();
3614
+ const specPath = resolvePackagePath(import.meta.url, "docs/markform-spec.md");
3599
3615
  try {
3600
3616
  return readFileSync(specPath, "utf-8");
3601
3617
  } catch (error) {
@@ -3727,6 +3743,201 @@ function registerModelsCommand(program) {
3727
3743
  });
3728
3744
  }
3729
3745
 
3746
+ //#endregion
3747
+ //#region src/cli/commands/next.ts
3748
+ /**
3749
+ * Build field metadata for an issue's field.
3750
+ */
3751
+ function buildFieldMeta(field) {
3752
+ const meta = {
3753
+ kind: field.kind,
3754
+ label: field.label,
3755
+ required: field.required
3756
+ };
3757
+ if (field.kind === "single_select" || field.kind === "multi_select") meta.options = field.options.map((o) => o.id);
3758
+ else if (field.kind === "checkboxes") {
3759
+ meta.options = field.options.map((o) => o.id);
3760
+ meta.checkbox_mode = field.checkboxMode;
3761
+ } else if (field.kind === "table") {
3762
+ meta.columns = field.columns.map((c) => ({
3763
+ id: c.id,
3764
+ type: c.type,
3765
+ required: c.required
3766
+ }));
3767
+ if (field.minRows !== void 0) meta.min_rows = field.minRows;
3768
+ if (field.maxRows !== void 0) meta.max_rows = field.maxRows;
3769
+ }
3770
+ return meta;
3771
+ }
3772
+ /**
3773
+ * Generate a concrete `markform set` example for a field.
3774
+ */
3775
+ function generateSetExample(file, field) {
3776
+ const base = `markform set ${file} ${field.id}`;
3777
+ switch (field.kind) {
3778
+ case "string": return `${base} "example text"`;
3779
+ case "number": return `${base} 42`;
3780
+ case "string_list": return `${base} '["item1", "item2"]'`;
3781
+ case "single_select": return field.options.length > 0 ? `${base} ${field.options[0].id}` : `${base} option_id`;
3782
+ case "multi_select": return field.options.length > 0 ? `${base} '["${field.options.map((o) => o.id).join("\", \"")}"]'` : `${base} '["option1"]'`;
3783
+ case "checkboxes":
3784
+ if (field.checkboxMode === "simple") return `${base} '${JSON.stringify(Object.fromEntries(field.options.map((o) => [o.id, true])))}'`;
3785
+ return `${base} '${JSON.stringify(Object.fromEntries(field.options.map((o) => [o.id, "done"])))}'`;
3786
+ case "url": return `${base} "https://example.com"`;
3787
+ case "url_list": return `${base} '["https://example.com"]'`;
3788
+ case "date": return `${base} "2024-01-15"`;
3789
+ case "year": return `${base} 2024`;
3790
+ case "table":
3791
+ if (field.columns.length > 0) {
3792
+ const example = Object.fromEntries(field.columns.map((c) => [c.id, `example_${c.type}`]));
3793
+ return `${base} --append '${JSON.stringify(example)}'`;
3794
+ }
3795
+ return `${base} --append '{}'`;
3796
+ }
3797
+ }
3798
+ /**
3799
+ * Generate a skip example for optional fields, or null for required fields.
3800
+ */
3801
+ function generateSkipExample(file, fieldId, required) {
3802
+ if (required) return null;
3803
+ return `markform set ${file} ${fieldId} --skip --reason "Not applicable"`;
3804
+ }
3805
+ /**
3806
+ * Convert a FieldValue to a serializable current_value for the report.
3807
+ */
3808
+ function serializeCurrentValue(value) {
3809
+ switch (value.kind) {
3810
+ case "string":
3811
+ case "url":
3812
+ case "date": return value.value;
3813
+ case "number":
3814
+ case "year": return value.value;
3815
+ case "string_list":
3816
+ case "url_list": return value.items;
3817
+ case "single_select": return value.selected;
3818
+ case "multi_select": return value.selected;
3819
+ case "checkboxes": return value.values;
3820
+ case "table": return value.rows;
3821
+ }
3822
+ }
3823
+ /**
3824
+ * Enrich an InspectIssue into a NextIssue with field metadata and examples.
3825
+ */
3826
+ function enrichIssue(issue, form, file) {
3827
+ const fieldId = getFieldIdFromRef(issue.ref, issue.scope);
3828
+ const field = fieldId ? findFieldById(form, fieldId) : void 0;
3829
+ const enriched = {
3830
+ ref: issue.ref,
3831
+ scope: issue.scope,
3832
+ reason: issue.reason,
3833
+ message: issue.message,
3834
+ severity: issue.severity,
3835
+ priority: issue.priority,
3836
+ set_example: field ? generateSetExample(file, field) : `markform set ${file} ${issue.ref} "value"`,
3837
+ skip_example: field ? generateSkipExample(file, field.id, field.required) : null
3838
+ };
3839
+ if (field) {
3840
+ enriched.field = buildFieldMeta(field);
3841
+ const response = form.responsesByFieldId[field.id];
3842
+ if (response?.state === "answered" && response.value) enriched.current_value = serializeCurrentValue(response.value);
3843
+ }
3844
+ return enriched;
3845
+ }
3846
+ /**
3847
+ * Format next report for console output.
3848
+ */
3849
+ function formatConsoleReport$3(report, useColors) {
3850
+ const lines = [];
3851
+ const bold = useColors ? pc.bold : (s) => s;
3852
+ const dim = useColors ? pc.dim : (s) => s;
3853
+ const cyan = useColors ? pc.cyan : (s) => s;
3854
+ const green = useColors ? pc.green : (s) => s;
3855
+ const yellow = useColors ? pc.yellow : (s) => s;
3856
+ const red = useColors ? pc.red : (s) => s;
3857
+ const stateColor = report.form_state === "complete" ? green : report.form_state === "invalid" ? red : yellow;
3858
+ const progressStr = `${report.progress.filled_fields}/${report.progress.total_fields} fields filled, ${report.progress.empty_required_fields} required remaining`;
3859
+ lines.push(`${bold("State:")} ${stateColor(report.form_state)} ${dim(`(${progressStr})`)}`);
3860
+ if (report.is_complete) {
3861
+ lines.push("");
3862
+ lines.push(green(bold("Form is complete!")));
3863
+ return lines.join("\n");
3864
+ }
3865
+ lines.push("");
3866
+ lines.push(bold(`Next fields to fill (${report.issues.length} issues, budget: ${report.step_budget}):`));
3867
+ lines.push("");
3868
+ for (const issue of report.issues) {
3869
+ const prioLabel = `P${issue.priority}`;
3870
+ const prioColor = issue.priority <= 1 ? red : issue.priority <= 2 ? yellow : cyan;
3871
+ const sevLabel = issue.severity === "required" ? "required" : "recommended";
3872
+ const kindStr = issue.field ? ` ${dim(`(${issue.field.kind})`)}` : "";
3873
+ let optionsStr = "";
3874
+ if (issue.field?.options && issue.field.options.length > 0) optionsStr = ` ${dim(`[${issue.field.options.join(", ")}]`)}`;
3875
+ lines.push(` ${prioColor(prioLabel)} ${dim(`[${sevLabel}]`)} ${bold(issue.ref)}${kindStr}${optionsStr}`);
3876
+ lines.push(` ${issue.message}`);
3877
+ lines.push(` ${dim("->")} ${cyan(issue.set_example)}`);
3878
+ if (issue.skip_example) lines.push(` ${dim("->")} ${dim(issue.skip_example)}`);
3879
+ lines.push("");
3880
+ }
3881
+ return lines.join("\n").trimEnd();
3882
+ }
3883
+ const DEFAULT_MAX_ISSUES = 10;
3884
+ /**
3885
+ * Register the next command.
3886
+ */
3887
+ function registerNextCommand(program) {
3888
+ program.command("next <file>").description("Show prioritized next fields to fill (field advisor for CLI form filling)").option("--roles <roles>", "Target roles (comma-separated, or '*' for all; default: all)").option("--max-fields <n>", "Max distinct fields per batch").option("--max-groups <n>", "Max distinct groups per batch").option("--max-issues <n>", `Max issues to return (default: ${DEFAULT_MAX_ISSUES})`).action(async (file, options, cmd) => {
3889
+ const ctx = getCommandContext(cmd);
3890
+ try {
3891
+ let targetRoles;
3892
+ if (options.roles) try {
3893
+ targetRoles = parseRolesFlag(options.roles);
3894
+ if (targetRoles.includes("*")) targetRoles = void 0;
3895
+ } catch (error) {
3896
+ logError(`Invalid --roles: ${error instanceof Error ? error.message : String(error)}`);
3897
+ process.exit(1);
3898
+ }
3899
+ const maxFields = options.maxFields ? parseInt(options.maxFields, 10) : void 0;
3900
+ const maxGroups = options.maxGroups ? parseInt(options.maxGroups, 10) : void 0;
3901
+ const maxIssuesParsed = options.maxIssues ? parseInt(options.maxIssues, 10) : void 0;
3902
+ if (maxFields !== void 0 && isNaN(maxFields) || maxGroups !== void 0 && isNaN(maxGroups) || maxIssuesParsed !== void 0 && isNaN(maxIssuesParsed)) {
3903
+ logError("--max-fields, --max-groups, and --max-issues must be numeric");
3904
+ process.exit(1);
3905
+ }
3906
+ logVerbose(ctx, `Reading file: ${file}`);
3907
+ const content = await readFile$1(file);
3908
+ logVerbose(ctx, "Parsing form...");
3909
+ const form = parseForm(content);
3910
+ const harnessConfig = form.metadata?.harnessConfig;
3911
+ const effectiveMaxIssues = maxIssuesParsed ?? harnessConfig?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES;
3912
+ const effectiveMaxFields = maxFields ?? void 0;
3913
+ const effectiveMaxGroups = maxGroups ?? void 0;
3914
+ logVerbose(ctx, "Running inspection...");
3915
+ const result = inspect(form, { targetRoles });
3916
+ const limitedIssues = filterIssuesByScope(filterIssuesByOrder(result.issues, form), form, effectiveMaxFields, effectiveMaxGroups).slice(0, effectiveMaxIssues);
3917
+ const maxPatches = harnessConfig?.maxPatchesPerTurn ?? limitedIssues.length;
3918
+ const stepBudget = Math.min(maxPatches, limitedIssues.length);
3919
+ const enrichedIssues = limitedIssues.map((issue) => enrichIssue(issue, form, file));
3920
+ const counts = result.progressSummary.counts;
3921
+ const output = formatOutput(ctx, {
3922
+ is_complete: result.isComplete,
3923
+ form_state: result.formState,
3924
+ step_budget: stepBudget,
3925
+ progress: {
3926
+ total_fields: counts.totalFields,
3927
+ required_fields: counts.requiredFields,
3928
+ filled_fields: counts.filledFields,
3929
+ empty_required_fields: counts.emptyRequiredFields
3930
+ },
3931
+ issues: enrichedIssues
3932
+ }, (data, useColors) => formatConsoleReport$3(data, useColors));
3933
+ console.log(output);
3934
+ } catch (error) {
3935
+ logError(error instanceof Error ? error.message : String(error));
3936
+ process.exit(1);
3937
+ }
3938
+ });
3939
+ }
3940
+
3730
3941
  //#endregion
3731
3942
  //#region src/cli/commands/plan.ts
3732
3943
  /**
@@ -3930,13 +4141,18 @@ function openBrowser(url) {
3930
4141
  /**
3931
4142
  * Build tabs for a form file.
3932
4143
  * All tabs are always present - content is generated dynamically from the form.
3933
- * Tab order: View, Edit, Source, Report, Values, Schema, Fill Record (if sidecar exists)
4144
+ * Tab order: Form, Report, Edit, Source, Values, Schema, Fill Record (if sidecar exists)
3934
4145
  */
3935
4146
  function buildFormTabs(formPath) {
3936
4147
  const tabs = [
3937
4148
  {
3938
4149
  id: "view",
3939
- label: "View",
4150
+ label: "Form",
4151
+ path: null
4152
+ },
4153
+ {
4154
+ id: "report",
4155
+ label: "Report",
3940
4156
  path: null
3941
4157
  },
3942
4158
  {
@@ -3949,11 +4165,6 @@ function buildFormTabs(formPath) {
3949
4165
  label: "Source",
3950
4166
  path: formPath
3951
4167
  },
3952
- {
3953
- id: "report",
3954
- label: "Report",
3955
- path: null
3956
- },
3957
4168
  {
3958
4169
  id: "values",
3959
4170
  label: "Values",
@@ -4326,7 +4537,7 @@ async function handleSave(req, res, form, filePath, ctx, updateForm) {
4326
4537
  }));
4327
4538
  return;
4328
4539
  }
4329
- await writeFile(newPath, content);
4540
+ await writeFile$1(newPath, content);
4330
4541
  logInfo(ctx, pc.green(`Saved to: ${newPath}`));
4331
4542
  res.writeHead(200, { "Content-Type": "application/json" });
4332
4543
  res.end(JSON.stringify({
@@ -4882,10 +5093,10 @@ function renderFormHtml(form, tabs) {
4882
5093
  if (response.ok) {
4883
5094
  tabCache[tabId] = await response.text();
4884
5095
  } else {
4885
- tabCache[tabId] = '<div class="error">Failed to load content</div>';
5096
+ tabCache[tabId] = '<div class="error">No content available for this tab.</div>';
4886
5097
  }
4887
5098
  } catch (err) {
4888
- tabCache[tabId] = '<div class="error">Failed to load content</div>';
5099
+ tabCache[tabId] = '<div class="error">No content available for this tab.</div>';
4889
5100
  }
4890
5101
  }
4891
5102
  tabViewContent.innerHTML = tabCache[tabId];
@@ -4902,10 +5113,10 @@ function renderFormHtml(form, tabs) {
4902
5113
  if (response.ok) {
4903
5114
  tabCache[tabId] = await response.text();
4904
5115
  } else {
4905
- tabCache[tabId] = '<div class="error">Failed to load content</div>';
5116
+ tabCache[tabId] = '<div class="error">No content available for this tab.</div>';
4906
5117
  }
4907
5118
  } catch (err) {
4908
- tabCache[tabId] = '<div class="error">Failed to load content</div>';
5119
+ tabCache[tabId] = '<div class="error">No content available for this tab.</div>';
4909
5120
  }
4910
5121
  }
4911
5122
  tabOtherContent.innerHTML = tabCache[tabId];
@@ -4913,20 +5124,34 @@ function renderFormHtml(form, tabs) {
4913
5124
  }
4914
5125
  }
4915
5126
 
5127
+ // Navigate to a tab by id, updating the button state and hash
5128
+ let navigating = false;
5129
+ async function navigateToTab(tabId) {
5130
+ navigating = true;
5131
+ tabButtons.forEach(b => b.classList.remove('active'));
5132
+ const btn = document.querySelector('[data-tab="' + tabId + '"]');
5133
+ if (btn) btn.classList.add('active');
5134
+ location.hash = tabId;
5135
+ await showTab(tabId);
5136
+ navigating = false;
5137
+ }
5138
+
4916
5139
  tabButtons.forEach(btn => {
4917
5140
  btn.addEventListener('click', async () => {
4918
- const tabId = btn.dataset.tab;
4919
-
4920
- // Update active button
4921
- tabButtons.forEach(b => b.classList.remove('active'));
4922
- btn.classList.add('active');
4923
-
4924
- await showTab(tabId);
5141
+ await navigateToTab(btn.dataset.tab);
4925
5142
  });
4926
5143
  });
4927
5144
 
4928
- // Load View tab on page load (it's the default tab)
4929
- showTab('view');
5145
+ // Handle hash-based navigation (back/forward, direct URL)
5146
+ window.addEventListener('hashchange', () => {
5147
+ if (navigating) return;
5148
+ const hash = window.location.hash.slice(1);
5149
+ if (hash) navigateToTab(hash);
5150
+ });
5151
+
5152
+ // Load initial tab from hash or default to Form (view) tab
5153
+ const initialTab = window.location.hash.slice(1) || 'view';
5154
+ navigateToTab(initialTab);
4930
5155
 
4931
5156
  // URL copy tooltip functionality - initialize once
4932
5157
  (function initUrlCopyTooltip() {
@@ -5523,7 +5748,7 @@ function registerRenderCommand(program) {
5523
5748
  logDryRun(`Would write HTML to: ${outputPath}`);
5524
5749
  return;
5525
5750
  }
5526
- await writeFile(outputPath, html);
5751
+ await writeFile$1(outputPath, html);
5527
5752
  logSuccess(ctx, pc.green(`✓ Rendered to ${outputPath}`));
5528
5753
  } catch (error) {
5529
5754
  logError(error instanceof Error ? error.message : String(error));
@@ -5685,9 +5910,9 @@ function registerResearchCommand(program) {
5685
5910
  console.log(` ${formPath} ${pc.dim("(filled markform source)")}`);
5686
5911
  console.log(` ${schemaPath} ${pc.dim("(JSON Schema)")}`);
5687
5912
  if (options.transcript && result.transcript) {
5688
- const { serializeSession } = await import("./session-VeSkVrck.mjs");
5913
+ const { serializeSession } = await import("./session-DHyTMP67.mjs");
5689
5914
  const transcriptPath = outputPath.replace(/\.form\.md$/, ".session.yaml");
5690
- const { writeFile } = await import("./shared-fb0nkzQi.mjs");
5915
+ const { writeFile } = await import("./shared-BLh342F5.mjs");
5691
5916
  await writeFile(transcriptPath, serializeSession(result.transcript));
5692
5917
  logInfo(ctx, `Transcript: ${transcriptPath}`);
5693
5918
  }
@@ -5734,6 +5959,354 @@ function registerSchemaCommand(program) {
5734
5959
  });
5735
5960
  }
5736
5961
 
5962
+ //#endregion
5963
+ //#region src/cli/commands/set.ts
5964
+ /**
5965
+ * Parse a CLI value string into a RawFieldValue.
5966
+ *
5967
+ * Only detects JSON arrays/objects ([ or { prefix). Everything else
5968
+ * is passed through as a string — the coercion layer handles
5969
+ * type conversion based on the field's kind.
5970
+ */
5971
+ function parseCliValue(raw) {
5972
+ if (raw.startsWith("[") || raw.startsWith("{")) try {
5973
+ return JSON.parse(raw);
5974
+ } catch {
5975
+ return raw;
5976
+ }
5977
+ return raw;
5978
+ }
5979
+ /**
5980
+ * Format set report for console output.
5981
+ */
5982
+ function formatConsoleReport$2(report, useColors) {
5983
+ const lines = [];
5984
+ const bold = useColors ? pc.bold : (s) => s;
5985
+ const dim = useColors ? pc.dim : (s) => s;
5986
+ const cyan = useColors ? pc.cyan : (s) => s;
5987
+ const green = useColors ? pc.green : (s) => s;
5988
+ const red = useColors ? pc.red : (s) => s;
5989
+ lines.push(bold(cyan("Set Result")));
5990
+ lines.push("");
5991
+ const statusColor = report.apply_status === "applied" ? green : red;
5992
+ lines.push(`${bold("Status:")} ${statusColor(report.apply_status)}`);
5993
+ lines.push(`${bold("Form State:")} ${report.form_state}`);
5994
+ lines.push(`${bold("Complete:")} ${report.is_complete ? green("yes") : dim("no")}`);
5995
+ lines.push("");
5996
+ const counts = report.progress.counts;
5997
+ lines.push(bold("Progress:"));
5998
+ lines.push(` Total fields: ${counts.totalFields}`);
5999
+ lines.push(` Filled: ${counts.filledFields}, Empty: ${counts.emptyFields}`);
6000
+ lines.push(` Empty required: ${counts.emptyRequiredFields}`);
6001
+ lines.push("");
6002
+ if (report.issues.length > 0) {
6003
+ lines.push(bold(`Issues (${report.issues.length}):`));
6004
+ for (const issue of report.issues) lines.push(` P${issue.priority} ${dim(issue.ref)}: ${issue.message}`);
6005
+ } else lines.push(dim("No issues."));
6006
+ return lines.join("\n");
6007
+ }
6008
+ /**
6009
+ * Register the set command.
6010
+ */
6011
+ function registerSetCommand(program) {
6012
+ program.command("set <file> [fieldId] [value]").description("Set field values with auto-coercion").option("--values <json>", "Batch set: JSON object of {fieldId: rawValue} pairs").option("--append <value>", "Append item/row to a collection field").option("--delete <n>", "Delete item/row at index (0-based) from a collection field").option("--clear", "Clear the field value").option("--skip", "Skip the field (marks as skipped)").option("--abort", "Abort the field (marks as unable to complete)").option("--role <role>", "Role for skip/abort (default: \"user\")", "user").option("--reason <text>", "Reason for skip/abort").option("-o, --output <file>", "Output file (default: modify in place)").option("--report", "Output JSON report after applying (issues, progress)").option("--normalize", "Regenerate form without preserving external content").action(async (file, fieldId, value, options, cmd) => {
6013
+ const ctx = getCommandContext(cmd);
6014
+ try {
6015
+ logVerbose(ctx, `Reading file: ${file}`);
6016
+ const content = await readFile$1(file);
6017
+ logVerbose(ctx, "Parsing form...");
6018
+ const form = parseForm(content);
6019
+ let patches;
6020
+ if (options.values) {
6021
+ if (fieldId) {
6022
+ logError("Cannot use --values with positional fieldId/value arguments");
6023
+ process.exit(1);
6024
+ }
6025
+ if (options.clear || options.skip || options.abort || options.append !== void 0 || options.delete !== void 0) {
6026
+ logError("Cannot use --values with --clear, --skip, --abort, --append, or --delete");
6027
+ process.exit(1);
6028
+ }
6029
+ let inputContext;
6030
+ try {
6031
+ inputContext = JSON.parse(options.values);
6032
+ } catch {
6033
+ logError("Invalid JSON in --values option");
6034
+ process.exit(1);
6035
+ }
6036
+ if (typeof inputContext !== "object" || Array.isArray(inputContext)) {
6037
+ logError("--values must be a JSON object");
6038
+ process.exit(1);
6039
+ }
6040
+ const result = coerceInputContext(form, inputContext);
6041
+ for (const w of result.warnings) logVerbose(ctx, `Warning: ${w}`);
6042
+ if (result.errors.length > 0) {
6043
+ for (const e of result.errors) logError(e);
6044
+ process.exit(1);
6045
+ }
6046
+ patches = result.patches;
6047
+ } else if (!fieldId) {
6048
+ logError("Either <fieldId> or --values is required");
6049
+ process.exit(1);
6050
+ } else if (options.clear) {
6051
+ if (!findFieldById(form, fieldId)) {
6052
+ logError(`Field "${fieldId}" not found in form`);
6053
+ process.exit(1);
6054
+ }
6055
+ patches = [{
6056
+ op: "clear_field",
6057
+ fieldId
6058
+ }];
6059
+ } else if (options.skip) {
6060
+ if (!findFieldById(form, fieldId)) {
6061
+ logError(`Field "${fieldId}" not found in form`);
6062
+ process.exit(1);
6063
+ }
6064
+ patches = [{
6065
+ op: "skip_field",
6066
+ fieldId,
6067
+ role: options.role,
6068
+ ...options.reason && { reason: options.reason }
6069
+ }];
6070
+ } else if (options.abort) {
6071
+ if (!findFieldById(form, fieldId)) {
6072
+ logError(`Field "${fieldId}" not found in form`);
6073
+ process.exit(1);
6074
+ }
6075
+ patches = [{
6076
+ op: "abort_field",
6077
+ fieldId,
6078
+ role: options.role,
6079
+ ...options.reason && { reason: options.reason }
6080
+ }];
6081
+ } else if (options.append !== void 0) {
6082
+ const field = findFieldById(form, fieldId);
6083
+ if (!field) {
6084
+ logError(`Field "${fieldId}" not found in form`);
6085
+ process.exit(1);
6086
+ }
6087
+ const rawValue = parseCliValue(options.append);
6088
+ if (field.kind === "table") {
6089
+ const rows = Array.isArray(rawValue) ? rawValue : typeof rawValue === "object" && rawValue !== null ? [rawValue] : null;
6090
+ if (!rows) {
6091
+ logError(`--append for table field "${fieldId}" requires a JSON object or array of row objects`);
6092
+ process.exit(1);
6093
+ }
6094
+ patches = [{
6095
+ op: "append_table",
6096
+ fieldId,
6097
+ value: rows
6098
+ }];
6099
+ } else if (field.kind === "string_list") patches = [{
6100
+ op: "append_string_list",
6101
+ fieldId,
6102
+ value: Array.isArray(rawValue) ? rawValue : [typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue)]
6103
+ }];
6104
+ else if (field.kind === "url_list") patches = [{
6105
+ op: "append_url_list",
6106
+ fieldId,
6107
+ value: Array.isArray(rawValue) ? rawValue : [typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue)]
6108
+ }];
6109
+ else {
6110
+ logError(`--append is not supported for ${field.kind} fields (only table, string_list, url_list)`);
6111
+ process.exit(1);
6112
+ }
6113
+ } else if (options.delete !== void 0) {
6114
+ const field = findFieldById(form, fieldId);
6115
+ if (!field) {
6116
+ logError(`Field "${fieldId}" not found in form`);
6117
+ process.exit(1);
6118
+ }
6119
+ const index = parseInt(options.delete, 10);
6120
+ if (isNaN(index) || index < 0) {
6121
+ logError(`--delete requires a non-negative integer index, got "${options.delete}"`);
6122
+ process.exit(1);
6123
+ }
6124
+ if (field.kind === "table") patches = [{
6125
+ op: "delete_table",
6126
+ fieldId,
6127
+ value: index
6128
+ }];
6129
+ else if (field.kind === "string_list") patches = [{
6130
+ op: "delete_string_list",
6131
+ fieldId,
6132
+ value: index
6133
+ }];
6134
+ else if (field.kind === "url_list") patches = [{
6135
+ op: "delete_url_list",
6136
+ fieldId,
6137
+ value: index
6138
+ }];
6139
+ else {
6140
+ logError(`--delete is not supported for ${field.kind} fields (only table, string_list, url_list)`);
6141
+ process.exit(1);
6142
+ }
6143
+ } else if (value !== void 0) {
6144
+ const result = coerceToFieldPatch(form, fieldId, parseCliValue(value));
6145
+ if (!result.ok) {
6146
+ logError(result.error);
6147
+ process.exit(1);
6148
+ }
6149
+ if ("warning" in result && result.warning) logVerbose(ctx, `Warning: ${result.warning}`);
6150
+ patches = [result.patch];
6151
+ } else {
6152
+ logError("No value provided. Use <value>, --clear, --skip, --abort, --append, or --delete");
6153
+ process.exit(1);
6154
+ }
6155
+ if (ctx.dryRun) {
6156
+ logDryRun(`Would apply ${patches.length} patches to ${file}`, { patches });
6157
+ return;
6158
+ }
6159
+ logVerbose(ctx, `Applying ${patches.length} patches...`);
6160
+ const applyResult = applyPatches(form, patches);
6161
+ if (applyResult.applyStatus === "rejected") {
6162
+ logError("Patches rejected");
6163
+ for (const rp of applyResult.rejectedPatches) logError(` ${rp.message}`);
6164
+ process.exit(1);
6165
+ }
6166
+ if (!(Boolean(options.clear) || Boolean(options.skip) || Boolean(options.abort) || options.delete !== void 0)) {
6167
+ const patchedFieldIds = new Set(patches.map((p) => "fieldId" in p ? p.fieldId : "").filter(Boolean));
6168
+ const relevantIssues = applyResult.issues.filter((i) => i.reason === "validation_error" && patchedFieldIds.has(i.ref));
6169
+ for (const issue of relevantIssues) logWarn(ctx, issue.message);
6170
+ }
6171
+ if (options.report) {
6172
+ const output = formatOutput(ctx, {
6173
+ apply_status: applyResult.applyStatus,
6174
+ form_state: applyResult.formState,
6175
+ is_complete: applyResult.isComplete,
6176
+ structure: applyResult.structureSummary,
6177
+ progress: applyResult.progressSummary,
6178
+ issues: applyResult.issues
6179
+ }, (data, useColors) => formatConsoleReport$2(data, useColors));
6180
+ if (options.output) {
6181
+ await writeFile$1(options.output, output);
6182
+ logSuccess(ctx, `Report written to ${options.output}`);
6183
+ } else console.log(output);
6184
+ } else {
6185
+ const output = serializeForm(form, { preserveContent: !options.normalize });
6186
+ const target = options.output ?? file;
6187
+ await writeFile$1(target, output);
6188
+ logSuccess(ctx, `Form updated: ${target}`);
6189
+ }
6190
+ } catch (error) {
6191
+ logError(error instanceof Error ? error.message : String(error));
6192
+ process.exit(1);
6193
+ }
6194
+ });
6195
+ }
6196
+
6197
+ //#endregion
6198
+ //#region src/cli/commands/setup.ts
6199
+ const DO_NOT_EDIT_MARKER = `<!-- DO NOT EDIT: Generated by markform setup.
6200
+ Run 'markform setup' to update.
6201
+ -->`;
6202
+ /**
6203
+ * Load SKILL.md content and insert the DO NOT EDIT marker after frontmatter.
6204
+ */
6205
+ function loadSkillWithMarker() {
6206
+ const lines = readFileSync(resolvePackagePath(import.meta.url, "docs/skill/SKILL.md"), "utf-8").split("\n");
6207
+ let endOfFrontmatter = -1;
6208
+ if (lines[0] === "---") {
6209
+ for (let i = 1; i < lines.length; i++) if (lines[i] === "---") {
6210
+ endOfFrontmatter = i;
6211
+ break;
6212
+ }
6213
+ }
6214
+ if (endOfFrontmatter >= 0) lines.splice(endOfFrontmatter + 1, 0, "", DO_NOT_EDIT_MARKER);
6215
+ return lines.join("\n");
6216
+ }
6217
+ /**
6218
+ * Install the skill file to .claude/skills/markform/SKILL.md.
6219
+ */
6220
+ async function installSkill(projectDir, ctx) {
6221
+ const skillDir = join(projectDir, ".claude", "skills", "markform");
6222
+ const skillPath = join(skillDir, "SKILL.md");
6223
+ const isUpdate = existsSync(skillPath);
6224
+ const content = loadSkillWithMarker();
6225
+ await mkdir(skillDir, { recursive: true });
6226
+ await writeFile(skillPath, content, "utf-8");
6227
+ logSuccess(ctx, `${isUpdate ? "Updated" : "Installed"} ${pc.bold(".claude/skills/markform/SKILL.md")}`);
6228
+ }
6229
+ /**
6230
+ * Run setup in auto mode (non-interactive, for agents).
6231
+ */
6232
+ async function setupAuto(ctx) {
6233
+ logInfo(ctx, "Installing Markform skill for Claude Code...");
6234
+ await installSkill(process.cwd(), ctx);
6235
+ logInfo(ctx, `Run ${pc.cyan("markform skill")} to view the installed skill content.`);
6236
+ }
6237
+ /**
6238
+ * Run setup in interactive mode (guided, for humans).
6239
+ */
6240
+ async function setupInteractive(ctx) {
6241
+ p.intro(pc.bgCyan(pc.black(" markform setup ")));
6242
+ p.log.info([
6243
+ "This will install Markform as a Claude Code skill in your project.",
6244
+ "",
6245
+ ` ${pc.dim("Creates:")} .claude/skills/markform/SKILL.md`,
6246
+ "",
6247
+ "This teaches Claude Code how to use markform commands when working",
6248
+ "with .form.md files in this project."
6249
+ ].join("\n"));
6250
+ const shouldContinue = await p.confirm({
6251
+ message: "Install the Markform skill?",
6252
+ initialValue: true
6253
+ });
6254
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
6255
+ p.outro("Setup cancelled.");
6256
+ return;
6257
+ }
6258
+ await installSkill(process.cwd(), ctx);
6259
+ p.outro("Markform skill installed! Claude Code will now know how to use markform.");
6260
+ }
6261
+ /**
6262
+ * Register the setup command.
6263
+ */
6264
+ function registerSetupCommand(program) {
6265
+ const cmd = program.command("setup").description("Install Markform as a Claude Code skill in the current project").option("--auto", "Non-interactive setup (for agents and scripts)").option("--interactive", "Guided setup with prompts (for humans)").action(async (options, command) => {
6266
+ const ctx = getCommandContext(command);
6267
+ try {
6268
+ if (!options.auto && !options.interactive) {
6269
+ cmd.help();
6270
+ return;
6271
+ }
6272
+ if (options.auto) await setupAuto(ctx);
6273
+ else await setupInteractive(ctx);
6274
+ } catch (error) {
6275
+ logError(error instanceof Error ? error.message : String(error));
6276
+ process.exit(1);
6277
+ }
6278
+ });
6279
+ }
6280
+
6281
+ //#endregion
6282
+ //#region src/cli/commands/skill.ts
6283
+ /**
6284
+ * Load the SKILL.md content.
6285
+ */
6286
+ function loadSkillContent() {
6287
+ const skillPath = resolvePackagePath(import.meta.url, "docs/skill/SKILL.md");
6288
+ try {
6289
+ return readFileSync(skillPath, "utf-8");
6290
+ } catch (error) {
6291
+ const message = error instanceof Error ? error.message : String(error);
6292
+ throw new Error(`Failed to load SKILL.md from ${skillPath}: ${message}`);
6293
+ }
6294
+ }
6295
+ /**
6296
+ * Register the skill command.
6297
+ */
6298
+ function registerSkillCommand(program) {
6299
+ program.command("skill").description("Output SKILL.md content for Claude Code integration").action(() => {
6300
+ try {
6301
+ const content = loadSkillContent();
6302
+ process.stdout.write(content);
6303
+ } catch (error) {
6304
+ logError(error instanceof Error ? error.message : String(error));
6305
+ process.exit(1);
6306
+ }
6307
+ });
6308
+ }
6309
+
5737
6310
  //#endregion
5738
6311
  //#region src/cli/commands/status.ts
5739
6312
  /**
@@ -6039,7 +6612,7 @@ function registerValidateCommand(program) {
6039
6612
  /**
6040
6613
  * CLI implementation for markform.
6041
6614
  *
6042
- * Provides commands for inspecting, applying patches, exporting,
6615
+ * Provides commands for inspecting, patching, exporting,
6043
6616
  * serving, and running harness loops on .form.md files.
6044
6617
  */
6045
6618
  /**
@@ -6047,6 +6620,7 @@ function registerValidateCommand(program) {
6047
6620
  */
6048
6621
  function withColoredHelp(cmd) {
6049
6622
  cmd.configureHelp({
6623
+ helpWidth: 88,
6050
6624
  styleTitle: (str) => pc.bold(pc.cyan(str)),
6051
6625
  styleCommandText: (str) => pc.green(str),
6052
6626
  styleOptionText: (str) => pc.yellow(str),
@@ -6059,12 +6633,18 @@ function withColoredHelp(cmd) {
6059
6633
  */
6060
6634
  function createProgram() {
6061
6635
  const program = withColoredHelp(new Command());
6062
- program.name("markform").description("Agent-friendly, human-readable, editable forms").version(CLI_VERSION, "--version", "output the version number").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})`).option("--overwrite", "Overwrite existing field values (default: continue/skip filled)");
6636
+ program.name("markform").description("Agent-friendly, human-readable, editable forms").version(CLI_VERSION, "--version", "output the version number").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})`).option("--overwrite", "Overwrite existing field values (default: continue/skip filled)").addHelpText("after", `
6637
+ Skill Setup:
6638
+ To use Markform as a Claude Code skill, run: markform setup --auto
6639
+ To view the skill content: markform skill
6640
+
6641
+ Getting Started:
6642
+ npm install -g markform && markform setup --auto`);
6063
6643
  registerReadmeCommand(program);
6064
6644
  registerDocsCommand(program);
6065
6645
  registerSpecCommand(program);
6066
6646
  registerApisCommand(program);
6067
- registerApplyCommand(program);
6647
+ registerPatchCommand(program);
6068
6648
  registerBrowseCommand(program);
6069
6649
  registerDumpCommand(program);
6070
6650
  registerExamplesCommand(program);
@@ -6072,6 +6652,7 @@ function createProgram() {
6072
6652
  registerFillCommand(program);
6073
6653
  registerInspectCommand(program);
6074
6654
  registerModelsCommand(program);
6655
+ registerNextCommand(program);
6075
6656
  registerPlanCommand(program);
6076
6657
  registerRenderCommand(program);
6077
6658
  registerReportCommand(program);
@@ -6079,6 +6660,9 @@ function createProgram() {
6079
6660
  registerRunCommand(program);
6080
6661
  registerSchemaCommand(program);
6081
6662
  registerServeCommand(program);
6663
+ registerSetCommand(program);
6664
+ registerSetupCommand(program);
6665
+ registerSkillCommand(program);
6082
6666
  registerStatusCommand(program);
6083
6667
  registerValidateCommand(program);
6084
6668
  return program;
@@ -6092,4 +6676,4 @@ async function runCli() {
6092
6676
 
6093
6677
  //#endregion
6094
6678
  export { runCli as t };
6095
- //# sourceMappingURL=cli-ZcOC47KK.mjs.map
6679
+ //# sourceMappingURL=cli-B1T8kMFt.mjs.map