markform 0.1.16 → 0.1.18

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 (35) hide show
  1. package/LICENSE +369 -0
  2. package/README.md +154 -214
  3. package/dist/ai-sdk.d.mts +1 -1
  4. package/dist/ai-sdk.mjs +2 -2
  5. package/dist/{apply-CXsI5N9x.mjs → apply-BYgtU64w.mjs} +203 -16
  6. package/dist/apply-BYgtU64w.mjs.map +1 -0
  7. package/dist/bin.mjs +1 -1
  8. package/dist/{cli-BsFessUW.mjs → cli-D9w0Bp4J.mjs} +199 -13
  9. package/dist/cli-D9w0Bp4J.mjs.map +1 -0
  10. package/dist/cli.mjs +1 -1
  11. package/dist/{coreTypes-DE6Giau5.d.mts → coreTypes-BMEs8h_2.d.mts} +165 -2
  12. package/dist/{coreTypes-DiCddBKu.mjs → coreTypes-SDB3KRRJ.mjs} +9 -4
  13. package/dist/coreTypes-SDB3KRRJ.mjs.map +1 -0
  14. package/dist/index.d.mts +266 -2
  15. package/dist/index.mjs +5 -5
  16. package/dist/{session-B7aR6hno.mjs → session-CW9AQw6i.mjs} +1 -1
  17. package/dist/{session-XDrocA3j.mjs → session-Ci4B0Pna.mjs} +2 -2
  18. package/dist/{session-XDrocA3j.mjs.map → session-Ci4B0Pna.mjs.map} +1 -1
  19. package/dist/{src-Dv3IZSQU.mjs → src-DDxi-2ne.mjs} +966 -32
  20. package/dist/src-DDxi-2ne.mjs.map +1 -0
  21. package/docs/markform-apis.md +110 -0
  22. package/docs/markform-reference.md +58 -0
  23. package/docs/markform-spec.md +204 -9
  24. package/examples/movie-research/movie-deep-research-mock-filled.form.md +1 -1
  25. package/examples/movie-research/movie-deep-research.form.md +1 -1
  26. package/examples/parallel/parallel-research.form.md +57 -0
  27. package/examples/plan-document/plan-document-markdoc.form.md +35 -0
  28. package/examples/plan-document/plan-document-progress.form.md +47 -0
  29. package/examples/plan-document/plan-document.form.md +47 -0
  30. package/examples/startup-deep-research/startup-deep-research.form.md +1 -1
  31. package/package.json +2 -2
  32. package/dist/apply-CXsI5N9x.mjs.map +0 -1
  33. package/dist/cli-BsFessUW.mjs.map +0 -1
  34. package/dist/coreTypes-DiCddBKu.mjs.map +0 -1
  35. package/dist/src-Dv3IZSQU.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
 
2
- import { L as PatchSchema, V as RunModeSchema } from "./coreTypes-DiCddBKu.mjs";
3
- import { C as DEFAULT_MAX_TURNS, D as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, E as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, G as MarkformConfigError, J as MarkformParseError, O as DEFAULT_ROLES, S as DEFAULT_MAX_STEPS_PER_TURN, T as DEFAULT_PRIORITY, V as getWebSearchConfig, b as DEFAULT_MAX_ISSUES_PER_TURN, d as serializeForm, g as preprocessCommentSyntax, h as detectSyntaxStyle, i as inspect, k as DEFAULT_ROLE_INSTRUCTIONS, r as getFieldsForRoles, t as applyPatches, v as AGENT_ROLE, x as DEFAULT_MAX_PATCHES_PER_TURN } from "./apply-CXsI5N9x.mjs";
2
+ import { L as PatchSchema, V as RunModeSchema } from "./coreTypes-SDB3KRRJ.mjs";
3
+ import { A as DEFAULT_ROLE_INSTRUCTIONS, C as DEFAULT_MAX_STEPS_PER_TURN, D as DEFAULT_RESEARCH_MAX_ISSUES_PER_TURN, E as DEFAULT_PRIORITY, H as getWebSearchConfig, K as MarkformConfigError, O as DEFAULT_RESEARCH_MAX_PATCHES_PER_TURN, S as DEFAULT_MAX_PATCHES_PER_TURN, Y as MarkformParseError, b as DEFAULT_MAX_ISSUES_PER_TURN, d as serializeForm, g as preprocessCommentSyntax, h as detectSyntaxStyle, i as inspect, k as DEFAULT_ROLES, r as getFieldsForRoles, t as applyPatches, v as AGENT_ROLE, w as DEFAULT_MAX_TURNS, x as DEFAULT_MAX_PARALLEL_AGENTS } from "./apply-BYgtU64w.mjs";
4
4
  import { z } from "zod";
5
5
  import Markdoc from "@markdoc/markdoc";
6
6
  import YAML from "yaml";
@@ -588,14 +588,16 @@ function parseBaseFieldAttrs(node, kind) {
588
588
  };
589
589
  }
590
590
  /**
591
- * Get common field attributes (priority, role, validate, report).
591
+ * Get common field attributes (priority, role, validate, report, parallel, order).
592
592
  */
593
593
  function getCommonFieldAttrs(node) {
594
594
  return {
595
595
  priority: getPriorityAttr(node),
596
596
  role: getStringAttr(node, "role") ?? AGENT_ROLE,
597
597
  validate: getValidateAttr(node),
598
- report: getBooleanAttr(node, "report")
598
+ report: getBooleanAttr(node, "report"),
599
+ parallel: getStringAttr(node, "parallel"),
600
+ order: getNumberAttr(node, "order")
599
601
  };
600
602
  }
601
603
  /**
@@ -1125,11 +1127,23 @@ const LEGACY_TAG_TO_KIND = {
1125
1127
  "table-field": "table"
1126
1128
  };
1127
1129
  /**
1130
+ * Recursively check if any children of a node are field tags.
1131
+ * Throws MarkformParseError if a nested field is found.
1132
+ */
1133
+ function validateNoNestedFields(node, outerFieldId) {
1134
+ if (!node.children || !Array.isArray(node.children)) return;
1135
+ for (const child of node.children) {
1136
+ if (isTagNode(child, "field")) throw new MarkformParseError(`Field tags cannot be nested. Found '${getStringAttr(child, "id") ?? "unknown"}' inside '${outerFieldId}'`);
1137
+ validateNoNestedFields(child, outerFieldId);
1138
+ }
1139
+ }
1140
+ /**
1128
1141
  * Parse a unified field tag: {% field kind="..." ... %}
1129
1142
  */
1130
1143
  function parseUnifiedField(node) {
1131
1144
  const kind = getStringAttr(node, "kind");
1132
1145
  if (!kind) throw new MarkformParseError("field tag missing required 'kind' attribute");
1146
+ validateNoNestedFields(node, getStringAttr(node, "id") ?? "unknown");
1133
1147
  if (!FIELD_KINDS.includes(kind)) throw new MarkformParseError(`field tag has invalid kind '${kind}'. Valid kinds: ${FIELD_KINDS.join(", ")}`);
1134
1148
  switch (kind) {
1135
1149
  case "string": return parseStringField(node);
@@ -1159,6 +1173,274 @@ function parseField(node) {
1159
1173
  return null;
1160
1174
  }
1161
1175
 
1176
+ //#endregion
1177
+ //#region src/markdown/markdownHeaders.ts
1178
+ const ATX_HEADING_PATTERN = /^(#{1,6})\s+(.*)$/;
1179
+ /**
1180
+ * Find all headings in a markdown document.
1181
+ * Returns headings in document order.
1182
+ *
1183
+ * Only ATX-style headings (# Title) are recognized.
1184
+ * Headings inside fenced code blocks are ignored.
1185
+ */
1186
+ function findAllHeadings(markdown) {
1187
+ const lines = markdown.split("\n");
1188
+ const headings = [];
1189
+ let inCodeBlock = false;
1190
+ for (let i = 0; i < lines.length; i++) {
1191
+ const line = lines[i] ?? "";
1192
+ const lineNumber = i + 1;
1193
+ if (line.startsWith("```") || line.startsWith("~~~")) {
1194
+ inCodeBlock = !inCodeBlock;
1195
+ continue;
1196
+ }
1197
+ if (inCodeBlock) continue;
1198
+ const match = ATX_HEADING_PATTERN.exec(line);
1199
+ if (match) {
1200
+ const hashes = match[1] ?? "";
1201
+ const title = (match[2] ?? "").trim();
1202
+ headings.push({
1203
+ level: hashes.length,
1204
+ title,
1205
+ line: lineNumber,
1206
+ position: {
1207
+ start: {
1208
+ line: lineNumber,
1209
+ col: 1
1210
+ },
1211
+ end: {
1212
+ line: lineNumber,
1213
+ col: line.length + 1
1214
+ }
1215
+ }
1216
+ });
1217
+ }
1218
+ }
1219
+ return headings;
1220
+ }
1221
+ /**
1222
+ * Find all headings that enclose a given line position.
1223
+ * Returns headings from innermost (most specific) to outermost (least specific).
1224
+ *
1225
+ * A heading "encloses" a line if:
1226
+ * 1. The heading appears before the line
1227
+ * 2. No heading of equal or higher level appears between them
1228
+ *
1229
+ * @param markdown - The markdown source text
1230
+ * @param line - The line number (1-indexed)
1231
+ * @returns Array of enclosing headings, innermost first
1232
+ */
1233
+ function findEnclosingHeadings(markdown, line) {
1234
+ if (line <= 0) return [];
1235
+ const precedingHeadings = findAllHeadings(markdown).filter((h) => h.line < line);
1236
+ if (precedingHeadings.length === 0) return [];
1237
+ const result = [];
1238
+ let minLevelSeen = Infinity;
1239
+ for (let i = precedingHeadings.length - 1; i >= 0; i--) {
1240
+ const heading = precedingHeadings[i];
1241
+ if (!heading) continue;
1242
+ if (heading.level < minLevelSeen) {
1243
+ result.push(heading);
1244
+ minLevelSeen = heading.level;
1245
+ }
1246
+ }
1247
+ return result;
1248
+ }
1249
+
1250
+ //#endregion
1251
+ //#region src/engine/injectIds.ts
1252
+ const CHECKBOX_PATTERN = /^(\s*)-\s+(\[[^\]]\])\s+(.*)$/;
1253
+ const MARKDOC_ID_PATTERN = /\{%\s*#(\w+)\s*%\}/;
1254
+ const HTML_COMMENT_ID_PATTERN = /<!--\s*#(\w+)\s*-->/;
1255
+ /**
1256
+ * Find all checkboxes in a markdown document.
1257
+ * Returns checkboxes in document order with enclosing heading info.
1258
+ */
1259
+ function findAllCheckboxes(markdown) {
1260
+ const lines = markdown.split("\n");
1261
+ const checkboxes = [];
1262
+ let inCodeBlock = false;
1263
+ for (let i = 0; i < lines.length; i++) {
1264
+ const line = lines[i] ?? "";
1265
+ const lineNumber = i + 1;
1266
+ if (line.trimStart().startsWith("```") || line.trimStart().startsWith("~~~")) {
1267
+ inCodeBlock = !inCodeBlock;
1268
+ continue;
1269
+ }
1270
+ if (inCodeBlock) continue;
1271
+ const match = CHECKBOX_PATTERN.exec(line);
1272
+ if (!match) continue;
1273
+ const marker$2 = match[2] ?? "";
1274
+ const rest = match[3] ?? "";
1275
+ const state = CHECKBOX_MARKERS[marker$2];
1276
+ if (state === void 0) continue;
1277
+ let label = rest;
1278
+ let id;
1279
+ const markdocMatch = MARKDOC_ID_PATTERN.exec(rest);
1280
+ if (markdocMatch) {
1281
+ id = markdocMatch[1];
1282
+ label = rest.replace(MARKDOC_ID_PATTERN, "").trim();
1283
+ } else {
1284
+ const htmlMatch = HTML_COMMENT_ID_PATTERN.exec(rest);
1285
+ if (htmlMatch) {
1286
+ id = htmlMatch[1];
1287
+ label = rest.replace(HTML_COMMENT_ID_PATTERN, "").trim();
1288
+ }
1289
+ }
1290
+ const enclosingHeadings = findEnclosingHeadings(markdown, lineNumber);
1291
+ checkboxes.push({
1292
+ id,
1293
+ label,
1294
+ state,
1295
+ line: lineNumber,
1296
+ position: {
1297
+ start: {
1298
+ line: lineNumber,
1299
+ col: 1
1300
+ },
1301
+ end: {
1302
+ line: lineNumber,
1303
+ col: line.length + 1
1304
+ }
1305
+ },
1306
+ enclosingHeadings
1307
+ });
1308
+ }
1309
+ return checkboxes;
1310
+ }
1311
+ /**
1312
+ * Inject IDs into checkboxes in a markdown document.
1313
+ *
1314
+ * Uses a generator function to create unique IDs for each checkbox.
1315
+ * Throws if duplicate IDs are generated or if generated IDs conflict
1316
+ * with existing ones.
1317
+ */
1318
+ function injectCheckboxIds(markdown, options) {
1319
+ const { generator, onlyMissing = true } = options;
1320
+ const checkboxes = findAllCheckboxes(markdown);
1321
+ const existingIds = /* @__PURE__ */ new Set();
1322
+ for (const checkbox of checkboxes) if (checkbox.id && onlyMissing) existingIds.add(checkbox.id);
1323
+ const needsId = checkboxes.filter((cb) => onlyMissing ? !cb.id : true);
1324
+ const generatedIds = /* @__PURE__ */ new Map();
1325
+ const allGeneratedIds = /* @__PURE__ */ new Set();
1326
+ for (let i = 0; i < needsId.length; i++) {
1327
+ const checkbox = needsId[i];
1328
+ const newId = generator(checkbox, i);
1329
+ if (allGeneratedIds.has(newId)) throw new MarkformParseError(`Duplicate generated ID '${newId}' for checkbox '${checkbox.label}'`, { line: checkbox.line });
1330
+ if (onlyMissing && existingIds.has(newId)) throw new MarkformParseError(`Generated ID '${newId}' conflicts with existing ID`, { line: checkbox.line });
1331
+ allGeneratedIds.add(newId);
1332
+ generatedIds.set(checkbox.label, newId);
1333
+ }
1334
+ if (needsId.length === 0) return {
1335
+ markdown,
1336
+ injectedCount: 0,
1337
+ injectedIds: /* @__PURE__ */ new Map()
1338
+ };
1339
+ const lines = markdown.split("\n");
1340
+ const sortedByLine = [...needsId].sort((a, b) => b.line - a.line);
1341
+ for (const checkbox of sortedByLine) {
1342
+ const lineIndex = checkbox.line - 1;
1343
+ const line = lines[lineIndex];
1344
+ const newId = generatedIds.get(checkbox.label);
1345
+ let updatedLine = line;
1346
+ if (!onlyMissing || checkbox.id) {
1347
+ updatedLine = updatedLine.replace(MARKDOC_ID_PATTERN, "").trim();
1348
+ updatedLine = updatedLine.replace(HTML_COMMENT_ID_PATTERN, "").trim();
1349
+ }
1350
+ lines[lineIndex] = `${updatedLine} {% #${newId} %}`;
1351
+ }
1352
+ return {
1353
+ markdown: lines.join("\n"),
1354
+ injectedCount: needsId.length,
1355
+ injectedIds: generatedIds
1356
+ };
1357
+ }
1358
+ /**
1359
+ * Find all headings with their existing IDs.
1360
+ * Strips ID annotations from title for clean generator input.
1361
+ */
1362
+ function findAllHeadingsWithIds(markdown) {
1363
+ const headings = findAllHeadings(markdown);
1364
+ const lines = markdown.split("\n");
1365
+ return headings.map((heading) => {
1366
+ const line = lines[heading.line - 1] ?? "";
1367
+ let id;
1368
+ let cleanTitle = heading.title;
1369
+ const markdocMatch = MARKDOC_ID_PATTERN.exec(line);
1370
+ if (markdocMatch) {
1371
+ id = markdocMatch[1];
1372
+ cleanTitle = heading.title.replace(MARKDOC_ID_PATTERN, "").trim();
1373
+ } else {
1374
+ const htmlMatch = HTML_COMMENT_ID_PATTERN.exec(line);
1375
+ if (htmlMatch) {
1376
+ id = htmlMatch[1];
1377
+ cleanTitle = heading.title.replace(HTML_COMMENT_ID_PATTERN, "").trim();
1378
+ }
1379
+ }
1380
+ return {
1381
+ ...heading,
1382
+ title: cleanTitle,
1383
+ id
1384
+ };
1385
+ });
1386
+ }
1387
+ /**
1388
+ * Inject IDs into headings in a markdown document.
1389
+ *
1390
+ * Uses a generator function to create unique IDs for each heading.
1391
+ * Throws if duplicate IDs are generated or if generated IDs conflict
1392
+ * with existing ones.
1393
+ */
1394
+ function injectHeaderIds(markdown, options) {
1395
+ const { generator, onlyMissing = true, levels = [
1396
+ 1,
1397
+ 2,
1398
+ 3,
1399
+ 4,
1400
+ 5,
1401
+ 6
1402
+ ] } = options;
1403
+ const allHeadings = findAllHeadingsWithIds(markdown);
1404
+ const levelSet = new Set(levels);
1405
+ const headings = allHeadings.filter((h) => levelSet.has(h.level));
1406
+ const existingIds = /* @__PURE__ */ new Set();
1407
+ for (const heading of headings) if (heading.id && onlyMissing) existingIds.add(heading.id);
1408
+ const needsId = headings.filter((h) => onlyMissing ? !h.id : true);
1409
+ const generatedIds = /* @__PURE__ */ new Map();
1410
+ const allGeneratedIds = /* @__PURE__ */ new Set();
1411
+ for (let i = 0; i < needsId.length; i++) {
1412
+ const heading = needsId[i];
1413
+ const newId = generator(heading, i);
1414
+ if (allGeneratedIds.has(newId)) throw new MarkformParseError(`Duplicate generated ID '${newId}' for heading '${heading.title}'`, { line: heading.line });
1415
+ if (onlyMissing && existingIds.has(newId)) throw new MarkformParseError(`Generated ID '${newId}' conflicts with existing ID`, { line: heading.line });
1416
+ allGeneratedIds.add(newId);
1417
+ generatedIds.set(heading.title, newId);
1418
+ }
1419
+ if (needsId.length === 0) return {
1420
+ markdown,
1421
+ injectedCount: 0,
1422
+ injectedIds: /* @__PURE__ */ new Map()
1423
+ };
1424
+ const lines = markdown.split("\n");
1425
+ const sortedByLine = [...needsId].sort((a, b) => b.line - a.line);
1426
+ for (const heading of sortedByLine) {
1427
+ const lineIndex = heading.line - 1;
1428
+ const line = lines[lineIndex];
1429
+ const newId = generatedIds.get(heading.title);
1430
+ let updatedLine = line;
1431
+ if (!onlyMissing || heading.id) {
1432
+ updatedLine = updatedLine.replace(MARKDOC_ID_PATTERN, "").trim();
1433
+ updatedLine = updatedLine.replace(HTML_COMMENT_ID_PATTERN, "").trim();
1434
+ }
1435
+ lines[lineIndex] = `${updatedLine} {% #${newId} %}`;
1436
+ }
1437
+ return {
1438
+ markdown: lines.join("\n"),
1439
+ injectedCount: needsId.length,
1440
+ injectedIds: generatedIds
1441
+ };
1442
+ }
1443
+
1162
1444
  //#endregion
1163
1445
  //#region src/engine/parse.ts
1164
1446
  /**
@@ -1180,7 +1462,8 @@ const VALID_FORM_TAGS = new Set([
1180
1462
  ]);
1181
1463
  /**
1182
1464
  * Parse harness configuration from frontmatter.
1183
- * Converts snake_case keys to camelCase.
1465
+ * YAML keys must be snake_case; they are mapped to camelCase internally.
1466
+ * Unrecognized keys produce a parse error.
1184
1467
  */
1185
1468
  function parseHarnessConfig(raw) {
1186
1469
  if (!raw || typeof raw !== "object") return;
@@ -1188,15 +1471,15 @@ function parseHarnessConfig(raw) {
1188
1471
  const result = {};
1189
1472
  const keyMap = {
1190
1473
  max_turns: "maxTurns",
1191
- maxTurns: "maxTurns",
1192
1474
  max_patches_per_turn: "maxPatchesPerTurn",
1193
- maxPatchesPerTurn: "maxPatchesPerTurn",
1194
1475
  max_issues_per_turn: "maxIssuesPerTurn",
1195
- maxIssuesPerTurn: "maxIssuesPerTurn"
1476
+ max_parallel_agents: "maxParallelAgents"
1196
1477
  };
1197
1478
  for (const [key, value] of Object.entries(config)) {
1198
1479
  const camelKey = keyMap[key];
1199
- if (camelKey && typeof value === "number") result[camelKey] = value;
1480
+ if (!camelKey) throw new MarkformParseError(`Unknown harness config key '${key}'. Valid keys: ${Object.keys(keyMap).join(", ")}`);
1481
+ if (typeof value !== "number") throw new MarkformParseError(`Harness config key '${key}' must be a number, got ${typeof value}`);
1482
+ result[camelKey] = value;
1200
1483
  }
1201
1484
  return Object.keys(result).length > 0 ? result : void 0;
1202
1485
  }
@@ -1237,6 +1520,100 @@ function extractFrontmatter(ast) {
1237
1520
  }
1238
1521
  }
1239
1522
  /**
1523
+ * Extract the raw source content minus frontmatter.
1524
+ * This is the content that will be used for splice-based serialization.
1525
+ */
1526
+ function extractRawSource(preprocessed) {
1527
+ const frontmatterMatch = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/.exec(preprocessed);
1528
+ if (frontmatterMatch) return preprocessed.slice(frontmatterMatch[0].length);
1529
+ return preprocessed;
1530
+ }
1531
+ /**
1532
+ * Build a line-to-offset mapping for converting Markdoc line numbers to byte offsets.
1533
+ * Returns an array where lineOffsets[lineNumber] = byte offset of line start.
1534
+ * Line numbers are 0-indexed (Markdoc uses 0-indexed lines).
1535
+ */
1536
+ function buildLineOffsets(source) {
1537
+ const offsets = [0];
1538
+ for (let i = 0; i < source.length; i++) if (source[i] === "\n") offsets.push(i + 1);
1539
+ return offsets;
1540
+ }
1541
+ /**
1542
+ * Extract tag region from a Markdoc AST node.
1543
+ * Uses the node's location property (line numbers) and converts to byte offsets.
1544
+ */
1545
+ function extractTagRegion(node, tagType, tagId, lineOffsets, frontmatterLineCount, sourceLength, hasValue) {
1546
+ const location = node.location;
1547
+ if (!location?.start || !location?.end) return null;
1548
+ const startLine = location.start.line - frontmatterLineCount;
1549
+ const endLine = location.end.line - frontmatterLineCount;
1550
+ if (startLine < 0 || endLine < 0) return null;
1551
+ const startOffset = lineOffsets[startLine] ?? 0;
1552
+ let endOffset;
1553
+ if (endLine + 1 < lineOffsets.length) endOffset = lineOffsets[endLine + 1] ?? sourceLength;
1554
+ else endOffset = sourceLength;
1555
+ if (startOffset >= endOffset || startOffset < 0 || endOffset > sourceLength) return null;
1556
+ return {
1557
+ tagId,
1558
+ tagType,
1559
+ startOffset,
1560
+ endOffset,
1561
+ ...hasValue !== void 0 && { includesValue: hasValue }
1562
+ };
1563
+ }
1564
+ /**
1565
+ * Collect all tag regions from the AST for content preservation.
1566
+ * Traverses the AST and extracts positions of all Markform tags.
1567
+ */
1568
+ function collectTagRegions(ast, rawSource, frontmatterLineCount, responsesByFieldId) {
1569
+ const regions = [];
1570
+ const lineOffsets = buildLineOffsets(rawSource);
1571
+ const sourceLength = rawSource.length;
1572
+ function traverse(node) {
1573
+ if (!node || typeof node !== "object") return;
1574
+ if (isTagNode(node, "form")) {
1575
+ const id = getStringAttr(node, "id");
1576
+ if (id) {
1577
+ const region = extractTagRegion(node, "form", id, lineOffsets, frontmatterLineCount, sourceLength);
1578
+ if (region) regions.push(region);
1579
+ }
1580
+ }
1581
+ if (isTagNode(node, "group")) {
1582
+ const id = getStringAttr(node, "id");
1583
+ if (id) {
1584
+ const region = extractTagRegion(node, "group", id, lineOffsets, frontmatterLineCount, sourceLength);
1585
+ if (region) regions.push(region);
1586
+ }
1587
+ }
1588
+ if (isTagNode(node, "field")) {
1589
+ const id = getStringAttr(node, "id");
1590
+ if (id) {
1591
+ const region = extractTagRegion(node, "field", id, lineOffsets, frontmatterLineCount, sourceLength, responsesByFieldId[id]?.value !== void 0);
1592
+ if (region) regions.push(region);
1593
+ }
1594
+ }
1595
+ if (isTagNode(node, "note")) {
1596
+ const id = getStringAttr(node, "id");
1597
+ if (id) {
1598
+ const region = extractTagRegion(node, "note", id, lineOffsets, frontmatterLineCount, sourceLength);
1599
+ if (region) regions.push(region);
1600
+ }
1601
+ }
1602
+ if (node.type === "tag" && node.tag && [
1603
+ "instructions",
1604
+ "description",
1605
+ "documentation"
1606
+ ].includes(node.tag)) {
1607
+ const region = extractTagRegion(node, "documentation", getStringAttr(node, "ref") ?? `doc_${regions.length}`, lineOffsets, frontmatterLineCount, sourceLength);
1608
+ if (region) regions.push(region);
1609
+ }
1610
+ if (node.children && Array.isArray(node.children)) for (const child of node.children) traverse(child);
1611
+ }
1612
+ traverse(ast);
1613
+ regions.sort((a, b) => a.startOffset - b.startOffset);
1614
+ return regions;
1615
+ }
1616
+ /**
1240
1617
  * Parse a group tag.
1241
1618
  */
1242
1619
  function parseFieldGroup(node, responsesByFieldId, orderIndex, idIndex, parentId) {
@@ -1275,19 +1652,31 @@ function parseFieldGroup(node, responsesByFieldId, orderIndex, idIndex, parentId
1275
1652
  if (child.children && Array.isArray(child.children)) for (const c of child.children) processChildren(c);
1276
1653
  }
1277
1654
  if (node.children && Array.isArray(node.children)) for (const child of node.children) processChildren(child);
1655
+ const parallel = getStringAttr(node, "parallel");
1656
+ const order = getNumberAttr(node, "order");
1657
+ const groupEffectiveOrder = order ?? 0;
1658
+ for (const child of children) {
1659
+ if (child.parallel) throw new MarkformParseError(`Field '${child.id}' has parallel='${child.parallel}' but is inside group '${id}'. The parallel attribute is only allowed on top-level fields and groups.`);
1660
+ if (child.order !== void 0) {
1661
+ if (child.order !== groupEffectiveOrder) throw new MarkformParseError(`Field '${child.id}' has order=${child.order} but is inside group '${id}' with order=${groupEffectiveOrder}. A field inside a group must not specify a different order.`);
1662
+ }
1663
+ }
1278
1664
  return {
1279
1665
  id,
1280
1666
  title,
1281
1667
  validate: getValidateAttr(node),
1282
1668
  children,
1283
- report: getBooleanAttr(node, "report")
1669
+ report: getBooleanAttr(node, "report"),
1670
+ parallel,
1671
+ order
1284
1672
  };
1285
1673
  }
1286
1674
  /**
1287
1675
  * Parse a form tag.
1288
1676
  * Handles both explicit groups and fields placed directly under the form.
1677
+ * Also handles implicit checkboxes when form has no explicit fields.
1289
1678
  */
1290
- function parseFormTag(node, responsesByFieldId, orderIndex, idIndex) {
1679
+ function parseFormTag(node, responsesByFieldId, orderIndex, idIndex, markdown) {
1291
1680
  const id = getStringAttr(node, "id");
1292
1681
  const title = getStringAttr(node, "title");
1293
1682
  if (!id) throw new MarkformParseError("form missing required 'id' attribute");
@@ -1328,18 +1717,89 @@ function parseFormTag(node, responsesByFieldId, orderIndex, idIndex) {
1328
1717
  }
1329
1718
  if (node.children && Array.isArray(node.children)) for (const child of node.children) processContent(child);
1330
1719
  if (ungroupedFields.length > 0) {
1331
- const implicitGroupId = `_default`;
1332
- if (idIndex.has(implicitGroupId)) throw new MarkformParseError(`ID '${implicitGroupId}' is reserved for implicit field groups. Please use a different ID for your field or group.`);
1333
- idIndex.set(implicitGroupId, {
1334
- nodeType: "group",
1720
+ const implicitGroupId = "default";
1721
+ const existingDefault = groups.find((g) => g.id === implicitGroupId);
1722
+ if (existingDefault) existingDefault.children = [...existingDefault.children ?? [], ...ungroupedFields];
1723
+ else {
1724
+ idIndex.set(implicitGroupId, {
1725
+ nodeType: "group",
1726
+ parentId: id
1727
+ });
1728
+ groups.push({
1729
+ id: implicitGroupId,
1730
+ children: ungroupedFields,
1731
+ implicit: true
1732
+ });
1733
+ }
1734
+ }
1735
+ const hasExplicitFields = ungroupedFields.length > 0 || groups.some((g) => g.children && g.children.length > 0);
1736
+ const hasExplicitCheckboxStyleFields = groups.some((g) => g.children?.some((f) => f.kind === "checkboxes" || f.kind === "single_select" || f.kind === "multi_select"));
1737
+ const allCheckboxes = findAllCheckboxes(markdown);
1738
+ if (allCheckboxes.length > 0) if (hasExplicitFields) {
1739
+ if (!hasExplicitCheckboxStyleFields) throw new MarkformParseError("Checkboxes found outside of field tags. Either wrap all checkboxes in fields or remove all explicit fields for implicit checkboxes mode.");
1740
+ } else {
1741
+ const seenIds = /* @__PURE__ */ new Set();
1742
+ const options = [];
1743
+ const values = {};
1744
+ for (const checkbox of allCheckboxes) {
1745
+ if (!checkbox.id) throw new MarkformParseError(`Option in implicit field 'checkboxes' missing ID annotation. Use {% #option_id %}`, { line: checkbox.line });
1746
+ if (seenIds.has(checkbox.id)) throw new MarkformParseError(`Duplicate option ID '${checkbox.id}' in field 'checkboxes'`, { line: checkbox.line });
1747
+ seenIds.add(checkbox.id);
1748
+ options.push({
1749
+ id: checkbox.id,
1750
+ label: checkbox.label
1751
+ });
1752
+ values[checkbox.id] = checkbox.state;
1753
+ }
1754
+ const implicitField = {
1755
+ kind: "checkboxes",
1756
+ id: "checkboxes",
1757
+ label: "Checkboxes",
1758
+ checkboxMode: "multi",
1759
+ implicit: true,
1760
+ options,
1761
+ required: false,
1762
+ priority: DEFAULT_PRIORITY,
1763
+ role: AGENT_ROLE,
1764
+ approvalMode: "none"
1765
+ };
1766
+ idIndex.set("checkboxes", {
1767
+ nodeType: "field",
1335
1768
  parentId: id
1336
1769
  });
1337
- groups.push({
1338
- id: implicitGroupId,
1339
- children: ungroupedFields,
1340
- implicit: true
1341
- });
1770
+ orderIndex.push("checkboxes");
1771
+ for (const opt of options) {
1772
+ const qualifiedRef = `checkboxes.${opt.id}`;
1773
+ idIndex.set(qualifiedRef, {
1774
+ nodeType: "option",
1775
+ parentId: id,
1776
+ fieldId: "checkboxes"
1777
+ });
1778
+ }
1779
+ responsesByFieldId.checkboxes = {
1780
+ state: "answered",
1781
+ value: {
1782
+ kind: "checkboxes",
1783
+ values
1784
+ }
1785
+ };
1786
+ let defaultGroup = groups.find((g) => g.id === "default");
1787
+ if (!defaultGroup) {
1788
+ defaultGroup = {
1789
+ id: "default",
1790
+ children: [],
1791
+ implicit: true
1792
+ };
1793
+ idIndex.set("default", {
1794
+ nodeType: "group",
1795
+ parentId: id
1796
+ });
1797
+ groups.push(defaultGroup);
1798
+ }
1799
+ defaultGroup.children = defaultGroup.children || [];
1800
+ defaultGroup.children.push(implicitField);
1342
1801
  }
1802
+ validateParallelBatches(groups);
1343
1803
  return {
1344
1804
  id,
1345
1805
  title,
@@ -1347,6 +1807,39 @@ function parseFormTag(node, responsesByFieldId, orderIndex, idIndex) {
1347
1807
  };
1348
1808
  }
1349
1809
  /**
1810
+ * Validate that parallel batches have consistent order and role values.
1811
+ */
1812
+ function validateParallelBatches(groups) {
1813
+ const batches = /* @__PURE__ */ new Map();
1814
+ for (const group of groups) if (group.implicit) {
1815
+ for (const field of group.children) if (field.parallel) {
1816
+ const list = batches.get(field.parallel) ?? [];
1817
+ list.push({
1818
+ order: field.order ?? 0,
1819
+ role: field.role,
1820
+ itemId: field.id
1821
+ });
1822
+ batches.set(field.parallel, list);
1823
+ }
1824
+ } else if (group.parallel) {
1825
+ const groupRole = group.children[0]?.role ?? AGENT_ROLE;
1826
+ const list = batches.get(group.parallel) ?? [];
1827
+ list.push({
1828
+ order: group.order ?? 0,
1829
+ role: groupRole,
1830
+ itemId: group.id
1831
+ });
1832
+ batches.set(group.parallel, list);
1833
+ }
1834
+ for (const [batchId, items] of batches) {
1835
+ if (items.length < 2) continue;
1836
+ const firstOrder = items[0].order;
1837
+ const firstRole = items[0].role;
1838
+ if (items.filter((i) => i.order !== firstOrder).length > 0) throw new MarkformParseError(`Parallel batch '${batchId}' has items with different order values (${[...new Set(items.map((i) => i.order))].join(", ")}). All items in a parallel batch must have the same order.`);
1839
+ if (items.filter((i) => i.role !== firstRole).length > 0) throw new MarkformParseError(`Parallel batch '${batchId}' has items with different roles (${[...new Set(items.map((i) => i.role))].join(", ")}). All items in a parallel batch must have the same role.`);
1840
+ }
1841
+ }
1842
+ /**
1350
1843
  * Extract all notes from AST.
1351
1844
  * Looks for {% note %} tags with id, ref, role, and optional state.
1352
1845
  */
@@ -1441,6 +1934,8 @@ function extractDocBlocks(ast, idIndex) {
1441
1934
  function parseForm(markdown) {
1442
1935
  const syntaxStyle = detectSyntaxStyle(markdown);
1443
1936
  const preprocessed = preprocessCommentSyntax(markdown);
1937
+ const rawSource = extractRawSource(preprocessed);
1938
+ const frontmatterLineCount = (preprocessed.slice(0, preprocessed.length - rawSource.length).match(/\n/g) ?? []).length;
1444
1939
  const ast = Markdoc.parse(preprocessed);
1445
1940
  const { metadata, description } = extractFrontmatter(ast);
1446
1941
  let formSchema = null;
@@ -1451,25 +1946,31 @@ function parseForm(markdown) {
1451
1946
  if (!node || typeof node !== "object") return;
1452
1947
  if (isTagNode(node, "form")) {
1453
1948
  if (formSchema) throw new MarkformParseError("Multiple form tags found - only one allowed");
1454
- formSchema = parseFormTag(node, responsesByFieldId, orderIndex, idIndex);
1949
+ formSchema = parseFormTag(node, responsesByFieldId, orderIndex, idIndex, preprocessed);
1455
1950
  return;
1456
1951
  }
1457
1952
  if (node.children && Array.isArray(node.children)) for (const child of node.children) findFormTag(child);
1458
1953
  }
1459
1954
  findFormTag(ast);
1460
1955
  if (!formSchema) throw new MarkformParseError("No form tag found in document");
1956
+ const schema = {
1957
+ ...formSchema,
1958
+ ...description && { description }
1959
+ };
1960
+ const notes = extractNotes(ast, idIndex);
1961
+ const docs = extractDocBlocks(ast, idIndex);
1962
+ const tagRegions = collectTagRegions(ast, rawSource, frontmatterLineCount, responsesByFieldId);
1461
1963
  return {
1462
- schema: {
1463
- ...formSchema,
1464
- ...description && { description }
1465
- },
1964
+ schema,
1466
1965
  responsesByFieldId,
1467
- notes: extractNotes(ast, idIndex),
1468
- docs: extractDocBlocks(ast, idIndex),
1966
+ notes,
1967
+ docs,
1469
1968
  orderIndex,
1470
1969
  idIndex,
1471
1970
  ...metadata && { metadata },
1472
- syntaxStyle
1971
+ syntaxStyle,
1972
+ rawSource,
1973
+ tagRegions
1473
1974
  };
1474
1975
  }
1475
1976
 
@@ -2590,7 +3091,8 @@ var FormHarness = class {
2590
3091
  * Applies issue filtering and computes step budget.
2591
3092
  */
2592
3093
  computeStepResult(result) {
2593
- const limitedIssues = this.filterIssuesByScope(result.issues).slice(0, this.config.maxIssuesPerTurn);
3094
+ const orderFiltered = this.filterIssuesByOrder(result.issues);
3095
+ const limitedIssues = this.filterIssuesByScope(orderFiltered).slice(0, this.config.maxIssuesPerTurn);
2594
3096
  const stepBudget = Math.min(this.config.maxPatchesPerTurn, limitedIssues.length);
2595
3097
  return {
2596
3098
  structureSummary: result.structureSummary,
@@ -2681,6 +3183,36 @@ var FormHarness = class {
2681
3183
  return result;
2682
3184
  }
2683
3185
  /**
3186
+ * Filter issues by order level.
3187
+ *
3188
+ * Only includes issues for fields at the current (lowest incomplete) order level.
3189
+ * Fields at higher order levels are deferred until all lower-order fields are complete.
3190
+ * If no order attributes are used, all issues pass through (all at order 0).
3191
+ */
3192
+ filterIssuesByOrder(issues) {
3193
+ const fieldOrderMap = /* @__PURE__ */ new Map();
3194
+ for (const group of this.form.schema.groups) {
3195
+ const groupOrder = group.order ?? 0;
3196
+ for (const field of group.children) fieldOrderMap.set(field.id, field.order ?? groupOrder);
3197
+ }
3198
+ const openOrderLevels = /* @__PURE__ */ new Set();
3199
+ for (const issue of issues) {
3200
+ const fieldId = this.getFieldIdFromRef(issue.ref, issue.scope);
3201
+ if (fieldId) {
3202
+ const order = fieldOrderMap.get(fieldId) ?? 0;
3203
+ openOrderLevels.add(order);
3204
+ } else if (issue.scope === "form") openOrderLevels.add(0);
3205
+ }
3206
+ if (openOrderLevels.size <= 1) return issues;
3207
+ const currentOrder = Math.min(...openOrderLevels);
3208
+ return issues.filter((issue) => {
3209
+ if (issue.scope === "form") return true;
3210
+ const fieldId = this.getFieldIdFromRef(issue.ref, issue.scope);
3211
+ if (!fieldId) return true;
3212
+ return (fieldOrderMap.get(fieldId) ?? 0) === currentOrder;
3213
+ });
3214
+ }
3215
+ /**
2684
3216
  * Extract field ID from an issue ref.
2685
3217
  */
2686
3218
  getFieldIdFromRef(ref, scope) {
@@ -2885,6 +3417,62 @@ function createMockAgent(completedForm) {
2885
3417
  return new MockAgent(completedForm);
2886
3418
  }
2887
3419
 
3420
+ //#endregion
3421
+ //#region src/engine/executionPlan.ts
3422
+ /**
3423
+ * Compute an execution plan from a parsed form.
3424
+ *
3425
+ * Walks top-level items (groups, or individual fields in implicit groups)
3426
+ * in document order and partitions them by their `parallel` attribute:
3427
+ * - Items without `parallel` go to the loose-serial pool.
3428
+ * - Items with the same `parallel` value form a parallel batch.
3429
+ *
3430
+ * Also computes distinct order levels across all items.
3431
+ */
3432
+ function computeExecutionPlan(form) {
3433
+ const looseSerial = [];
3434
+ const batchMap = /* @__PURE__ */ new Map();
3435
+ const orderSet = /* @__PURE__ */ new Set();
3436
+ for (const group of form.schema.groups) if (group.implicit) for (const field of group.children) {
3437
+ const order = field.order ?? 0;
3438
+ orderSet.add(order);
3439
+ const item = {
3440
+ itemId: field.id,
3441
+ itemType: "field",
3442
+ order
3443
+ };
3444
+ if (field.parallel) {
3445
+ const list = batchMap.get(field.parallel) ?? [];
3446
+ list.push(item);
3447
+ batchMap.set(field.parallel, list);
3448
+ } else looseSerial.push(item);
3449
+ }
3450
+ else {
3451
+ const order = group.order ?? 0;
3452
+ orderSet.add(order);
3453
+ const item = {
3454
+ itemId: group.id,
3455
+ itemType: "group",
3456
+ order
3457
+ };
3458
+ if (group.parallel) {
3459
+ const list = batchMap.get(group.parallel) ?? [];
3460
+ list.push(item);
3461
+ batchMap.set(group.parallel, list);
3462
+ } else looseSerial.push(item);
3463
+ }
3464
+ const parallelBatches = [];
3465
+ for (const [batchId, items] of batchMap) parallelBatches.push({
3466
+ batchId,
3467
+ items
3468
+ });
3469
+ return {
3470
+ looseSerial,
3471
+ parallelBatches,
3472
+ orderLevels: [...orderSet].sort((a, b) => a - b)
3473
+ };
3474
+ }
3475
+
2888
3476
  //#endregion
2889
3477
  //#region ../../node_modules/.pnpm/@ai-sdk+provider@3.0.0/node_modules/@ai-sdk/provider/dist/index.mjs
2890
3478
  var marker$1 = "vercel.ai.error";
@@ -8671,6 +9259,185 @@ function getProviderInfo(provider) {
8671
9259
  };
8672
9260
  }
8673
9261
 
9262
+ //#endregion
9263
+ //#region src/harness/parallelHarness.ts
9264
+ /**
9265
+ * Parallel Harness - Orchestrates concurrent agent execution for parallel form filling.
9266
+ *
9267
+ * Uses the execution plan to identify parallel batches and order levels,
9268
+ * spawns concurrent agents for batch items, merges patches, and applies them.
9269
+ */
9270
+ /**
9271
+ * Get field IDs that belong to an execution plan item.
9272
+ */
9273
+ function getFieldIdsForItem(form, item) {
9274
+ if (item.itemType === "field") return [item.itemId];
9275
+ const group = form.schema.groups.find((g) => g.id === item.itemId);
9276
+ if (!group) return [];
9277
+ return group.children.map((f) => f.id);
9278
+ }
9279
+ /**
9280
+ * Filter issues to only those relevant to an execution plan item's fields.
9281
+ * Form-scoped issues are excluded (they don't belong to individual agents).
9282
+ */
9283
+ function scopeIssuesForItem(form, item, allIssues) {
9284
+ const targetFieldIds = new Set(getFieldIdsForItem(form, item));
9285
+ return allIssues.filter((issue) => {
9286
+ if (issue.scope === "form") return false;
9287
+ if (issue.scope === "field") return targetFieldIds.has(issue.ref);
9288
+ if (issue.scope === "option") {
9289
+ const dotIndex = issue.ref.indexOf(".");
9290
+ const fieldId = dotIndex > 0 ? issue.ref.slice(0, dotIndex) : issue.ref;
9291
+ return targetFieldIds.has(fieldId);
9292
+ }
9293
+ if (issue.scope === "cell") {
9294
+ const dotIndex = issue.ref.indexOf(".");
9295
+ const fieldId = dotIndex > 0 ? issue.ref.slice(0, dotIndex) : issue.ref;
9296
+ return targetFieldIds.has(fieldId);
9297
+ }
9298
+ return false;
9299
+ });
9300
+ }
9301
+ /**
9302
+ * Parallel harness that orchestrates concurrent agent execution.
9303
+ */
9304
+ var ParallelHarness = class {
9305
+ form;
9306
+ plan;
9307
+ config;
9308
+ constructor(form, config = {}) {
9309
+ this.form = form;
9310
+ this.plan = computeExecutionPlan(form);
9311
+ this.config = config;
9312
+ }
9313
+ /**
9314
+ * Get the execution plan.
9315
+ */
9316
+ getExecutionPlan() {
9317
+ return this.plan;
9318
+ }
9319
+ /**
9320
+ * Run a single order level: execute loose serial items with the primary agent,
9321
+ * and parallel batch items concurrently.
9322
+ */
9323
+ async runOrderLevel(order, primaryAgent) {
9324
+ const maxPatches = this.config.maxPatchesPerTurn ?? 20;
9325
+ let totalPatchesApplied = 0;
9326
+ const errors = [];
9327
+ const allIssues = inspect(this.form).issues;
9328
+ const looseItems = this.plan.looseSerial.filter((i) => i.order === order);
9329
+ for (const item of looseItems) {
9330
+ const scopedIssues = scopeIssuesForItem(this.form, item, allIssues);
9331
+ if (scopedIssues.length === 0) continue;
9332
+ try {
9333
+ const response = await primaryAgent.fillFormTool(scopedIssues, this.form, maxPatches);
9334
+ if (response.patches.length > 0) {
9335
+ const result = applyPatches(this.form, response.patches);
9336
+ totalPatchesApplied += result.appliedPatches.length;
9337
+ }
9338
+ } catch (err) {
9339
+ errors.push(`Loose serial item ${item.itemId}: ${String(err)}`);
9340
+ }
9341
+ }
9342
+ for (const batch of this.plan.parallelBatches) {
9343
+ const batchItems = batch.items.filter((i) => i.order === order);
9344
+ if (batchItems.length === 0) continue;
9345
+ this.config.onBatchStart?.(batch.batchId);
9346
+ const batchIssues = inspect(this.form).issues;
9347
+ const maxConcurrent = this.config.maxParallelAgents ?? DEFAULT_MAX_PARALLEL_AGENTS;
9348
+ const agentPromises = [];
9349
+ const itemFieldIds = /* @__PURE__ */ new Map();
9350
+ for (let i = 0; i < batchItems.length; i++) {
9351
+ const item = batchItems[i];
9352
+ const scopedIssues = scopeIssuesForItem(this.form, item, batchIssues);
9353
+ const targetFieldIds = getFieldIdsForItem(this.form, item);
9354
+ itemFieldIds.set(i, targetFieldIds);
9355
+ if (scopedIssues.length === 0) {
9356
+ agentPromises.push(Promise.resolve({ patches: [] }));
9357
+ continue;
9358
+ }
9359
+ const request = {
9360
+ form: this.form,
9361
+ targetFieldIds,
9362
+ targetGroupIds: item.itemType === "group" ? [item.itemId] : [],
9363
+ issues: scopedIssues
9364
+ };
9365
+ const agent = this.config.agentFactory ? this.config.agentFactory(request) : primaryAgent;
9366
+ agentPromises.push(agent.fillFormTool(scopedIssues, this.form, maxPatches));
9367
+ }
9368
+ const results = await runWithConcurrency(agentPromises, maxConcurrent);
9369
+ const allPatches = [];
9370
+ for (const result of results) if (result.status === "fulfilled") allPatches.push(...result.value.patches);
9371
+ else errors.push(`Parallel agent error: ${result.reason}`);
9372
+ if (allPatches.length > 0) {
9373
+ const applyResult = applyPatches(this.form, allPatches);
9374
+ totalPatchesApplied += applyResult.appliedPatches.length;
9375
+ }
9376
+ this.config.onBatchComplete?.(batch.batchId);
9377
+ }
9378
+ return {
9379
+ patchesApplied: totalPatchesApplied,
9380
+ errors
9381
+ };
9382
+ }
9383
+ /**
9384
+ * Run all order levels sequentially, filling the entire form.
9385
+ */
9386
+ async runAll(primaryAgent) {
9387
+ let totalPatchesApplied = 0;
9388
+ const orderLevelsProcessed = [];
9389
+ const allErrors = [];
9390
+ for (const order of this.plan.orderLevels) {
9391
+ this.config.onOrderLevelStart?.(order);
9392
+ const result = await this.runOrderLevel(order, primaryAgent);
9393
+ totalPatchesApplied += result.patchesApplied;
9394
+ orderLevelsProcessed.push(order);
9395
+ allErrors.push(...result.errors);
9396
+ this.config.onOrderLevelComplete?.(order);
9397
+ }
9398
+ return {
9399
+ isComplete: inspect(this.form).isComplete,
9400
+ totalPatchesApplied,
9401
+ orderLevelsProcessed,
9402
+ errors: allErrors
9403
+ };
9404
+ }
9405
+ };
9406
+ /**
9407
+ * Run promises with a concurrency limit.
9408
+ * Returns results in the same order as input.
9409
+ */
9410
+ async function runWithConcurrency(promises, maxConcurrent) {
9411
+ if (maxConcurrent >= promises.length) return Promise.allSettled(promises);
9412
+ const results = new Array(promises.length);
9413
+ let nextIndex = 0;
9414
+ async function runNext() {
9415
+ while (nextIndex < promises.length) {
9416
+ const idx = nextIndex++;
9417
+ try {
9418
+ results[idx] = {
9419
+ status: "fulfilled",
9420
+ value: await promises[idx]
9421
+ };
9422
+ } catch (reason) {
9423
+ results[idx] = {
9424
+ status: "rejected",
9425
+ reason
9426
+ };
9427
+ }
9428
+ }
9429
+ }
9430
+ const workers = Array.from({ length: Math.min(maxConcurrent, promises.length) }, () => runNext());
9431
+ await Promise.all(workers);
9432
+ return results;
9433
+ }
9434
+ /**
9435
+ * Create a parallel harness for the given form.
9436
+ */
9437
+ function createParallelHarness(form, config) {
9438
+ return new ParallelHarness(form, config);
9439
+ }
9440
+
8674
9441
  //#endregion
8675
9442
  //#region src/harness/programmaticFill.ts
8676
9443
  function buildErrorResult(form, errors, warnings) {
@@ -8798,6 +9565,9 @@ async function fillForm(options) {
8798
9565
  }
8799
9566
  inputContextWarnings = coercionResult.warnings;
8800
9567
  }
9568
+ if (options.enableParallel) {
9569
+ if (computeExecutionPlan(form).parallelBatches.length > 0) return fillFormParallel(form, model, provider, options, totalPatches, inputContextWarnings);
9570
+ }
8801
9571
  const maxTurnsTotal = options.maxTurnsTotal ?? DEFAULT_MAX_TURNS;
8802
9572
  const startingTurnNumber = options.startingTurnNumber ?? 0;
8803
9573
  const maxPatchesPerTurn = options.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN;
@@ -8903,6 +9673,169 @@ async function fillForm(options) {
8903
9673
  message: `Reached maximum total turns (${maxTurnsTotal})`
8904
9674
  }, inputContextWarnings, stepResult.issues);
8905
9675
  }
9676
+ /**
9677
+ * Fill a form using parallel execution.
9678
+ *
9679
+ * For each order level, runs serial items with the primary agent (multi-turn),
9680
+ * then runs parallel batch items concurrently (multi-turn per agent).
9681
+ */
9682
+ async function fillFormParallel(form, model, provider, options, initialPatches, inputContextWarnings) {
9683
+ const plan = computeExecutionPlan(form);
9684
+ const maxTurnsTotal = options.maxTurnsTotal ?? DEFAULT_MAX_TURNS;
9685
+ const startingTurnNumber = options.startingTurnNumber ?? 0;
9686
+ const maxPatchesPerTurn = options.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN;
9687
+ const maxIssuesPerTurn = options.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN;
9688
+ const maxParallelAgents = options.maxParallelAgents ?? DEFAULT_MAX_PARALLEL_AGENTS;
9689
+ const targetRoles = options.targetRoles ?? [AGENT_ROLE];
9690
+ let totalPatches = initialPatches;
9691
+ let turnCount = startingTurnNumber;
9692
+ const primaryAgent = options._testAgent ?? createLiveAgent({
9693
+ model,
9694
+ systemPromptAddition: options.systemPromptAddition,
9695
+ targetRole: targetRoles[0] ?? AGENT_ROLE,
9696
+ provider,
9697
+ enableWebSearch: options.enableWebSearch,
9698
+ additionalTools: options.additionalTools,
9699
+ callbacks: options.callbacks,
9700
+ maxStepsPerTurn: options.maxStepsPerTurn
9701
+ });
9702
+ for (const order of plan.orderLevels) {
9703
+ if (options.signal?.aborted) return buildResult(form, turnCount, totalPatches, {
9704
+ ok: false,
9705
+ reason: "cancelled"
9706
+ }, inputContextWarnings);
9707
+ if (turnCount >= maxTurnsTotal) return buildResult(form, turnCount, totalPatches, {
9708
+ ok: false,
9709
+ reason: "max_turns",
9710
+ message: `Reached maximum total turns (${maxTurnsTotal})`
9711
+ }, inputContextWarnings);
9712
+ try {
9713
+ options.callbacks?.onOrderLevelStart?.({ order });
9714
+ } catch {}
9715
+ const serialItems = plan.looseSerial.filter((i) => i.order === order);
9716
+ if (serialItems.length > 0) {
9717
+ const result = await runMultiTurnForItems(form, primaryAgent, serialItems, targetRoles, maxPatchesPerTurn, maxIssuesPerTurn, maxTurnsTotal, turnCount, options);
9718
+ totalPatches += result.patchesApplied;
9719
+ turnCount += result.turnsUsed;
9720
+ if (result.aborted) return buildResult(form, turnCount, totalPatches, result.status, inputContextWarnings);
9721
+ }
9722
+ for (const batch of plan.parallelBatches) {
9723
+ const batchItems = batch.items.filter((i) => i.order === order);
9724
+ if (batchItems.length === 0) continue;
9725
+ try {
9726
+ options.callbacks?.onBatchStart?.({
9727
+ batchId: batch.batchId,
9728
+ itemCount: batchItems.length
9729
+ });
9730
+ } catch {}
9731
+ const results = await runWithConcurrency(batchItems.map((item) => {
9732
+ return runMultiTurnForItems(form, options._testAgent ?? createLiveAgent({
9733
+ model,
9734
+ systemPromptAddition: options.systemPromptAddition,
9735
+ targetRole: targetRoles[0] ?? AGENT_ROLE,
9736
+ provider,
9737
+ enableWebSearch: options.enableWebSearch,
9738
+ additionalTools: options.additionalTools,
9739
+ callbacks: options.callbacks,
9740
+ maxStepsPerTurn: options.maxStepsPerTurn
9741
+ }), [item], targetRoles, maxPatchesPerTurn, maxIssuesPerTurn, maxTurnsTotal, turnCount, options);
9742
+ }).map((p) => p), maxParallelAgents);
9743
+ let batchPatches = 0;
9744
+ for (const result of results) if (result.status === "fulfilled") {
9745
+ totalPatches += result.value.patchesApplied;
9746
+ batchPatches += result.value.patchesApplied;
9747
+ turnCount += result.value.turnsUsed;
9748
+ }
9749
+ try {
9750
+ options.callbacks?.onBatchComplete?.({
9751
+ batchId: batch.batchId,
9752
+ patchesApplied: batchPatches
9753
+ });
9754
+ } catch {}
9755
+ }
9756
+ const levelInspect = inspect(form);
9757
+ try {
9758
+ options.callbacks?.onOrderLevelComplete?.({
9759
+ order,
9760
+ patchesApplied: totalPatches
9761
+ });
9762
+ } catch {}
9763
+ if (levelInspect.isComplete) return buildResult(form, turnCount, totalPatches, { ok: true }, inputContextWarnings);
9764
+ }
9765
+ const finalInspect = inspect(form);
9766
+ if (finalInspect.isComplete) return buildResult(form, turnCount, totalPatches, { ok: true }, inputContextWarnings);
9767
+ return buildResult(form, turnCount, totalPatches, {
9768
+ ok: false,
9769
+ reason: "max_turns",
9770
+ message: `Reached maximum total turns (${maxTurnsTotal})`
9771
+ }, inputContextWarnings, finalInspect.issues);
9772
+ }
9773
+ /**
9774
+ * Run a multi-turn loop for a set of execution plan items.
9775
+ * Scoped issues are filtered to only the target items' fields.
9776
+ * Retries with rejection feedback, same as the serial fillForm path.
9777
+ */
9778
+ async function runMultiTurnForItems(form, agent, items, targetRoles, maxPatchesPerTurn, _maxIssuesPerTurn, maxTurnsTotal, startTurn, options) {
9779
+ let turnsUsed = 0;
9780
+ let patchesApplied = 0;
9781
+ let previousRejections;
9782
+ const maxTurnsForItems = Math.min(maxTurnsTotal - startTurn, options.maxTurnsThisCall ?? Infinity);
9783
+ for (let turn = 0; turn < maxTurnsForItems; turn++) {
9784
+ if (options.signal?.aborted) return {
9785
+ patchesApplied,
9786
+ turnsUsed,
9787
+ aborted: true,
9788
+ status: {
9789
+ ok: false,
9790
+ reason: "cancelled"
9791
+ }
9792
+ };
9793
+ const allIssues = inspect(form, { targetRoles }).issues;
9794
+ let scopedIssues = [];
9795
+ for (const item of items) scopedIssues.push(...scopeIssuesForItem(form, item, allIssues));
9796
+ const seen = /* @__PURE__ */ new Set();
9797
+ scopedIssues = scopedIssues.filter((issue) => {
9798
+ const key = `${issue.scope}:${issue.ref}:${issue.message}`;
9799
+ if (seen.has(key)) return false;
9800
+ seen.add(key);
9801
+ return true;
9802
+ });
9803
+ if (scopedIssues.length === 0) break;
9804
+ try {
9805
+ options.callbacks?.onTurnStart?.({
9806
+ turnNumber: startTurn + turnsUsed + 1,
9807
+ issuesCount: scopedIssues.length
9808
+ });
9809
+ } catch {}
9810
+ const response = await agent.fillFormTool(scopedIssues, form, maxPatchesPerTurn, previousRejections);
9811
+ if (response.patches.length > 0) {
9812
+ const applyResult = applyPatches(form, response.patches);
9813
+ patchesApplied += applyResult.appliedPatches.length;
9814
+ previousRejections = applyResult.rejectedPatches;
9815
+ } else previousRejections = void 0;
9816
+ turnsUsed++;
9817
+ try {
9818
+ const postInspect = inspect(form, { targetRoles });
9819
+ const requiredIssues = postInspect.issues.filter((i) => i.severity === "required");
9820
+ options.callbacks?.onTurnComplete?.({
9821
+ turnNumber: startTurn + turnsUsed,
9822
+ issuesShown: scopedIssues.length,
9823
+ patchesApplied: response.patches.length,
9824
+ requiredIssuesRemaining: requiredIssues.length,
9825
+ isComplete: postInspect.isComplete,
9826
+ stats: response.stats,
9827
+ issues: scopedIssues,
9828
+ patches: response.patches,
9829
+ rejectedPatches: previousRejections ?? []
9830
+ });
9831
+ } catch {}
9832
+ }
9833
+ return {
9834
+ patchesApplied,
9835
+ turnsUsed,
9836
+ aborted: false
9837
+ };
9838
+ }
8906
9839
 
8907
9840
  //#endregion
8908
9841
  //#region src/harness/harnessConfigResolver.ts
@@ -8924,6 +9857,7 @@ function resolveHarnessConfig(form, options) {
8924
9857
  maxTurns: options?.maxTurnsTotal ?? frontmatterConfig?.maxTurns ?? DEFAULT_MAX_TURNS,
8925
9858
  maxPatchesPerTurn: options?.maxPatchesPerTurn ?? frontmatterConfig?.maxPatchesPerTurn ?? DEFAULT_MAX_PATCHES_PER_TURN,
8926
9859
  maxIssuesPerTurn: options?.maxIssuesPerTurn ?? frontmatterConfig?.maxIssuesPerTurn ?? DEFAULT_MAX_ISSUES_PER_TURN,
9860
+ maxParallelAgents: options?.maxParallelAgents ?? frontmatterConfig?.maxParallelAgents ?? DEFAULT_MAX_PARALLEL_AGENTS,
8927
9861
  targetRoles: options?.targetRoles,
8928
9862
  fillMode: options?.fillMode
8929
9863
  };
@@ -9048,8 +9982,8 @@ function validateResearchForm(form) {
9048
9982
  //#endregion
9049
9983
  //#region src/index.ts
9050
9984
  /** Markform version (injected at build time). */
9051
- const VERSION = "0.1.16";
9985
+ const VERSION = "0.1.18";
9052
9986
 
9053
9987
  //#endregion
9054
- export { parseRawTable as A, parseScopeRef as C, parseForm as D, formToJsonSchema as E, parseCellValue as O, isQualifiedRef as S, fieldToJsonSchema as T, coerceToFieldPatch as _, resolveHarnessConfig as a, isCellRef as b, getProviderNames as c, createLiveAgent as d, MockAgent as f, coerceInputContext as g, createHarness as h, runResearch as i, parseMarkdownTable as k, resolveModel as l, FormHarness as m, isResearchForm as n, fillForm as o, createMockAgent as p, validateResearchForm as r, getProviderInfo as s, VERSION as t, buildMockWireFormat as u, findFieldById as v, serializeScopeRef as w, isFieldRef as x, getFieldId as y };
9055
- //# sourceMappingURL=src-Dv3IZSQU.mjs.map
9988
+ export { formToJsonSchema as A, getFieldId as C, parseScopeRef as D, isQualifiedRef as E, findAllHeadings as F, findEnclosingHeadings as I, parseCellValue as L, findAllCheckboxes as M, injectCheckboxIds as N, serializeScopeRef as O, injectHeaderIds as P, parseMarkdownTable as R, findFieldById as S, isFieldRef as T, createMockAgent as _, resolveHarnessConfig as a, coerceInputContext as b, createParallelHarness as c, getProviderNames as d, resolveModel as f, MockAgent as g, computeExecutionPlan as h, runResearch as i, parseForm as j, fieldToJsonSchema as k, scopeIssuesForItem as l, createLiveAgent as m, isResearchForm as n, fillForm as o, buildMockWireFormat as p, validateResearchForm as r, ParallelHarness as s, VERSION as t, getProviderInfo as u, FormHarness as v, isCellRef as w, coerceToFieldPatch as x, createHarness as y, parseRawTable as z };
9989
+ //# sourceMappingURL=src-DDxi-2ne.mjs.map