syntaur 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -317,7 +317,7 @@ function getTargetStatus(_from, command, table) {
317
317
  if (!table) {
318
318
  return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
319
319
  }
320
- return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
320
+ return table.get(`${_from}:${command}`) ?? table.get(command) ?? null;
321
321
  }
322
322
  var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
323
323
  var init_state_machine = __esm({
@@ -359,6 +359,287 @@ var init_state_machine = __esm({
359
359
  });
360
360
 
361
361
  // src/lifecycle/frontmatter.ts
362
+ function extractFrontmatter(fileContent) {
363
+ const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
364
+ if (!match) {
365
+ throw new Error("No frontmatter found in file. Expected --- delimiters.");
366
+ }
367
+ const frontmatterBlock = match[1];
368
+ const body = fileContent.slice(match[0].length);
369
+ return [frontmatterBlock, body];
370
+ }
371
+ function parseSimpleValue(raw) {
372
+ const trimmed = raw.trim();
373
+ if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
374
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
375
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
376
+ }
377
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
378
+ return trimmed.slice(1, -1);
379
+ }
380
+ return trimmed;
381
+ }
382
+ function parseDependsOn(frontmatter) {
383
+ const inlineMatch = frontmatter.match(/^dependsOn:\s*\[\s*\]/m);
384
+ if (inlineMatch) return [];
385
+ const results = [];
386
+ const blockMatch = frontmatter.match(/^dependsOn:\s*\n((?:\s+-\s+.*\n?)*)/m);
387
+ if (blockMatch) {
388
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
389
+ for (const item of items) {
390
+ results.push(item[1].trim());
391
+ }
392
+ }
393
+ return results;
394
+ }
395
+ function parseLinks(frontmatter) {
396
+ const inlineMatch = frontmatter.match(/^links:\s*\[\s*\]/m);
397
+ if (inlineMatch) return [];
398
+ const results = [];
399
+ const blockMatch = frontmatter.match(/^links:\s*\n((?:\s+-\s+.*\n?)*)/m);
400
+ if (blockMatch) {
401
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
402
+ for (const item of items) {
403
+ results.push(item[1].trim());
404
+ }
405
+ }
406
+ return results;
407
+ }
408
+ function parseExternalIds(frontmatter) {
409
+ const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
410
+ if (inlineMatch) return [];
411
+ const results = [];
412
+ const blockMatch = frontmatter.match(
413
+ /^externalIds:\s*\n((?:\s+-\s+[\s\S]*?)(?=^\w|\n---))/m
414
+ );
415
+ if (!blockMatch) return [];
416
+ const itemBlocks = blockMatch[1].split(/\n\s+-\s+/).filter(Boolean);
417
+ for (const block of itemBlocks) {
418
+ const lines = block.split("\n");
419
+ const entry = {};
420
+ for (const line of lines) {
421
+ const colonIdx = line.indexOf(":");
422
+ if (colonIdx < 0) continue;
423
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
424
+ if (!key) continue;
425
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
426
+ }
427
+ if (entry["system"] && entry["id"]) {
428
+ results.push({
429
+ system: entry["system"],
430
+ id: entry["id"],
431
+ url: entry["url"] || null
432
+ });
433
+ }
434
+ }
435
+ return results;
436
+ }
437
+ function parseStatusHistory(frontmatter) {
438
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
439
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
440
+ if (!headerMatch) return [];
441
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
442
+ const bodyStart = headerStart + headerMatch[0].length + 1;
443
+ const after = frontmatter.slice(bodyStart);
444
+ const bodyLines = [];
445
+ for (const line of after.split("\n")) {
446
+ if (line.length === 0) {
447
+ bodyLines.push(line);
448
+ continue;
449
+ }
450
+ if (line[0] !== " " && line[0] !== " ") break;
451
+ bodyLines.push(line);
452
+ }
453
+ const body = bodyLines.join("\n");
454
+ const results = [];
455
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
456
+ for (const block of itemBlocks) {
457
+ const entry = {};
458
+ for (const line of block.split("\n")) {
459
+ const colonIdx = line.indexOf(":");
460
+ if (colonIdx < 0) continue;
461
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
462
+ if (!key) continue;
463
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
464
+ }
465
+ if (!entry["to"]) continue;
466
+ const result = {
467
+ at: entry["at"] ?? "",
468
+ from: entry["from"] ?? null,
469
+ to: entry["to"],
470
+ command: entry["command"] ?? "",
471
+ by: entry["by"] ?? null
472
+ };
473
+ if (entry["reason"] != null) result.reason = entry["reason"];
474
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
475
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
476
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
477
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
478
+ results.push(result);
479
+ }
480
+ return results;
481
+ }
482
+ function parseNestedBlock(frontmatter, header) {
483
+ if (new RegExp(`^${header}:\\s*(null|~)\\s*$`, "m").test(frontmatter)) return null;
484
+ const headerMatch = frontmatter.match(new RegExp(`^${header}:\\s*$`, "m"));
485
+ if (!headerMatch) return null;
486
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
487
+ const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);
488
+ const out = {};
489
+ for (const line of after.split("\n")) {
490
+ if (line.length === 0) continue;
491
+ if (line[0] !== " " && line[0] !== " ") break;
492
+ const colonIdx = line.indexOf(":");
493
+ if (colonIdx < 0) continue;
494
+ const key = line.slice(0, colonIdx).trim();
495
+ if (!key) continue;
496
+ out[key] = parseSimpleValue(line.slice(colonIdx + 1));
497
+ }
498
+ return Object.keys(out).length > 0 ? out : null;
499
+ }
500
+ function parsePlanApproval(frontmatter) {
501
+ const block = parseNestedBlock(frontmatter, "planApproval");
502
+ if (!block || !block["file"] || !block["digest"]) return null;
503
+ return {
504
+ file: block["file"],
505
+ digest: block["digest"],
506
+ by: block["by"] ?? null,
507
+ at: block["at"] ?? ""
508
+ };
509
+ }
510
+ function parseOverride(frontmatter) {
511
+ const block = parseNestedBlock(frontmatter, "override");
512
+ if (!block || !block["status"]) return null;
513
+ return {
514
+ status: block["status"],
515
+ source: block["source"] ?? "human",
516
+ reason: block["reason"] ?? null,
517
+ at: block["at"] ?? ""
518
+ };
519
+ }
520
+ function parseFactsMap(frontmatter) {
521
+ const block = parseNestedBlock(frontmatter, "facts");
522
+ if (!block) return {};
523
+ const out = {};
524
+ for (const [k, v] of Object.entries(block)) {
525
+ if (v === null) continue;
526
+ out[k] = v;
527
+ }
528
+ return out;
529
+ }
530
+ function parseAttestations(frontmatter) {
531
+ if (/^attestations:\s*\[\s*\]/m.test(frontmatter)) return [];
532
+ const headerMatch = frontmatter.match(/^attestations:\s*$/m);
533
+ if (!headerMatch) return [];
534
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
535
+ const bodyStart = headerStart + headerMatch[0].length + 1;
536
+ const after = frontmatter.slice(bodyStart);
537
+ const bodyLines = [];
538
+ for (const line of after.split("\n")) {
539
+ if (line.length === 0) {
540
+ bodyLines.push(line);
541
+ continue;
542
+ }
543
+ if (line[0] !== " " && line[0] !== " ") break;
544
+ bodyLines.push(line);
545
+ }
546
+ const body = bodyLines.join("\n");
547
+ const results = [];
548
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
549
+ for (const block of itemBlocks) {
550
+ const entry = {};
551
+ for (const line of block.split("\n")) {
552
+ const colonIdx = line.indexOf(":");
553
+ if (colonIdx < 0) continue;
554
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
555
+ if (!key) continue;
556
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
557
+ }
558
+ const verdict = entry["verdict"];
559
+ if (!entry["fact"] || !entry["actor"] || !verdict || !entry["at"]) continue;
560
+ if (verdict !== "approved" && verdict !== "changes-requested") continue;
561
+ const record = {
562
+ fact: entry["fact"],
563
+ actor: entry["actor"],
564
+ verdict,
565
+ at: entry["at"]
566
+ };
567
+ if (entry["note"] != null) record.note = entry["note"];
568
+ if (entry["file"] != null) record.file = entry["file"];
569
+ if (entry["digest"] != null) record.digest = entry["digest"];
570
+ if (entry["commit"] != null) record.commit = entry["commit"];
571
+ results.push(record);
572
+ }
573
+ return results;
574
+ }
575
+ function parseWorkspace(frontmatter) {
576
+ const defaults = {
577
+ repository: null,
578
+ worktreePath: null,
579
+ branch: null,
580
+ parentBranch: null
581
+ };
582
+ const fields = ["repository", "worktreePath", "branch", "parentBranch"];
583
+ for (const field of fields) {
584
+ const match = frontmatter.match(new RegExp(`^\\s+${field}:\\s*(.*)$`, "m"));
585
+ if (match) {
586
+ defaults[field] = parseSimpleValue(match[1]);
587
+ }
588
+ }
589
+ return defaults;
590
+ }
591
+ function parseTags(frontmatter) {
592
+ const inlineMatch = frontmatter.match(/^tags:\s*\[\s*\]/m);
593
+ if (inlineMatch) return [];
594
+ const results = [];
595
+ const blockMatch = frontmatter.match(/^tags:\s*\n((?:\s+-\s+.*\n?)*)/m);
596
+ if (blockMatch) {
597
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
598
+ for (const item of items) {
599
+ results.push(item[1].trim());
600
+ }
601
+ }
602
+ return results;
603
+ }
604
+ function parseAssignmentFrontmatter(fileContent) {
605
+ const [frontmatter] = extractFrontmatter(fileContent);
606
+ function getField2(key) {
607
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
608
+ if (!match) return null;
609
+ return parseSimpleValue(match[1]);
610
+ }
611
+ return {
612
+ id: getField2("id") ?? "",
613
+ slug: getField2("slug") ?? "",
614
+ title: getField2("title") ?? "",
615
+ project: getField2("project"),
616
+ type: getField2("type"),
617
+ status: getField2("status") ?? "pending",
618
+ priority: getField2("priority") ?? "medium",
619
+ created: getField2("created") ?? "",
620
+ updated: getField2("updated") ?? "",
621
+ assignee: getField2("assignee"),
622
+ externalIds: parseExternalIds(frontmatter),
623
+ statusHistory: parseStatusHistory(frontmatter),
624
+ dependsOn: parseDependsOn(frontmatter),
625
+ links: parseLinks(frontmatter),
626
+ blockedReason: getField2("blockedReason"),
627
+ workspace: parseWorkspace(frontmatter),
628
+ tags: parseTags(frontmatter),
629
+ archived: getField2("archived") === "true",
630
+ archivedAt: getField2("archivedAt"),
631
+ archivedReason: getField2("archivedReason"),
632
+ phase: getField2("phase"),
633
+ disposition: getField2("disposition"),
634
+ planApproval: parsePlanApproval(frontmatter),
635
+ parked: getField2("parked") === "true",
636
+ reviewRequested: getField2("reviewRequested") === "true",
637
+ implementationStarted: getField2("implementationStarted") === "true",
638
+ override: parseOverride(frontmatter),
639
+ facts: parseFactsMap(frontmatter),
640
+ attestations: parseAttestations(frontmatter)
641
+ };
642
+ }
362
643
  var init_frontmatter = __esm({
363
644
  "src/lifecycle/frontmatter.ts"() {
364
645
  "use strict";
@@ -393,7 +674,7 @@ var init_event_emit = __esm({
393
674
  });
394
675
 
395
676
  // src/dashboard/parser.ts
396
- function extractFrontmatter(fileContent) {
677
+ function extractFrontmatter2(fileContent) {
397
678
  const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
398
679
  if (!match) {
399
680
  return ["", fileContent];
@@ -402,7 +683,7 @@ function extractFrontmatter(fileContent) {
402
683
  const body = fileContent.slice(match[0].length).trim();
403
684
  return [frontmatterBlock, body];
404
685
  }
405
- function parseSimpleValue(raw) {
686
+ function parseSimpleValue2(raw) {
406
687
  const trimmed = raw.trim();
407
688
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
408
689
  if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
@@ -416,7 +697,7 @@ function parseSimpleValue(raw) {
416
697
  function getField(frontmatter, key) {
417
698
  const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
418
699
  if (!match) return null;
419
- return parseSimpleValue(match[1]);
700
+ return parseSimpleValue2(match[1]);
420
701
  }
421
702
  function getNestedField(frontmatter, parent, key) {
422
703
  const parentRegex = new RegExp(`^${parent}:\\s*\\n((?:\\s+.*\\n?)*)`, "m");
@@ -425,7 +706,7 @@ function getNestedField(frontmatter, parent, key) {
425
706
  const block = parentMatch[1];
426
707
  const fieldMatch = block.match(new RegExp(`^\\s+${key}:\\s*(.*)$`, "m"));
427
708
  if (!fieldMatch) return null;
428
- return parseSimpleValue(fieldMatch[1]);
709
+ return parseSimpleValue2(fieldMatch[1]);
429
710
  }
430
711
  function parseListField(frontmatter, fieldName) {
431
712
  const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\s*\\[\\s*\\]`, "m"));
@@ -450,7 +731,7 @@ function unquoteYamlString(value) {
450
731
  return value;
451
732
  }
452
733
  function parseProject(fileContent) {
453
- const [fm, body] = extractFrontmatter(fileContent);
734
+ const [fm, body] = extractFrontmatter2(fileContent);
454
735
  const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
455
736
  return {
456
737
  id: getField(fm, "id") ?? "",
@@ -465,12 +746,12 @@ function parseProject(fileContent) {
465
746
  tags: parseListField(fm, "tags"),
466
747
  workspace: getField(fm, "workspace"),
467
748
  repositories: parseListField(fm, "repositories").map(unquoteYamlString),
468
- externalIds: parseExternalIds(fm),
749
+ externalIds: parseExternalIds2(fm),
469
750
  body
470
751
  };
471
752
  }
472
753
  function parseStatus(fileContent) {
473
- const [fm, body] = extractFrontmatter(fileContent);
754
+ const [fm, body] = extractFrontmatter2(fileContent);
474
755
  const progress = { total: 0 };
475
756
  const progressMatch = fm.match(/^progress:\s*\n((?:\s+.*\n?)*)/m);
476
757
  if (progressMatch) {
@@ -494,7 +775,7 @@ function parseStatus(fileContent) {
494
775
  body
495
776
  };
496
777
  }
497
- function parseExternalIds(frontmatter) {
778
+ function parseExternalIds2(frontmatter) {
498
779
  const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
499
780
  if (inlineMatch) return [];
500
781
  const results = [];
@@ -511,7 +792,7 @@ function parseExternalIds(frontmatter) {
511
792
  if (colonIdx < 0) continue;
512
793
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
513
794
  if (!key) continue;
514
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
795
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
515
796
  }
516
797
  if (entry["system"] && entry["id"]) {
517
798
  results.push({
@@ -523,7 +804,7 @@ function parseExternalIds(frontmatter) {
523
804
  }
524
805
  return results;
525
806
  }
526
- function parseStatusHistory(frontmatter) {
807
+ function parseStatusHistory2(frontmatter) {
527
808
  if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
528
809
  const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
529
810
  if (!headerMatch) return [];
@@ -549,7 +830,7 @@ function parseStatusHistory(frontmatter) {
549
830
  if (colonIdx < 0) continue;
550
831
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
551
832
  if (!key) continue;
552
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
833
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
553
834
  }
554
835
  if (!entry["to"]) continue;
555
836
  const result = {
@@ -568,7 +849,7 @@ function parseStatusHistory(frontmatter) {
568
849
  }
569
850
  return results;
570
851
  }
571
- function parseFactsMap(frontmatter) {
852
+ function parseFactsMap2(frontmatter) {
572
853
  const headerMatch = frontmatter.match(/^facts:\s*$/m);
573
854
  if (!headerMatch) return {};
574
855
  const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
@@ -581,13 +862,13 @@ function parseFactsMap(frontmatter) {
581
862
  if (colonIdx < 0) continue;
582
863
  const key = line.slice(0, colonIdx).trim();
583
864
  if (!key) continue;
584
- const value = parseSimpleValue(line.slice(colonIdx + 1));
865
+ const value = parseSimpleValue2(line.slice(colonIdx + 1));
585
866
  if (value === null) continue;
586
867
  out[key] = value;
587
868
  }
588
869
  return out;
589
870
  }
590
- function parseAttestations(frontmatter) {
871
+ function parseAttestations2(frontmatter) {
591
872
  if (/^attestations:\s*\[\s*\]/m.test(frontmatter)) return [];
592
873
  const headerMatch = frontmatter.match(/^attestations:\s*$/m);
593
874
  if (!headerMatch) return [];
@@ -613,7 +894,7 @@ function parseAttestations(frontmatter) {
613
894
  if (colonIdx < 0) continue;
614
895
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
615
896
  if (!key) continue;
616
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
897
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
617
898
  }
618
899
  const verdict = entry["verdict"];
619
900
  if (!entry["fact"] || !entry["actor"] || !verdict || !entry["at"]) continue;
@@ -633,7 +914,7 @@ function parseAttestations(frontmatter) {
633
914
  return results;
634
915
  }
635
916
  function parseAssignmentFull(fileContent) {
636
- const [fm, body] = extractFrontmatter(fileContent);
917
+ const [fm, body] = extractFrontmatter2(fileContent);
637
918
  return {
638
919
  id: getField(fm, "id") ?? "",
639
920
  slug: getField(fm, "slug") ?? "",
@@ -653,8 +934,8 @@ function parseAssignmentFull(fileContent) {
653
934
  branch: getNestedField(fm, "workspace", "branch"),
654
935
  parentBranch: getNestedField(fm, "workspace", "parentBranch")
655
936
  },
656
- externalIds: parseExternalIds(fm),
657
- statusHistory: parseStatusHistory(fm),
937
+ externalIds: parseExternalIds2(fm),
938
+ statusHistory: parseStatusHistory2(fm),
658
939
  tags: parseListField(fm, "tags"),
659
940
  archived: getField(fm, "archived") === "true",
660
941
  archivedAt: getField(fm, "archivedAt"),
@@ -688,12 +969,12 @@ function parseAssignmentFull(fileContent) {
688
969
  at: getNestedField(fm, "override", "at") ?? ""
689
970
  };
690
971
  })(),
691
- facts: parseFactsMap(fm),
692
- attestations: parseAttestations(fm)
972
+ facts: parseFactsMap2(fm),
973
+ attestations: parseAttestations2(fm)
693
974
  };
694
975
  }
695
976
  function parsePlan(fileContent) {
696
- const [fm, body] = extractFrontmatter(fileContent);
977
+ const [fm, body] = extractFrontmatter2(fileContent);
697
978
  return {
698
979
  assignment: getField(fm, "assignment") ?? "",
699
980
  status: getField(fm, "status") ?? "",
@@ -703,7 +984,7 @@ function parsePlan(fileContent) {
703
984
  };
704
985
  }
705
986
  function parseScratchpad(fileContent) {
706
- const [fm, body] = extractFrontmatter(fileContent);
987
+ const [fm, body] = extractFrontmatter2(fileContent);
707
988
  return {
708
989
  assignment: getField(fm, "assignment") ?? "",
709
990
  updated: getField(fm, "updated") ?? "",
@@ -711,7 +992,7 @@ function parseScratchpad(fileContent) {
711
992
  };
712
993
  }
713
994
  function parseHandoff(fileContent) {
714
- const [fm, body] = extractFrontmatter(fileContent);
995
+ const [fm, body] = extractFrontmatter2(fileContent);
715
996
  return {
716
997
  assignment: getField(fm, "assignment") ?? "",
717
998
  handoffCount: parseInt(getField(fm, "handoffCount") ?? "0", 10),
@@ -720,7 +1001,7 @@ function parseHandoff(fileContent) {
720
1001
  };
721
1002
  }
722
1003
  function parseDecisionRecord(fileContent) {
723
- const [fm, body] = extractFrontmatter(fileContent);
1004
+ const [fm, body] = extractFrontmatter2(fileContent);
724
1005
  return {
725
1006
  assignment: getField(fm, "assignment") ?? "",
726
1007
  decisionCount: parseInt(getField(fm, "decisionCount") ?? "0", 10),
@@ -729,7 +1010,7 @@ function parseDecisionRecord(fileContent) {
729
1010
  };
730
1011
  }
731
1012
  function parseComments(fileContent) {
732
- const [fm, body] = extractFrontmatter(fileContent);
1013
+ const [fm, body] = extractFrontmatter2(fileContent);
733
1014
  const entries = [];
734
1015
  const sections = body.split(
735
1016
  /^## (?=[^\n]*\n\s*\*\*Recorded:\*\*[^\n]*\n\*\*Author:\*\*[^\n]*\n\*\*Type:\*\*\s*(?:question|note|feedback)\b)/m
@@ -764,7 +1045,7 @@ function parseComments(fileContent) {
764
1045
  };
765
1046
  }
766
1047
  function parseProgress(fileContent) {
767
- const [fm, body] = extractFrontmatter(fileContent);
1048
+ const [fm, body] = extractFrontmatter2(fileContent);
768
1049
  const entries = [];
769
1050
  const sections = body.split(/^## /m).slice(1);
770
1051
  for (const section of sections) {
@@ -783,7 +1064,7 @@ function parseProgress(fileContent) {
783
1064
  };
784
1065
  }
785
1066
  function parsePlaybook(fileContent) {
786
- const [fm, body] = extractFrontmatter(fileContent);
1067
+ const [fm, body] = extractFrontmatter2(fileContent);
787
1068
  return {
788
1069
  slug: getField(fm, "slug") ?? "",
789
1070
  name: getField(fm, "name") ?? "",
@@ -3951,7 +4232,7 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
3951
4232
  let workspaceGroup = null;
3952
4233
  try {
3953
4234
  const content = await readFile6(standalonePath, "utf-8");
3954
- const [fm] = extractFrontmatter(content);
4235
+ const [fm] = extractFrontmatter2(content);
3955
4236
  workspaceGroup = getField(fm, "workspaceGroup");
3956
4237
  } catch {
3957
4238
  }
@@ -3979,7 +4260,7 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
3979
4260
  if (!await fileExists(aPath)) continue;
3980
4261
  try {
3981
4262
  const content = await readFile6(aPath, "utf-8");
3982
- const [fm] = extractFrontmatter(content);
4263
+ const [fm] = extractFrontmatter2(content);
3983
4264
  const fileId = getField(fm, "id");
3984
4265
  if (fileId === id) {
3985
4266
  projectMatch = {
@@ -4322,8 +4603,7 @@ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses)
4322
4603
  if (!await fileExists(depPath)) return false;
4323
4604
  try {
4324
4605
  const content = await readFile9(depPath, "utf-8");
4325
- const m = content.match(/^status:\s*(.+)$/m);
4326
- const status = m ? m[1].trim() : "";
4606
+ const { status } = parseAssignmentFrontmatter(content);
4327
4607
  if (!terminalStatuses.has(status)) return false;
4328
4608
  } catch {
4329
4609
  return false;
@@ -4438,6 +4718,7 @@ var init_facts = __esm({
4438
4718
  init_fs();
4439
4719
  init_git_worktree();
4440
4720
  init_derive();
4721
+ init_frontmatter();
4441
4722
  HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
4442
4723
  PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
4443
4724
  }
@@ -6595,6 +6876,12 @@ async function resolveSessionPlan(input, terminal) {
6595
6876
  }
6596
6877
  }
6597
6878
  }
6879
+ if (!cwd.trim()) {
6880
+ throw new LaunchError(
6881
+ "workspace-path-invalid",
6882
+ `Session ${input.id} has no recorded working directory and no linked assignment workspace resolved \u2014 refusing to launch in an unknown directory.`
6883
+ );
6884
+ }
6598
6885
  const agent = getAgents(input.config).find((a) => a.id === session.agent);
6599
6886
  if (!agent) {
6600
6887
  throw new LaunchError(