halfcycle 0.5.3 → 0.6.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.
package/dist/cli.js CHANGED
@@ -513,6 +513,11 @@ var TASK_LIST_FILE = "docs/build-records/build-tasks.json";
513
513
  var TASK_PLAN_TASKS_MAX = 500;
514
514
  var TASK_PLAN_ID_MAX = 64;
515
515
  var TASK_PLAN_BUILDS_NONE = "none";
516
+ var TASK_PLAN_CRITERIA_NONE = "none";
517
+ var TASK_PLAN_SPECS_NONE = "none";
518
+ var EXAMPLE_SPEC = "order-intake";
519
+ var SPEC_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
520
+ var CITATION = /^([a-z0-9][a-z0-9-]{0,63}) AC((?:0|[1-9][0-9]*)[a-z]?)$/;
516
521
  var TASK_PLAN_KINDS = ["build", "review", "spec", "walk", "check"];
517
522
  var TASK_PLAN_INTERFACE_KINDS = [
518
523
  "route",
@@ -538,7 +543,9 @@ var taskPlanTaskSchema = z2.object({
538
543
  }).strict().optional(),
539
544
  wholeProject: z2.literal(true).optional(),
540
545
  ships: z2.enum(["deploy", "release"]).optional(),
541
- builds: z2.union([z2.literal(TASK_PLAN_BUILDS_NONE), z2.array(planText)]).optional()
546
+ builds: z2.union([z2.literal(TASK_PLAN_BUILDS_NONE), z2.array(planText)]).optional(),
547
+ criteria: z2.union([z2.literal(TASK_PLAN_CRITERIA_NONE), z2.array(planText)]).optional(),
548
+ whyOutsidePlan: planText.optional()
542
549
  }).strict().superRefine((task, ctx) => {
543
550
  if (task.reviews !== void 0 && task.kind !== "review") {
544
551
  ctx.addIssue({
@@ -564,6 +571,7 @@ var taskPlanTaskSchema = z2.object({
564
571
  });
565
572
  var taskPlanSchema = z2.object({
566
573
  phase: z2.string().min(1).refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" }),
574
+ specs: z2.union([z2.literal(TASK_PLAN_SPECS_NONE), z2.array(planText)]).optional(),
567
575
  tasks: z2.array(taskPlanTaskSchema).max(TASK_PLAN_TASKS_MAX)
568
576
  }).strict();
569
577
  var taskListRowSchema = z2.object({
@@ -586,6 +594,20 @@ var taskPlanLineText = {
586
594
  N1: (asked) => `The task list in ${TASK_LIST_FILE} is not for "${asked}", so it was not compared.`,
587
595
  O6: (phase, asked) => `The plan is for "${phase}", not "${asked}".`,
588
596
  O7: (a, file, partIds) => `"${a}" does not say which parts of the system it builds. Add "builds" with the ids of the parts it builds, from ${file}: ${partIds.join(", ")}. If it builds none of them, write "builds": "${TASK_PLAN_BUILDS_NONE}".`,
597
+ O8: (a, parts, file) => parts.length === 1 ? `"${a}" builds "${parts[0]}", which is not in plan: ${file} does not change it. Copy "builds" from that plan's changes (a release, publish or pin task names the part it ships, or "${TASK_PLAN_BUILDS_NONE}"), or say why in "whyOutsidePlan".` : `"${a}" builds ${parts.map((x) => `"${x}"`).join(", ")}, which are not in plan: ${file} changes none of them. Copy "builds" from that plan's changes (a release, publish or pin task names the part it ships, or "${TASK_PLAN_BUILDS_NONE}"), or say why in "whyOutsidePlan".`,
598
+ C1Form: (a, x, first) => `"${a}" names "${x}", which is not a criterion. Write it as <spec name> AC<number>, for example "${first} AC1".`,
599
+ C1Number: (a, x, folder, slug, n) => `"${a}" names "${x}", but ${folder}/${slug}.md has no AC${n}.`,
600
+ C2: (a, x) => `"${a}" names "${x}", which was retired. Remove it, or name the criterion that replaced it.`,
601
+ C3: (slug, n) => `${slug} AC${n} is not delivered by any build task. Add "${slug} AC${n}" to the criteria of the build task that delivers it.`,
602
+ C4: (a) => `"${a}" does not say which acceptance criteria it delivers. Add "criteria" with each one as <spec name> AC<number>. If it delivers none, write "criteria": "${TASK_PLAN_CRITERIA_NONE}".`,
603
+ C5: (a, x, slug) => `"${a}" names "${x}", but "${slug}" is not in the plan's specs. Add it to "specs" if it belongs to this phase, or remove the citation.`,
604
+ C6None: (folder) => `The plan does not say which feature designs this phase has. Add "specs" with the name of each file in ${folder}/ this phase builds, without ".md". If it has none, write "specs": "${TASK_PLAN_SPECS_NONE}".`,
605
+ C6Malformed: (x, folder) => `The plan's specs name "${x}", which is not a spec name. Use the name of a file in ${folder}/, without ".md".`,
606
+ C6Missing: (slug, folder) => `The plan's specs name "${slug}", but there is no ${folder}/${slug}.md.`,
607
+ B1: (s, t, x) => `"${s}" and "${t}" both own ${x}. A file belongs to one spec: keep it in the Owns of the spec that mainly changes it, and let the other spec's tasks list it in what they touch.`,
608
+ B2: (s, x) => `"${s}" lists "${x}" under Owns, which is not a path inside this project. Use a path from the project's top folder; end a folder with /.`,
609
+ N2: (a, files) => `"${a}" changes files that none of its specs own: ${files.join(", ")}. If that is intended there is nothing to do; otherwise add them to a spec's Owns.`,
610
+ N3: (s) => `"${s}" has no Boundary section, so what it owns was not checked.`,
589
611
  K1: (a) => `"${a}" is in no batch: no whole-project check waits for it. Add it to the dependsOn of the first whole-project check that it does not already wait for and that waits for every build task it waits for, or add a whole-project check after it.`,
590
612
  K2: (a, b) => `"${a}" and "${b}" are both whole-project checks and neither waits for the other. Make the later one wait for the earlier.`,
591
613
  K3: (b, a, c) => `"${b}" builds on "${a}" before "${c}" has checked it. Add "${c}" to the dependsOn of "${b}".`,
@@ -610,10 +632,10 @@ function planInterfaceKey(text) {
610
632
  return null;
611
633
  return `${kind}: ${name}`.toLowerCase();
612
634
  }
613
- function comparableItemsOf(task) {
635
+ function pathItemsOf(paths) {
614
636
  const files = [];
615
637
  const folders = [];
616
- for (const path of task.touches?.files ?? []) {
638
+ for (const path of paths) {
617
639
  if (isMalformedPlanPath(path))
618
640
  continue;
619
641
  const item = { shown: path, key: path.toLowerCase() };
@@ -621,6 +643,10 @@ function comparableItemsOf(task) {
621
643
  if (item.key.endsWith("/"))
622
644
  folders.push(item);
623
645
  }
646
+ return { files, folders };
647
+ }
648
+ function comparableItemsOf(task) {
649
+ const { files, folders } = pathItemsOf(task.touches?.files ?? []);
624
650
  const interfaces = [];
625
651
  for (const text of task.touches?.interfaces ?? []) {
626
652
  const key = planInterfaceKey(text);
@@ -826,6 +852,9 @@ function firstDifference(plan, list2) {
826
852
  }
827
853
  return null;
828
854
  }
855
+ function note(code, text) {
856
+ return { code, text, refuses: false };
857
+ }
829
858
  function refusal(code, text) {
830
859
  return { code, text, refuses: true };
831
860
  }
@@ -838,6 +867,243 @@ function mustNameParts(task) {
838
867
  function namesNoPart(task) {
839
868
  return task.builds === void 0 || Array.isArray(task.builds) && task.builds.length === 0;
840
869
  }
870
+ function partsNotInPlan(tasks, planPartIds) {
871
+ const planned = new Set(planPartIds);
872
+ const out = [];
873
+ for (const task of tasks) {
874
+ if (task.reviews !== void 0 || task.kind === "review")
875
+ continue;
876
+ if (!Array.isArray(task.builds))
877
+ continue;
878
+ if (task.whyOutsidePlan !== void 0 && task.whyOutsidePlan.trim() !== "")
879
+ continue;
880
+ const parts = [...new Set(task.builds.filter((part) => !planned.has(part)))];
881
+ if (parts.length > 0)
882
+ out.push({ id: task.id, parts });
883
+ }
884
+ return out;
885
+ }
886
+ function namesNoCriterion(task) {
887
+ return task.criteria === void 0 || Array.isArray(task.criteria) && task.criteria.length === 0;
888
+ }
889
+ function isPlanSpecName(text) {
890
+ return SPEC_NAME.test(text);
891
+ }
892
+ function planSpecNamesToRead(plan) {
893
+ if (!Array.isArray(plan.specs))
894
+ return [];
895
+ return distinct(plan.specs.filter(isPlanSpecName));
896
+ }
897
+ var CRITERIA_HEADING = /^##\s+Acceptance criteria/;
898
+ var SECTION_HEADING = /^##\s/;
899
+ var ANY_HEADING = /^#{1,6}\s/;
900
+ var CHECKLIST_ITEM = /^- \[[ x]\] /;
901
+ var EXPLICIT_NUMBER = /^\*\*AC((?:0|[1-9][0-9]*)[a-z]?)(?: —|\.|:)/;
902
+ var RETIRED_LEAD = /^\*\*AC(?:0|[1-9][0-9]*)[a-z]? — retired \d{4}-\d{2}-\d{2}/;
903
+ function readSpecCriteria(markdown) {
904
+ const lines = markdown.split(/\r?\n/);
905
+ const start = lines.findIndex((line) => CRITERIA_HEADING.test(line));
906
+ if (start < 0)
907
+ return [];
908
+ const items = [];
909
+ let current = null;
910
+ const close = () => {
911
+ if (current !== null)
912
+ items.push(current);
913
+ current = null;
914
+ };
915
+ for (let i = start + 1; i < lines.length; i += 1) {
916
+ const line = lines[i];
917
+ if (SECTION_HEADING.test(line))
918
+ break;
919
+ if (CHECKLIST_ITEM.test(line)) {
920
+ close();
921
+ current = line.slice("- [ ] ".length);
922
+ continue;
923
+ }
924
+ if (line.trim() === "" || ANY_HEADING.test(line)) {
925
+ close();
926
+ continue;
927
+ }
928
+ if (current !== null)
929
+ current = `${current} ${line.trim()}`;
930
+ }
931
+ close();
932
+ return items.map((item, i) => {
933
+ const explicit = EXPLICIT_NUMBER.exec(item);
934
+ return {
935
+ number: explicit ? explicit[1] : String(i + 1),
936
+ retired: RETIRED_LEAD.test(item)
937
+ };
938
+ });
939
+ }
940
+ function liveNumbers(criteria) {
941
+ const live = new Set(criteria.filter((c) => !c.retired).map((c) => c.number));
942
+ return distinct(criteria.map((c) => c.number)).filter((n) => live.has(n));
943
+ }
944
+ function specLines(plan, specFiles) {
945
+ const folder = specFiles.folder;
946
+ if (plan.specs === TASK_PLAN_SPECS_NONE)
947
+ return [];
948
+ if (plan.specs === void 0 || plan.specs.length === 0) {
949
+ return [refusal("C6", taskPlanLineText.C6None(folder))];
950
+ }
951
+ const criteriaOf = specCriteriaBySlug(specFiles);
952
+ const lines = [];
953
+ for (const name of distinct(plan.specs)) {
954
+ if (!isPlanSpecName(name)) {
955
+ lines.push(refusal("C6", taskPlanLineText.C6Malformed(name, folder)));
956
+ } else if ((criteriaOf.get(name) ?? null) === null) {
957
+ lines.push(refusal("C6", taskPlanLineText.C6Missing(name, folder)));
958
+ }
959
+ }
960
+ const covered = /* @__PURE__ */ new Set();
961
+ for (const task of plan.tasks) {
962
+ if (task.kind !== "build" || !Array.isArray(task.criteria))
963
+ continue;
964
+ for (const citation of task.criteria)
965
+ covered.add(citation);
966
+ }
967
+ for (const slug of planSpecNamesToRead(plan)) {
968
+ const criteria = criteriaOf.get(slug) ?? null;
969
+ if (criteria === null)
970
+ continue;
971
+ for (const n of liveNumbers(criteria)) {
972
+ if (!covered.has(`${slug} AC${n}`))
973
+ lines.push(refusal("C3", taskPlanLineText.C3(slug, n)));
974
+ }
975
+ }
976
+ return lines;
977
+ }
978
+ function specCriteriaBySlug(specFiles) {
979
+ const bySlug = /* @__PURE__ */ new Map();
980
+ for (const entry of specFiles.entries) {
981
+ if (!bySlug.has(entry.slug))
982
+ bySlug.set(entry.slug, entry.criteria);
983
+ }
984
+ return bySlug;
985
+ }
986
+ function citationLine(taskId, citation, plan, specFiles, criteriaOf) {
987
+ const match = CITATION.exec(citation);
988
+ if (match === null) {
989
+ const first = Array.isArray(plan.specs) ? plan.specs.find(isPlanSpecName) : void 0;
990
+ return refusal("C1", taskPlanLineText.C1Form(taskId, citation, first ?? EXAMPLE_SPEC));
991
+ }
992
+ if (plan.specs === void 0 || plan.specs.length === 0)
993
+ return null;
994
+ const slug = match[1];
995
+ const n = match[2];
996
+ if (plan.specs === TASK_PLAN_SPECS_NONE || !plan.specs.includes(slug)) {
997
+ return refusal("C5", taskPlanLineText.C5(taskId, citation, slug));
998
+ }
999
+ const criteria = criteriaOf.get(slug) ?? null;
1000
+ if (criteria === null)
1001
+ return null;
1002
+ const items = criteria.filter((c) => c.number === n);
1003
+ if (items.length === 0) {
1004
+ return refusal("C1", taskPlanLineText.C1Number(taskId, citation, specFiles.folder, slug, n));
1005
+ }
1006
+ if (items.every((c) => c.retired))
1007
+ return refusal("C2", taskPlanLineText.C2(taskId, citation));
1008
+ return null;
1009
+ }
1010
+ function shownOwnsText(text) {
1011
+ return text.replace(/[\u0000-\u001f\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
1012
+ }
1013
+ function ownedPathsBySpec(plan, specFiles) {
1014
+ const boundaryOf = boundaryBySlug(specFiles);
1015
+ const owned = /* @__PURE__ */ new Map();
1016
+ for (const slug of planSpecNamesToRead(plan)) {
1017
+ const boundary = boundaryOf.get(slug) ?? null;
1018
+ if (boundary === null || boundary.kind !== "section")
1019
+ continue;
1020
+ const paths = [];
1021
+ for (const item of boundary.owns) {
1022
+ if (item.kind === "path" && !isMalformedOwnsPath(item.path))
1023
+ paths.push(item.path);
1024
+ }
1025
+ owned.set(slug, paths);
1026
+ }
1027
+ return owned;
1028
+ }
1029
+ function isMalformedOwnsPath(path) {
1030
+ return isMalformedPlanPath(path) || CONTROL_CHARACTER.test(path);
1031
+ }
1032
+ function boundaryBySlug(specFiles) {
1033
+ const bySlug = /* @__PURE__ */ new Map();
1034
+ for (const entry of specFiles.entries) {
1035
+ if (!bySlug.has(entry.slug))
1036
+ bySlug.set(entry.slug, entry.boundary);
1037
+ }
1038
+ return bySlug;
1039
+ }
1040
+ function ownershipLines(plan, specFiles) {
1041
+ const lines = [];
1042
+ const boundaryOf = boundaryBySlug(specFiles);
1043
+ for (const slug of planSpecNamesToRead(plan)) {
1044
+ const boundary = boundaryOf.get(slug) ?? null;
1045
+ if (boundary === null || boundary.kind !== "section")
1046
+ continue;
1047
+ for (const item of boundary.owns) {
1048
+ if (item.kind === "bad") {
1049
+ lines.push(refusal("B2", taskPlanLineText.B2(slug, shownOwnsText(item.text))));
1050
+ } else if (isMalformedOwnsPath(item.path)) {
1051
+ lines.push(refusal("B2", taskPlanLineText.B2(slug, shownOwnsText(item.path))));
1052
+ }
1053
+ }
1054
+ }
1055
+ const owned = [...ownedPathsBySpec(plan, specFiles)].map(([slug, paths]) => {
1056
+ const { files, folders } = pathItemsOf(paths);
1057
+ const items = {
1058
+ files,
1059
+ fileKeys: new Set(files.map((f) => f.key)),
1060
+ folders,
1061
+ interfaces: [],
1062
+ interfaceKeys: /* @__PURE__ */ new Set()
1063
+ };
1064
+ return { slug, items };
1065
+ });
1066
+ for (let i = 0; i < owned.length; i += 1) {
1067
+ for (let j = i + 1; j < owned.length; j += 1) {
1068
+ const shared = firstSharedItem(owned[i].items, owned[j].items);
1069
+ if (shared !== null)
1070
+ lines.push(refusal("B1", taskPlanLineText.B1(owned[i].slug, owned[j].slug, shared)));
1071
+ }
1072
+ }
1073
+ return lines;
1074
+ }
1075
+ function ownershipNotes(plan, specFiles) {
1076
+ const lines = [];
1077
+ const boundaryOf = boundaryBySlug(specFiles);
1078
+ for (const slug of planSpecNamesToRead(plan)) {
1079
+ if (boundaryOf.get(slug)?.kind === "none")
1080
+ lines.push(note("N3", taskPlanLineText.N3(slug)));
1081
+ }
1082
+ const owned = ownedPathsBySpec(plan, specFiles);
1083
+ const listed = new Set(planSpecNamesToRead(plan));
1084
+ for (const task of plan.tasks) {
1085
+ if (task.kind !== "build" || !Array.isArray(task.criteria))
1086
+ continue;
1087
+ const ownPaths = [];
1088
+ let anySection = false;
1089
+ for (const slug of distinct(task.criteria.map((c) => CITATION.exec(c)?.[1] ?? ""))) {
1090
+ const paths = listed.has(slug) ? owned.get(slug) : void 0;
1091
+ if (paths === void 0)
1092
+ continue;
1093
+ anySection = true;
1094
+ ownPaths.push(...paths);
1095
+ }
1096
+ if (!anySection)
1097
+ continue;
1098
+ const { files: ownedFiles, folders: ownedFolders } = pathItemsOf(ownPaths);
1099
+ const ownedKeys = new Set(ownedFiles.map((f) => f.key));
1100
+ const unowned = pathItemsOf(task.touches?.files ?? []).files.filter((touched) => !ownedKeys.has(touched.key) && !ownedFolders.some((folder) => touched.key.startsWith(folder.key)));
1101
+ if (unowned.length > 0) {
1102
+ lines.push(note("N2", taskPlanLineText.N2(task.id, distinct(unowned.map((f) => f.shown)))));
1103
+ }
1104
+ }
1105
+ return lines;
1106
+ }
841
1107
  function isWholeProjectCheck(task) {
842
1108
  return task.kind === "check" && task.wholeProject === true;
843
1109
  }
@@ -973,6 +1239,8 @@ function checkTaskPlan(plan, request) {
973
1239
  const tasks = plan.tasks;
974
1240
  const lines = [];
975
1241
  const system = request.system;
1242
+ const criteriaOf = specCriteriaBySlug(request.specFiles);
1243
+ const phasePlan = request.phasePlan;
976
1244
  const ids = new Set(tasks.map((task) => task.id));
977
1245
  const seen = /* @__PURE__ */ new Set();
978
1246
  const reportedTwice = /* @__PURE__ */ new Set();
@@ -1013,7 +1281,23 @@ function checkTaskPlan(plan, request) {
1013
1281
  if (system !== void 0 && system.partIds.length > 0 && mustNameParts(task) && namesNoPart(task)) {
1014
1282
  lines.push(refusal("O7", taskPlanLineText.O7(task.id, system.file, system.partIds)));
1015
1283
  }
1284
+ if (phasePlan !== void 0) {
1285
+ for (const found of partsNotInPlan([task], phasePlan.partIds)) {
1286
+ lines.push(refusal("O8", taskPlanLineText.O8(found.id, found.parts, phasePlan.file)));
1287
+ }
1288
+ }
1289
+ if (mustNameParts(task) && namesNoCriterion(task)) {
1290
+ lines.push(refusal("C4", taskPlanLineText.C4(task.id)));
1291
+ }
1292
+ if (Array.isArray(task.criteria)) {
1293
+ for (const citation of task.criteria) {
1294
+ const line = citationLine(task.id, citation, plan, request.specFiles, criteriaOf);
1295
+ if (line !== null)
1296
+ lines.push(line);
1297
+ }
1298
+ }
1016
1299
  }
1300
+ lines.push(...specLines(plan, request.specFiles));
1017
1301
  const { before, cycles } = waitsFor(tasks);
1018
1302
  for (const path of cycles)
1019
1303
  lines.push(refusal("O4", taskPlanLineText.O4Cycle(path)));
@@ -1034,6 +1318,7 @@ function checkTaskPlan(plan, request) {
1034
1318
  }
1035
1319
  }
1036
1320
  }
1321
+ lines.push(...ownershipLines(plan, request.specFiles));
1037
1322
  if (request.taskList !== void 0) {
1038
1323
  const file = request.taskList.file;
1039
1324
  if (file !== null && !Array.isArray(file) && file.phase === request.phase) {
@@ -1044,6 +1329,7 @@ function checkTaskPlan(plan, request) {
1044
1329
  lines.push({ code: "N1", text: taskPlanLineText.N1(request.phase), refuses: false });
1045
1330
  }
1046
1331
  }
1332
+ lines.push(...ownershipNotes(plan, request.specFiles));
1047
1333
  lines.push(...placementLines(tasks, before));
1048
1334
  const refused3 = lines.some((line) => line.refuses);
1049
1335
  if (!refused3)
@@ -1051,6 +1337,108 @@ function checkTaskPlan(plan, request) {
1051
1337
  return { lines, refused: refused3 };
1052
1338
  }
1053
1339
 
1340
+ // ../core/dist/spec-boundary.js
1341
+ var SPEC_BOUNDARY_OWNS_LABEL = "**Owns.**";
1342
+ var SPEC_BOUNDARY_LABELS = [
1343
+ SPEC_BOUNDARY_OWNS_LABEL,
1344
+ "**Out of boundary.**",
1345
+ "**Allowed dependencies.**",
1346
+ "**Revalidation triggers.**"
1347
+ ];
1348
+ var BOUNDARY_HEADING = /^## Boundary[ \t]*$/;
1349
+ var SECTION_HEADING2 = /^## /;
1350
+ var FENCE = /^ {0,3}(`{3,}|~{3,})/;
1351
+ var BULLET = /^- /;
1352
+ var INDENTED = /^[ \t]/;
1353
+ var ONE_SPAN = /^`([^`]+)`$/;
1354
+ function fenceTracker() {
1355
+ let open = null;
1356
+ return (line) => {
1357
+ const match = FENCE.exec(line);
1358
+ if (open === null) {
1359
+ if (match === null)
1360
+ return true;
1361
+ open = match[1];
1362
+ return false;
1363
+ }
1364
+ if (match !== null && match[1][0] === open[0] && match[1].length >= open.length) {
1365
+ const rest = line.slice(line.indexOf(match[1]) + match[1].length);
1366
+ if (rest.trim() === "")
1367
+ open = null;
1368
+ }
1369
+ return false;
1370
+ };
1371
+ }
1372
+ function itemOf(first, continued) {
1373
+ const head = first.trim();
1374
+ if (continued.length > 0)
1375
+ return { kind: "bad", text: [head, ...continued].join(" ") };
1376
+ const span = ONE_SPAN.exec(head);
1377
+ if (span === null)
1378
+ return { kind: "bad", text: head };
1379
+ return { kind: "path", path: span[1] };
1380
+ }
1381
+ function readSpecBoundary(markdown) {
1382
+ const lines = markdown.split(/\r?\n/);
1383
+ const outside = fenceTracker();
1384
+ let start = -1;
1385
+ for (let i = 0; i < lines.length; i += 1) {
1386
+ if (outside(lines[i]) && BOUNDARY_HEADING.test(lines[i])) {
1387
+ start = i;
1388
+ break;
1389
+ }
1390
+ }
1391
+ if (start < 0)
1392
+ return { kind: "none" };
1393
+ const owns = [];
1394
+ let ownsSeen = false;
1395
+ let listOpen = false;
1396
+ let first = null;
1397
+ let continued = [];
1398
+ const closeBullet = () => {
1399
+ if (first !== null)
1400
+ owns.push(itemOf(first, continued));
1401
+ first = null;
1402
+ continued = [];
1403
+ };
1404
+ const closeList = () => {
1405
+ closeBullet();
1406
+ listOpen = false;
1407
+ };
1408
+ for (let i = start + 1; i < lines.length; i += 1) {
1409
+ const line = lines[i];
1410
+ if (!outside(line)) {
1411
+ closeList();
1412
+ continue;
1413
+ }
1414
+ if (SECTION_HEADING2.test(line))
1415
+ break;
1416
+ const trimmed = line.trimEnd();
1417
+ if (SPEC_BOUNDARY_LABELS.includes(trimmed)) {
1418
+ closeList();
1419
+ if (trimmed === SPEC_BOUNDARY_OWNS_LABEL && !ownsSeen) {
1420
+ ownsSeen = true;
1421
+ listOpen = true;
1422
+ }
1423
+ continue;
1424
+ }
1425
+ if (!listOpen)
1426
+ continue;
1427
+ if (BULLET.test(line)) {
1428
+ closeBullet();
1429
+ first = line.slice(2);
1430
+ continue;
1431
+ }
1432
+ if (INDENTED.test(line) && line.trim() !== "" && first !== null) {
1433
+ continued.push(line.trim());
1434
+ continue;
1435
+ }
1436
+ closeList();
1437
+ }
1438
+ closeList();
1439
+ return { kind: "section", owns };
1440
+ }
1441
+
1054
1442
  // ../core/dist/agent-profile.js
1055
1443
  var AGENT_IDS = ["claude-code", "codex"];
1056
1444
  var CLAUDE_CODE_PROFILE = {
@@ -1148,6 +1536,222 @@ var AGENT_PROFILES = {
1148
1536
  };
1149
1537
  var ALL_HOOK_COMMANDS = new Set(AGENT_IDS.flatMap((id) => [...AGENT_PROFILES[id].commands]));
1150
1538
 
1539
+ // ../core/dist/worker-report.js
1540
+ var WORKER_REPORT_STATUSES = Object.freeze([
1541
+ "done",
1542
+ "needs-human-verification",
1543
+ "blocked-on-human",
1544
+ "failed"
1545
+ ]);
1546
+ var WORKER_REPORT_RULE_FIELDS = Object.freeze({
1547
+ W1: "Worker report",
1548
+ W2: "Status",
1549
+ W3: "Head",
1550
+ W4: "Head",
1551
+ W5: "Verification",
1552
+ W6: "Verification",
1553
+ W7: "Verification",
1554
+ W8: "Unverified"
1555
+ });
1556
+ var WORKER_REPORT_LINES = Object.freeze({
1557
+ noBlock: () => 'There is no "## Worker report" block in this report.',
1558
+ statusMissing: (statuses) => `The report has no Status line. Use one of: ${statuses.join(", ")}.`,
1559
+ statusUnknown: (found, statuses) => `The report's Status is "${found}". Use one of: ${statuses.join(", ")}.`,
1560
+ headMissing: () => "The report has no Head line. Give the full commit id the report is about.",
1561
+ headMalformed: (found) => `The report's Head is "${found}", which is not a full commit id.`,
1562
+ headDiffers: (found, head) => `The report is about commit ${found}, but the work's head is ${head}. Ask for a report on the current head.`,
1563
+ noVerification: () => "The report does not say what was run. List each check under Verification with its exit code.",
1564
+ noExitCode: (entry) => `"${entry}" under Verification does not give an exit code.`,
1565
+ nonZeroOnDone: (entry) => `"${entry}" exited with a code other than 0, so the work is not done.`,
1566
+ noUnverified: () => 'The report has no Unverified line. Say what was not checked, or "nothing".',
1567
+ clear: () => "The report is complete and names the commit it is about."
1568
+ });
1569
+ var FIELD_LINE = /^([A-Z][A-Za-z0-9 ]*):(.*)$/;
1570
+ var FULL_COMMIT = /^[0-9a-f]{40}$/;
1571
+ var EXIT_CODE = /(?:→|\bexit(?:ed)?(?:\s*code)?)\s*(\d+)/giu;
1572
+ function extractWorkerReportBlock(body) {
1573
+ const lines = String(body ?? "").replace(/\r\n/g, "\n").split("\n");
1574
+ const start = lines.findIndex((l) => /^##\s+Worker report\s*$/.test(l.trim()));
1575
+ if (start === -1)
1576
+ return null;
1577
+ const out = [];
1578
+ for (const line of lines.slice(start + 1)) {
1579
+ if (/^#{1,3}\s/.test(line.trim()))
1580
+ break;
1581
+ if (/^```/.test(line.trim()))
1582
+ continue;
1583
+ out.push(line);
1584
+ }
1585
+ return out;
1586
+ }
1587
+ function parseWorkerReportFields(blockLines) {
1588
+ const fields = /* @__PURE__ */ new Map();
1589
+ let current = null;
1590
+ for (const raw of blockLines) {
1591
+ const m = FIELD_LINE.exec(raw);
1592
+ if (m) {
1593
+ const name = m[1];
1594
+ if (fields.has(name)) {
1595
+ current = null;
1596
+ continue;
1597
+ }
1598
+ current = { inline: m[2].trim(), continuation: [] };
1599
+ fields.set(name, current);
1600
+ continue;
1601
+ }
1602
+ if (current)
1603
+ current.continuation.push(raw);
1604
+ }
1605
+ return fields;
1606
+ }
1607
+ function splitOutsideBackticks(text) {
1608
+ const parts = [];
1609
+ let buf = "";
1610
+ let inTick = false;
1611
+ for (let i = 0; i < text.length; i++) {
1612
+ const ch = text[i];
1613
+ if (ch === "`")
1614
+ inTick = !inTick;
1615
+ if (!inTick && ch === ";") {
1616
+ parts.push(buf);
1617
+ buf = "";
1618
+ continue;
1619
+ }
1620
+ if (!inTick && text.startsWith(" \xB7 ", i)) {
1621
+ parts.push(buf);
1622
+ buf = "";
1623
+ i += 2;
1624
+ continue;
1625
+ }
1626
+ buf += ch;
1627
+ }
1628
+ parts.push(buf);
1629
+ return parts.map((p) => p.trim()).filter(Boolean);
1630
+ }
1631
+ function verificationEntries(field) {
1632
+ if (!field)
1633
+ return [];
1634
+ const entries = field.inline ? splitOutsideBackticks(field.inline) : [];
1635
+ for (const line of field.continuation) {
1636
+ const m = /^\s*[-*]\s+(.*)$/.exec(line);
1637
+ if (m && m[1].trim())
1638
+ entries.push(m[1].trim());
1639
+ }
1640
+ return entries;
1641
+ }
1642
+ function verificationExitCode(entry) {
1643
+ let code = null;
1644
+ for (const m of entry.matchAll(EXIT_CODE))
1645
+ code = Number(m[1]);
1646
+ return code;
1647
+ }
1648
+ function checkWorkerReport(body, options = {}) {
1649
+ const statuses = options.statuses ?? WORKER_REPORT_STATUSES;
1650
+ const given = options.head ? options.head : null;
1651
+ const problems = [];
1652
+ const add = (id, line) => {
1653
+ problems.push({ id, field: WORKER_REPORT_RULE_FIELDS[id], line });
1654
+ };
1655
+ const block = extractWorkerReportBlock(body);
1656
+ if (block === null) {
1657
+ add("W1", WORKER_REPORT_LINES.noBlock());
1658
+ return { problems, headChecked: false, fields: {} };
1659
+ }
1660
+ const fields = parseWorkerReportFields(block);
1661
+ const inline = (name) => fields.get(name)?.inline;
1662
+ const status = inline("Status");
1663
+ if (status === void 0)
1664
+ add("W2", WORKER_REPORT_LINES.statusMissing(statuses));
1665
+ else if (!statuses.includes(status))
1666
+ add("W2", WORKER_REPORT_LINES.statusUnknown(status, statuses));
1667
+ const head = inline("Head");
1668
+ let headChecked = false;
1669
+ if (head === void 0)
1670
+ add("W3", WORKER_REPORT_LINES.headMissing());
1671
+ else if (!FULL_COMMIT.test(head))
1672
+ add("W3", WORKER_REPORT_LINES.headMalformed(head));
1673
+ else if (given !== null) {
1674
+ headChecked = true;
1675
+ if (head !== given)
1676
+ add("W4", WORKER_REPORT_LINES.headDiffers(head, given));
1677
+ }
1678
+ const entries = verificationEntries(fields.get("Verification"));
1679
+ if (entries.length === 0)
1680
+ add("W5", WORKER_REPORT_LINES.noVerification());
1681
+ const codes = entries.map((entry) => ({ entry, code: verificationExitCode(entry) }));
1682
+ for (const { entry, code } of codes) {
1683
+ if (code === null)
1684
+ add("W6", WORKER_REPORT_LINES.noExitCode(entry));
1685
+ }
1686
+ if (status === "done") {
1687
+ for (const { entry, code } of codes) {
1688
+ if (code !== null && code !== 0)
1689
+ add("W7", WORKER_REPORT_LINES.nonZeroOnDone(entry));
1690
+ }
1691
+ }
1692
+ if (!fields.has("Unverified"))
1693
+ add("W8", WORKER_REPORT_LINES.noUnverified());
1694
+ const flat = {};
1695
+ for (const [name, field] of fields)
1696
+ flat[name] = field.inline;
1697
+ return { problems, headChecked, fields: flat };
1698
+ }
1699
+
1700
+ // ../core/dist/task-ready.js
1701
+ var READY_DEFAULT_MINUTES = 60;
1702
+ function minutesOf(row) {
1703
+ return typeof row.minutes === "number" ? row.minutes : READY_DEFAULT_MINUTES;
1704
+ }
1705
+ function readyTasks(rows) {
1706
+ const byId = /* @__PURE__ */ new Map();
1707
+ for (const row of rows) {
1708
+ if (!byId.has(row.id))
1709
+ byId.set(row.id, row);
1710
+ }
1711
+ const children = /* @__PURE__ */ new Map();
1712
+ for (const id of byId.keys())
1713
+ children.set(id, []);
1714
+ for (const row of byId.values()) {
1715
+ for (const dep of row.dependsOn)
1716
+ children.get(dep)?.push(row.id);
1717
+ }
1718
+ const memo = /* @__PURE__ */ new Map();
1719
+ const pathFrom = (id) => {
1720
+ const known = memo.get(id);
1721
+ if (known !== void 0)
1722
+ return known;
1723
+ memo.set(id, 0);
1724
+ let best = 0;
1725
+ for (const childId of children.get(id) ?? []) {
1726
+ const child = byId.get(childId);
1727
+ if (child === void 0 || child.progress === "completed")
1728
+ continue;
1729
+ best = Math.max(best, pathFrom(childId));
1730
+ }
1731
+ const total = minutesOf(byId.get(id)) + best;
1732
+ memo.set(id, total);
1733
+ return total;
1734
+ };
1735
+ const startable = [];
1736
+ const person = [];
1737
+ for (const row of byId.values()) {
1738
+ if (row.progress !== "pending")
1739
+ continue;
1740
+ if (!row.dependsOn.every((dep) => byId.get(dep)?.progress === "completed"))
1741
+ continue;
1742
+ if (row.person) {
1743
+ person.push(row.id);
1744
+ continue;
1745
+ }
1746
+ startable.push({ id: row.id, path: 0, tieRank: row.tieRank });
1747
+ }
1748
+ for (const entry of startable)
1749
+ entry.path = pathFrom(entry.id);
1750
+ startable.sort((a, b) => b.path - a.path || a.tieRank - b.tieRank || a.id.localeCompare(b.id));
1751
+ person.sort((a, b) => a.localeCompare(b));
1752
+ return { ready: startable.map((entry) => entry.id), person };
1753
+ }
1754
+
1151
1755
  // ../events/dist/result.js
1152
1756
  import { z as z3 } from "zod";
1153
1757
  var firedGuardSchema = z3.object({
@@ -2293,16 +2897,16 @@ function planCrossFieldProblems(raw, description) {
2293
2897
  if (!isRecord(raw) || !Array.isArray(raw.changes))
2294
2898
  return problems;
2295
2899
  const known = new Set(description.parts.map((p) => p.id));
2296
- const named = /* @__PURE__ */ new Map();
2900
+ const named2 = /* @__PURE__ */ new Map();
2297
2901
  raw.changes.forEach((change, i) => {
2298
2902
  if (!isRecord(change) || typeof change.part !== "string")
2299
2903
  return;
2300
2904
  if (!known.has(change.part)) {
2301
2905
  problems.push({ path: `changes.${i}.part`, message: `names no part in the system description: ${shown(change.part)}` });
2302
2906
  }
2303
- const earlier = named.get(change.part);
2907
+ const earlier = named2.get(change.part);
2304
2908
  if (earlier === void 0) {
2305
- named.set(change.part, i);
2909
+ named2.set(change.part, i);
2306
2910
  } else {
2307
2911
  problems.push({ path: `changes.${i}.part`, message: `${shown(change.part)} is already listed at changes.${earlier}` });
2308
2912
  }
@@ -3600,8 +4204,8 @@ function contractProblems(raw, description) {
3600
4204
  if (!isRecord(contract) || typeof contract.from !== "string" || typeof contract.to !== "string")
3601
4205
  return;
3602
4206
  const { from, to } = contract;
3603
- const named = description.connections.some((c) => c.from === from && c.to === to || c.from === to && c.to === from);
3604
- if (!named) {
4207
+ const named2 = description.connections.some((c) => c.from === from && c.to === to || c.from === to && c.to === from);
4208
+ if (!named2) {
3605
4209
  problems.push({
3606
4210
  path: `contracts.${i}`,
3607
4211
  message: `names no connection in the system description: ${shown(from)} to ${shown(to)}`
@@ -7602,12 +8206,12 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
7602
8206
  if (ignored === null)
7603
8207
  return null;
7604
8208
  const sorted = [...ignored].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
7605
- const named = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
8209
+ const named2 = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
7606
8210
  return {
7607
- path: named.path,
7608
- pattern: named.pattern,
7609
- source: named.source,
7610
- line: named.line,
8211
+ path: named2.path,
8212
+ pattern: named2.pattern,
8213
+ source: named2.source,
8214
+ line: named2.line,
7611
8215
  others: sorted.length - 1
7612
8216
  };
7613
8217
  }
@@ -10460,8 +11064,8 @@ function renderBlock(content, layout, environment) {
10460
11064
  function renderLine(content, environment) {
10461
11065
  const unicode = decideUnicode(environment);
10462
11066
  const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
10463
- const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
10464
- const raw = `${named} ${dot} ${content.url}`;
11067
+ const named2 = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
11068
+ const raw = `${named2} ${dot} ${content.url}`;
10465
11069
  return unicode ? raw : toAscii(raw);
10466
11070
  }
10467
11071
  function renderBanner(content, environment = {}) {
@@ -11987,10 +12591,10 @@ function leftForYouLines(left, p) {
11987
12591
  return lines;
11988
12592
  }
11989
12593
  function shownBy(steps) {
11990
- const named = steps.filter((s) => s.name !== void 0).length;
11991
- if (named === 0)
12594
+ const named2 = steps.filter((s) => s.name !== void 0).length;
12595
+ if (named2 === 0)
11992
12596
  return "Each is shown by the method's own label for it:";
11993
- if (named === steps.length)
12597
+ if (named2 === steps.length)
11994
12598
  return "Each is shown by the method's own name for it:";
11995
12599
  return "Each is shown by the method's own name for it, or by its label where the record holds no name:";
11996
12600
  }
@@ -12715,7 +13319,7 @@ function isMissingRoute(method, status, body) {
12715
13319
  import { join as join17 } from "node:path";
12716
13320
 
12717
13321
  // dist/cli-contract.js
12718
- var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "uninstall", "change", "pace", "prove", "preview", "doctor", "restore"];
13322
+ var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "check-report", "ready", "uninstall", "change", "pace", "prove", "preview", "doctor", "restore"];
12719
13323
  var CONTRACTS = [
12720
13324
  { verb: "install", required: [], conditional: [] },
12721
13325
  { verb: "check-drift", required: [], conditional: [] },
@@ -12725,12 +13329,24 @@ var CONTRACTS = [
12725
13329
  { verb: "plan", required: [], conditional: [] },
12726
13330
  { verb: "review", required: [], conditional: [] },
12727
13331
  { verb: "check-tasks", required: [], conditional: [] },
13332
+ { verb: "ready", required: [], conditional: [] },
12728
13333
  { verb: "uninstall", required: [], conditional: [] },
12729
13334
  { verb: "pace", required: [], conditional: [] },
12730
13335
  { verb: "prove", required: [], conditional: [] },
12731
13336
  { verb: "preview", required: [], conditional: [] },
12732
13337
  { verb: "doctor", required: [], conditional: [] },
12733
13338
  { verb: "restore", required: [], conditional: [] },
13339
+ {
13340
+ verb: "check-report",
13341
+ required: [
13342
+ {
13343
+ flag: "--head",
13344
+ takesValue: true,
13345
+ missingMessage: "halfcycle check-report: --head is required \u2014 give the full commit id of the work the report is about, so the report can be checked against it."
13346
+ }
13347
+ ],
13348
+ conditional: []
13349
+ },
12734
13350
  {
12735
13351
  verb: "open-phase",
12736
13352
  required: [
@@ -13457,16 +14073,16 @@ function parseChangeArgs(rest) {
13457
14073
  return notAChangeId("up", change);
13458
14074
  return { ok: true, args: { form: "up", to, target: { kind: "change", sortingId: change }, ...where } };
13459
14075
  }
13460
- const named = phase ?? "";
13461
- if (!isValidPhaseIdentity(named)) {
13462
- return { ok: false, line: `halfcycle change up: '${named}' cannot be a phase name. Nothing was sent.
14076
+ const named2 = phase ?? "";
14077
+ if (!isValidPhaseIdentity(named2)) {
14078
+ return { ok: false, line: `halfcycle change up: '${named2}' cannot be a phase name. Nothing was sent.
13463
14079
  `, exitCode: 2 };
13464
14080
  }
13465
14081
  if (to === "mini") {
13466
14082
  return { ok: false, line: `halfcycle change up: ${CHANGE_PATH_WORDS["only-up"]} Nothing was sent.
13467
14083
  `, exitCode: 1 };
13468
14084
  }
13469
- return { ok: true, args: { form: "up", to, target: { kind: "phase", phase: named }, ...where } };
14085
+ return { ok: true, args: { form: "up", to, target: { kind: "phase", phase: named2 }, ...where } };
13470
14086
  }
13471
14087
  var OBSERVE_CHANGE_UNREACHABLE = { outcome: "not-sent", reason: "unreachable" };
13472
14088
  function readObserveChangeLine(stdout) {
@@ -13939,11 +14555,11 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
13939
14555
  return { ok: false, line: `${p} ${fileRel} is not valid JSON (${why}). Fix it, then run this again. Nothing was sent.
13940
14556
  ` };
13941
14557
  }
13942
- const named = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
13943
- if (isValidPhaseIdentity(named) && renderPhaseSegment(named) !== renderPhaseSegment(phase)) {
14558
+ const named2 = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
14559
+ if (isValidPhaseIdentity(named2) && renderPhaseSegment(named2) !== renderPhaseSegment(phase)) {
13944
14560
  return {
13945
14561
  ok: false,
13946
- line: `${p} ${fileRel} is the plan for phase "${String(named)}", not phase "${phase}". Run this for the phase the plan names, or correct the plan's phase. Nothing was sent.
14562
+ line: `${p} ${fileRel} is the plan for phase "${String(named2)}", not phase "${phase}". Run this for the phase the plan names, or correct the plan's phase. Nothing was sent.
13947
14563
  `
13948
14564
  };
13949
14565
  }
@@ -14719,10 +15335,10 @@ async function openPhaseReview(credential, phase, request, observe, repoRoot, se
14719
15335
  }
14720
15336
  };
14721
15337
  const parsed = openReviewRequestSchema.safeParse(body);
14722
- const named = phaseNameSchema.safeParse(phase);
14723
- if (!parsed.success || !named.success) {
15338
+ const named2 = phaseNameSchema.safeParse(phase);
15339
+ if (!parsed.success || !named2.success) {
14724
15340
  const issues = [
14725
- ...named.success ? [] : named.error.issues.map((i) => `the phase name: ${i.message}`),
15341
+ ...named2.success ? [] : named2.error.issues.map((i) => `the phase name: ${i.message}`),
14726
15342
  ...parsed.success ? [] : parsed.error.issues.map((i) => `${describeIssuePath(i.path)}: ${i.message}`)
14727
15343
  ];
14728
15344
  throw new CloseRequestInvalid(`that close is not one this platform accepts, so nothing was sent: ${issues.join("; ")}.`);
@@ -15605,6 +16221,7 @@ function parseCheckTasksArgs(argv) {
15605
16221
  return { ok: false };
15606
16222
  return { ok: true, phase: positionals[0], withTaskList, repoRoot };
15607
16223
  }
16224
+ var MISSING_PLAN_LINE = `there is no ${TASK_PLAN_FILE} in this project yet. Run the task-graph step to write it, then run this again.`;
15608
16225
  function issuePlace(rel, path) {
15609
16226
  return path.length === 0 ? rel : `${rel}.${path.map(String).join(".")}`;
15610
16227
  }
@@ -15661,7 +16278,42 @@ function readSystemParts(repoRoot) {
15661
16278
  ` + problemLines(file.value, checked.problems)
15662
16279
  };
15663
16280
  }
15664
- return { kind: "ok", partIds: checked.description.parts.map((part) => part.id) };
16281
+ return { kind: "ok", partIds: checked.description.parts.map((part) => part.id), description: checked.description };
16282
+ }
16283
+ function readPhasePlanParts(repoRoot, phase, description) {
16284
+ if (!isValidPhaseIdentity(phase))
16285
+ return void 0;
16286
+ const file = planFileRel(phase);
16287
+ let raw;
16288
+ try {
16289
+ raw = JSON.parse(readFileSync18(join21(repoRoot, file), "utf-8"));
16290
+ } catch {
16291
+ return void 0;
16292
+ }
16293
+ const checked = checkPhasePlan(raw, description);
16294
+ if (!checked.ok || renderPhaseSegment(checked.plan.phase) !== renderPhaseSegment(phase))
16295
+ return void 0;
16296
+ return { file, partIds: checked.plan.changes.map((change) => change.part) };
16297
+ }
16298
+ var SPEC_FOLDER = LAYER_ARTEFACT_PATHS.l4[0];
16299
+ function readPlanSpecFiles(repoRoot, plan) {
16300
+ const entries = [];
16301
+ for (const slug of planSpecNamesToRead(plan)) {
16302
+ const rel = `${SPEC_FOLDER}/${slug}.md`;
16303
+ let text;
16304
+ try {
16305
+ text = readFileSync18(join21(repoRoot, rel), "utf-8");
16306
+ } catch (err) {
16307
+ const code = err.code;
16308
+ if (code === "ENOENT" || code === "ENOTDIR") {
16309
+ entries.push({ slug, criteria: null, boundary: null });
16310
+ continue;
16311
+ }
16312
+ return { kind: "bad", line: `${rel} could not be read. Check that it is a file you can open, then run this again.` };
16313
+ }
16314
+ entries.push({ slug, criteria: readSpecCriteria(text), boundary: readSpecBoundary(text) });
16315
+ }
16316
+ return { kind: "ok", specFiles: { folder: SPEC_FOLDER, entries } };
15665
16317
  }
15666
16318
  function runCheckTasks(repoRoot, phase, withTaskList) {
15667
16319
  const p = "halfcycle check-tasks:";
@@ -15669,7 +16321,7 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
15669
16321
  if (planRead.kind === "missing") {
15670
16322
  return {
15671
16323
  stdout: "",
15672
- stderr: `${p} there is no ${TASK_PLAN_FILE} in this project yet. Run the task-graph step to write it, then run this again.
16324
+ stderr: `${p} ${MISSING_PLAN_LINE}
15673
16325
  `,
15674
16326
  exitCode: 2
15675
16327
  };
@@ -15692,16 +16344,207 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
15692
16344
  if (system.kind === "bad") {
15693
16345
  return { stdout: "", stderr: `${p} ${system.line}`, exitCode: 2 };
15694
16346
  }
16347
+ const specs = readPlanSpecFiles(repoRoot, plan);
16348
+ if (specs.kind === "bad") {
16349
+ return { stdout: "", stderr: `${p} ${specs.line}
16350
+ `, exitCode: 2 };
16351
+ }
16352
+ const phasePlan = system.kind === "ok" ? readPhasePlanParts(repoRoot, phase, system.description) : void 0;
15695
16353
  const result = checkTaskPlan(plan, {
15696
16354
  phase,
15697
16355
  taskList,
15698
- ...system.kind === "ok" ? { system: { file: SYSTEM_DESCRIPTION_REL, partIds: system.partIds } } : {}
16356
+ specFiles: specs.specFiles,
16357
+ ...system.kind === "ok" ? { system: { file: SYSTEM_DESCRIPTION_REL, partIds: system.partIds } } : {},
16358
+ ...phasePlan !== void 0 ? { phasePlan } : {}
15699
16359
  });
15700
16360
  const stdout = result.lines.map((line) => `${line.text}
15701
16361
  `).join("");
15702
16362
  return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
15703
16363
  }
15704
16364
 
16365
+ // dist/check-report.js
16366
+ import { readFileSync as readFileSync19 } from "node:fs";
16367
+ var CHECK_REPORT_USAGE = `${HALFCYCLE_COMMAND} check-report <file | -> --head <sha>`;
16368
+ function parseCheckReportArgs(argv) {
16369
+ const positionals = [];
16370
+ let head;
16371
+ for (let i = 0; i < argv.length; i++) {
16372
+ const arg = argv[i];
16373
+ if (arg === "--head") {
16374
+ const value = argv[i + 1];
16375
+ if (head !== void 0 || value === void 0 || value.trim() === "" || value.startsWith("-"))
16376
+ return { ok: false };
16377
+ head = value.trim();
16378
+ i++;
16379
+ continue;
16380
+ }
16381
+ if (arg !== "-" && arg.startsWith("-"))
16382
+ return { ok: false };
16383
+ positionals.push(arg);
16384
+ }
16385
+ if (positionals.length !== 1 || head === void 0)
16386
+ return { ok: false };
16387
+ return { ok: true, source: positionals[0], head };
16388
+ }
16389
+ function runCheckReport(source, head, deps = {}) {
16390
+ const p = "halfcycle check-report:";
16391
+ let body;
16392
+ try {
16393
+ body = source === "-" ? (deps.readStdin ?? (() => readFileSync19(0, "utf-8")))() : readFileSync19(source, "utf-8");
16394
+ } catch (err) {
16395
+ const code = err.code;
16396
+ const what = source === "-" ? "The report could not be read from standard input" : `${source} could not be read`;
16397
+ const line = code === "ENOENT" && source !== "-" ? `${p} there is no file at ${source}. Save the report to a file, then run this again.` : `${p} ${what}. Check that it is a file you can open, then run this again.`;
16398
+ return { stdout: "", stderr: `${line}
16399
+ `, exitCode: 2 };
16400
+ }
16401
+ const result = checkWorkerReport(body, { head, statuses: WORKER_REPORT_STATUSES });
16402
+ if (result.problems.length === 0) {
16403
+ return { stdout: `${WORKER_REPORT_LINES.clear()}
16404
+ `, stderr: "", exitCode: 0 };
16405
+ }
16406
+ return { stdout: result.problems.map((problem) => `${problem.line}
16407
+ `).join(""), stderr: "", exitCode: 1 };
16408
+ }
16409
+
16410
+ // dist/ready.js
16411
+ var READY_USAGE = `${HALFCYCLE_COMMAND} ready <phase> [--cap <n>] [--repo <root>]`;
16412
+ var STATES_TEXT = buildTaskStateSchema.options.join(", ");
16413
+ var READY_LINES = {
16414
+ start: (id, title) => `Ready to start: ${named(id, title)}`,
16415
+ pastCap: (id, title) => `Ready, but past the number allowed now: ${named(id, title)}`,
16416
+ person: (id, title) => `Waiting on a person: ${named(id, title)}`,
16417
+ noCap: () => "No number was given, so every task that can start is listed.",
16418
+ nothing: () => "Nothing can start now.",
16419
+ noTaskList: () => `There is no task list at ${TASK_LIST_FILE}, so which tasks are done is not known. Write the task list for this phase, then run this again.`,
16420
+ otherPhase: (phase) => `The task list in ${TASK_LIST_FILE} is not for "${phase}", so which tasks are done is not known. Write the task list for this phase, then run this again.`,
16421
+ noState: (id) => `"${id}" in ${TASK_LIST_FILE} does not give its state as one of: ${STATES_TEXT}.`
16422
+ };
16423
+ function named(id, title) {
16424
+ return title === void 0 ? id : `${id}, ${title}`;
16425
+ }
16426
+ function parseReadyArgs(argv) {
16427
+ const positionals = [];
16428
+ let repoRoot;
16429
+ let cap;
16430
+ for (let i = 0; i < argv.length; i++) {
16431
+ const arg = argv[i];
16432
+ if (arg === "--repo") {
16433
+ const value = argv[i + 1];
16434
+ if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
16435
+ return { ok: false };
16436
+ repoRoot = value;
16437
+ i++;
16438
+ continue;
16439
+ }
16440
+ if (arg === "--cap") {
16441
+ const value = argv[i + 1];
16442
+ if (cap !== void 0 || value === void 0 || !/^[0-9]+$/.test(value))
16443
+ return { ok: false };
16444
+ const n = Number(value);
16445
+ if (!Number.isSafeInteger(n))
16446
+ return { ok: false };
16447
+ cap = n;
16448
+ i++;
16449
+ continue;
16450
+ }
16451
+ if (arg.startsWith("-"))
16452
+ return { ok: false };
16453
+ positionals.push(arg);
16454
+ }
16455
+ if (positionals.length !== 1)
16456
+ return { ok: false };
16457
+ return { ok: true, phase: positionals[0], cap, repoRoot };
16458
+ }
16459
+ var PROGRESS = {
16460
+ pending: "pending",
16461
+ completed: "completed",
16462
+ "in-flight": "other",
16463
+ failed: "other",
16464
+ blocked: "other"
16465
+ };
16466
+ function readyRowsFromFiles(plan, taskList) {
16467
+ const listed = /* @__PURE__ */ new Map();
16468
+ for (const entry of taskList.tasks) {
16469
+ if (!listed.has(entry.id))
16470
+ listed.set(entry.id, entry);
16471
+ }
16472
+ const rows = [];
16473
+ const lines = [];
16474
+ plan.tasks.forEach((task, place2) => {
16475
+ const entry = listed.get(task.id);
16476
+ const state = buildTaskStateSchema.safeParse(entry?.state);
16477
+ if (entry === void 0 || !state.success) {
16478
+ lines.push(READY_LINES.noState(task.id));
16479
+ return;
16480
+ }
16481
+ const title = typeof entry.title === "string" && entry.title !== "" ? entry.title : void 0;
16482
+ const minutes = typeof entry.budgetMinutes === "number" ? entry.budgetMinutes : void 0;
16483
+ rows.push({
16484
+ id: task.id,
16485
+ dependsOn: task.dependsOn,
16486
+ progress: PROGRESS[state.data],
16487
+ person: task.heldBy === "person" || entry.agent === HUMAN_AGENT,
16488
+ ...minutes === void 0 ? {} : { minutes },
16489
+ tieRank: place2,
16490
+ title
16491
+ });
16492
+ });
16493
+ return lines.length > 0 ? { ok: false, lines } : { ok: true, rows };
16494
+ }
16495
+ function failed2(stderr) {
16496
+ return { stdout: "", stderr, exitCode: 2 };
16497
+ }
16498
+ function runReady(repoRoot, phase, cap) {
16499
+ const p = "halfcycle ready:";
16500
+ const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
16501
+ if (planRead.kind === "missing")
16502
+ return failed2(`${p} ${MISSING_PLAN_LINE}
16503
+ `);
16504
+ if (planRead.kind === "bad")
16505
+ return failed2(`${p} ${planRead.line}
16506
+ `);
16507
+ const plan = planRead.value;
16508
+ const listRead = readJsonFile(repoRoot, TASK_LIST_FILE, (raw) => taskListFileSchema.safeParse(raw));
16509
+ if (listRead.kind === "bad")
16510
+ return failed2(`${p} ${listRead.line}
16511
+ `);
16512
+ const list2 = listRead.kind === "ok" ? listRead.value : null;
16513
+ const specs = readPlanSpecFiles(repoRoot, plan);
16514
+ if (specs.kind === "bad")
16515
+ return failed2(`${p} ${specs.line}
16516
+ `);
16517
+ const check = checkTaskPlan(plan, { phase, taskList: { file: list2 }, specFiles: specs.specFiles });
16518
+ if (check.refused) {
16519
+ return { stdout: check.lines.map((line) => `${line.text}
16520
+ `).join(""), stderr: "", exitCode: 1 };
16521
+ }
16522
+ if (list2 === null)
16523
+ return failed2(`${READY_LINES.noTaskList()}
16524
+ `);
16525
+ if (Array.isArray(list2) || list2.phase !== phase)
16526
+ return failed2(`${READY_LINES.otherPhase(phase)}
16527
+ `);
16528
+ const read = readyRowsFromFiles(plan, list2);
16529
+ if (!read.ok)
16530
+ return failed2(read.lines.map((line) => `${line}
16531
+ `).join(""));
16532
+ const titles = new Map(read.rows.map((row) => [row.id, row.title]));
16533
+ const set = readyTasks(read.rows);
16534
+ const out = [];
16535
+ if (cap === void 0)
16536
+ out.push(READY_LINES.noCap());
16537
+ set.ready.forEach((id, i) => {
16538
+ out.push(cap === void 0 || i < cap ? READY_LINES.start(id, titles.get(id)) : READY_LINES.pastCap(id, titles.get(id)));
16539
+ });
16540
+ for (const id of set.person)
16541
+ out.push(READY_LINES.person(id, titles.get(id)));
16542
+ if (set.ready.length === 0 && set.person.length === 0)
16543
+ out.push(READY_LINES.nothing());
16544
+ return { stdout: out.map((line) => `${line}
16545
+ `).join(""), stderr: "", exitCode: 0 };
16546
+ }
16547
+
15705
16548
  // dist/prove.js
15706
16549
  import { spawn as spawn2 } from "node:child_process";
15707
16550
  var PROVE_USAGE = `${HALFCYCLE_COMMAND} prove <phase> <task> --label "<label>" --before -- <test command>
@@ -15754,7 +16597,7 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
15754
16597
  }
15755
16598
 
15756
16599
  // dist/uninstall.js
15757
- import { lstatSync as lstatSync3, readFileSync as readFileSync19, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
16600
+ import { lstatSync as lstatSync3, readFileSync as readFileSync20, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
15758
16601
  import { homedir as homedir5 } from "node:os";
15759
16602
  import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
15760
16603
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
@@ -15797,7 +16640,7 @@ function readMergedJson(targetRepo, rule) {
15797
16640
  let text;
15798
16641
  let value;
15799
16642
  try {
15800
- text = readFileSync19(path, "utf-8");
16643
+ text = readFileSync20(path, "utf-8");
15801
16644
  } catch {
15802
16645
  text = "";
15803
16646
  }
@@ -15822,7 +16665,7 @@ function validate(options, rules) {
15822
16665
  let manifest = null;
15823
16666
  if (lexists(manifestPath)) {
15824
16667
  try {
15825
- manifest = parseManifest(readFileSync19(manifestPath, "utf-8"));
16668
+ manifest = parseManifest(readFileSync20(manifestPath, "utf-8"));
15826
16669
  } catch (err) {
15827
16670
  const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
15828
16671
  throw new Refusal(`${INSTALL_MANIFEST_REL} cannot be read (${why}), so nothing was removed. Restore it from git, or run "${HALFCYCLE_COMMAND}" once to rewrite it, then run "${HALFCYCLE_COMMAND} uninstall" again.`);
@@ -15832,7 +16675,7 @@ function validate(options, rules) {
15832
16675
  let pinReadable = true;
15833
16676
  if (lexists(pinPath)) {
15834
16677
  try {
15835
- const pin = JSON.parse(readFileSync19(pinPath, "utf-8"));
16678
+ const pin = JSON.parse(readFileSync20(pinPath, "utf-8"));
15836
16679
  const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
15837
16680
  engagementId = typeof id === "string" && id !== "" ? id : void 0;
15838
16681
  } catch {
@@ -15899,7 +16742,7 @@ function statusLineClaimedFromTree(targetRepo, writeSet) {
15899
16742
  try {
15900
16743
  if (symlinkOnPath(targetRepo, SETTINGS_REL))
15901
16744
  return {};
15902
- const value = JSON.parse(readFileSync19(join22(targetRepo, SETTINGS_REL), "utf-8"));
16745
+ const value = JSON.parse(readFileSync20(join22(targetRepo, SETTINGS_REL), "utf-8"));
15903
16746
  if (!isPlainObject3(value) || !("statusLine" in value))
15904
16747
  return {};
15905
16748
  const hash = canonicalSha256(value["statusLine"]);
@@ -15913,7 +16756,7 @@ function noRecord(targetRepo, writeSet) {
15913
16756
  const claim = (rel, header, lines) => {
15914
16757
  let text;
15915
16758
  try {
15916
- text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync19(join22(targetRepo, rel), "utf-8");
16759
+ text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync20(join22(targetRepo, rel), "utf-8");
15917
16760
  } catch {
15918
16761
  text = "";
15919
16762
  }
@@ -16195,7 +17038,7 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
16195
17038
  const abs = join22(targetRepo, rel);
16196
17039
  let bytes;
16197
17040
  try {
16198
- bytes = readFileSync19(abs);
17041
+ bytes = readFileSync20(abs);
16199
17042
  } catch {
16200
17043
  return;
16201
17044
  }
@@ -16222,7 +17065,7 @@ function removeSelfBuildCrewProfile(targetRepo, rel, text, lists, name) {
16222
17065
  const abs = join22(targetRepo, rel);
16223
17066
  let bytes;
16224
17067
  try {
16225
- bytes = readFileSync19(abs);
17068
+ bytes = readFileSync20(abs);
16226
17069
  } catch {
16227
17070
  return;
16228
17071
  }
@@ -16333,17 +17176,17 @@ function uninstall(options) {
16333
17176
  const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
16334
17177
  const deleteWhenEmpty = (created) => created || checked.manifest === null;
16335
17178
  const deleteCodexWhenEmpty = (record2) => record2 === void 0 || deleteWhenEmpty(record2.created);
16336
- const named = /* @__PURE__ */ new Set();
17179
+ const named2 = /* @__PURE__ */ new Set();
16337
17180
  const name = (list2, text, path) => {
16338
17181
  list2.push(text);
16339
17182
  if (path !== void 0)
16340
- named.add(path);
17183
+ named2.add(path);
16341
17184
  };
16342
17185
  const settingsEmptyGoes = deleteWhenEmpty(manifest.settings.created);
16343
17186
  applyMergedJson(targetRepo, rules.settings, checked.settings, settingsEmptyGoes, result, name, (value) => {
16344
17187
  const { changed, refused: refused3 } = unmergeSettings(value, manifest.settings, writeSet);
16345
- for (const note of refused3)
16346
- name(result.leftAlone, note);
17188
+ for (const note2 of refused3)
17189
+ name(result.leftAlone, note2);
16347
17190
  return { changed, adopted: manifest.settings.adopted };
16348
17191
  });
16349
17192
  const mcpRecord = manifest.mcp ?? BEFORE_UNKNOWN_MCP;
@@ -16403,17 +17246,17 @@ function uninstall(options) {
16403
17246
  removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
16404
17247
  try {
16405
17248
  rmSync6(join22(targetRepo, INSTALL_MANIFEST_REL), { force: true });
16406
- named.add(INSTALL_MANIFEST_REL);
17249
+ named2.add(INSTALL_MANIFEST_REL);
16407
17250
  } catch {
16408
17251
  name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
16409
17252
  }
16410
17253
  removeEmptyDir(targetRepo, ".halfcycle", writeSet, result, name);
16411
17254
  } else {
16412
- named.add(BUNDLE_PIN_REL);
16413
- named.add(INSTALL_MANIFEST_REL);
17255
+ named2.add(BUNDLE_PIN_REL);
17256
+ named2.add(INSTALL_MANIFEST_REL);
16414
17257
  }
16415
17258
  for (const path of filesUnder(targetRepo, ".halfcycle")) {
16416
- if (!named.has(path))
17259
+ if (!named2.has(path))
16417
17260
  name(result.leftAlone, path, path);
16418
17261
  }
16419
17262
  const exitCode = result.failed.length > 0 || credentialFailed || credential.kind === "no-project-id" ? 1 : 0;
@@ -16453,7 +17296,7 @@ function removeCodexConfigBlock(targetRepo, record2, deleteWhenEmpty, lists, nam
16453
17296
  try {
16454
17297
  if (!lstatSync3(path).isFile())
16455
17298
  return;
16456
- text = readFileSync19(path, "utf-8");
17299
+ text = readFileSync20(path, "utf-8");
16457
17300
  } catch {
16458
17301
  return;
16459
17302
  }
@@ -16502,7 +17345,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
16502
17345
  if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
16503
17346
  let bytes;
16504
17347
  try {
16505
- bytes = readFileSync19(abs);
17348
+ bytes = readFileSync20(abs);
16506
17349
  } catch {
16507
17350
  name(lists.failed, path, path);
16508
17351
  return;
@@ -16544,7 +17387,7 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
16544
17387
  const path = join22(targetRepo, rel);
16545
17388
  let before;
16546
17389
  try {
16547
- before = readFileSync19(path, "utf-8");
17390
+ before = readFileSync20(path, "utf-8");
16548
17391
  } catch {
16549
17392
  return [];
16550
17393
  }
@@ -16669,7 +17512,7 @@ function renderUninstallReport(result, home) {
16669
17512
  }
16670
17513
 
16671
17514
  // dist/banner-facts.js
16672
- import { readFileSync as readFileSync20 } from "node:fs";
17515
+ import { readFileSync as readFileSync21 } from "node:fs";
16673
17516
  import { basename as basename4, join as join23, resolve as resolve6 } from "node:path";
16674
17517
  var BRAND_URL = "halfcycle.ai";
16675
17518
  var TAGLINE = [
@@ -16683,7 +17526,7 @@ var TAGLINE_NARROW = [
16683
17526
  ];
16684
17527
  function bundleVersion() {
16685
17528
  try {
16686
- const pkg = JSON.parse(readFileSync20(join23(BUNDLE_ROOT, "package.json"), "utf-8"));
17529
+ const pkg = JSON.parse(readFileSync21(join23(BUNDLE_ROOT, "package.json"), "utf-8"));
16687
17530
  return typeof pkg.version === "string" ? pkg.version : void 0;
16688
17531
  } catch {
16689
17532
  return void 0;
@@ -16921,7 +17764,7 @@ async function runPace(repoRoot, start, deps) {
16921
17764
 
16922
17765
  // dist/restore.js
16923
17766
  import { spawnSync as spawnSync3 } from "node:child_process";
16924
- import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync21, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
17767
+ import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync22, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
16925
17768
  import { homedir as homedir6 } from "node:os";
16926
17769
  import { join as join24, resolve as resolve7 } from "node:path";
16927
17770
  var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
@@ -16998,9 +17841,9 @@ function printable(text) {
16998
17841
  }
16999
17842
  function restoreOne(root, folder, refs, rawId) {
17000
17843
  const valid = savedIdSchema.safeParse(rawId);
17001
- const named = valid.success ? `"${rawId}"` : "with that name";
17844
+ const named2 = valid.success ? `"${rawId}"` : "with that name";
17002
17845
  if (!valid.success || !refs.has(rawId)) {
17003
- return refused2(`No saved copy ${named} is kept in this project. Saved copies are kept for 7 days. To see the ones there are, run: ${LIST_COMMAND}`);
17846
+ return refused2(`No saved copy ${named2} is kept in this project. Saved copies are kept for 7 days. To see the ones there are, run: ${LIST_COMMAND}`);
17004
17847
  }
17005
17848
  const id = valid.data;
17006
17849
  const saved = readSaved(folder, id);
@@ -17107,7 +17950,7 @@ function sameAs(abs, st, content) {
17107
17950
  try {
17108
17951
  if ("link" in content)
17109
17952
  return st.isSymbolicLink() && readlinkSync(abs) === content.link;
17110
- return st.isFile() && readFileSync21(abs).equals(content.bytes);
17953
+ return st.isFile() && readFileSync22(abs).equals(content.bytes);
17111
17954
  } catch {
17112
17955
  return false;
17113
17956
  }
@@ -17130,7 +17973,7 @@ function restoreCopy(root, saveDir, rel, out) {
17130
17973
  if (st !== null && st.isSymbolicLink())
17131
17974
  return place(root, rel, { link: readlinkSync(src) }, out);
17132
17975
  if (st !== null && st.isFile())
17133
- return place(root, rel, { bytes: readFileSync21(src), mode: st.mode & 511 }, out);
17976
+ return place(root, rel, { bytes: readFileSync22(src), mode: st.mode & 511 }, out);
17134
17977
  } catch {
17135
17978
  }
17136
17979
  out.left.push(`${rel}: the saved copy does not hold it`);
@@ -17228,7 +18071,7 @@ function restoreLines(root, ref, saved, out) {
17228
18071
  }
17229
18072
  function pinnedEngagement(root) {
17230
18073
  try {
17231
- const pin = JSON.parse(readFileSync21(join24(root, BUNDLE_PIN_REL), "utf-8"));
18074
+ const pin = JSON.parse(readFileSync22(join24(root, BUNDLE_PIN_REL), "utf-8"));
17232
18075
  return isEngagementId(pin.engagementId) ? pin.engagementId : null;
17233
18076
  } catch {
17234
18077
  return null;
@@ -17259,7 +18102,7 @@ function readSaved(folder, id) {
17259
18102
  if (fst === null || !fst.isFile())
17260
18103
  return null;
17261
18104
  try {
17262
- const parsed = savedSchema.safeParse(JSON.parse(readFileSync21(file, "utf-8")));
18105
+ const parsed = savedSchema.safeParse(JSON.parse(readFileSync22(file, "utf-8")));
17263
18106
  return parsed.success && parsed.data.id === id ? parsed.data : null;
17264
18107
  } catch {
17265
18108
  return null;
@@ -17308,7 +18151,7 @@ function pruneAtRestore(root, folder, refs, now) {
17308
18151
  }
17309
18152
  function markedRepository(dir) {
17310
18153
  try {
17311
- const text = readFileSync21(join24(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
18154
+ const text = readFileSync22(join24(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
17312
18155
  return text === "" ? null : text;
17313
18156
  } catch {
17314
18157
  return null;
@@ -17412,6 +18255,10 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
17412
18255
  run a task's test command on this machine, once before the change and once after, and record what it saw
17413
18256
  ${CHECK_TASKS_USAGE}
17414
18257
  check a phase's plan in docs/build-records/ for work that would collide if it ran at once
18258
+ ${CHECK_REPORT_USAGE}
18259
+ check a worker's report, saved from its pull request, against the commit it must be about
18260
+ ${READY_USAGE}
18261
+ list the tasks of a phase's plan that can start now, longest chain first, up to the number given
17415
18262
  ${CHANGE_USAGE}
17416
18263
  sort a change you are asked for into a small change, a mini phase or a full phase; move it up;
17417
18264
  end a small change and write its record into docs/build-records/changes/
@@ -18082,6 +18929,45 @@ halfcycle build-record: until ${obtained.stampFailure.path} can be written, the
18082
18929
  process.exitCode = run.exitCode;
18083
18930
  return;
18084
18931
  }
18932
+ if (cmd === "check-report") {
18933
+ const reportShortfalls = missingRequirements("check-report", rest);
18934
+ if (reportShortfalls.length > 0) {
18935
+ process.stderr.write(`${reportShortfalls[0].message}
18936
+ `);
18937
+ process.exitCode = 2;
18938
+ return;
18939
+ }
18940
+ const parsed = parseCheckReportArgs(rest);
18941
+ if (!parsed.ok) {
18942
+ process.stderr.write(`halfcycle check-report: usage: ${CHECK_REPORT_USAGE}
18943
+ `);
18944
+ process.exitCode = 2;
18945
+ return;
18946
+ }
18947
+ const run = runCheckReport(parsed.source, parsed.head);
18948
+ if (run.stdout !== "")
18949
+ process.stdout.write(run.stdout);
18950
+ if (run.stderr !== "")
18951
+ process.stderr.write(run.stderr);
18952
+ process.exitCode = run.exitCode;
18953
+ return;
18954
+ }
18955
+ if (cmd === "ready") {
18956
+ const parsed = parseReadyArgs(rest);
18957
+ if (!parsed.ok) {
18958
+ process.stderr.write(`halfcycle ready: usage: ${READY_USAGE}
18959
+ `);
18960
+ process.exitCode = 2;
18961
+ return;
18962
+ }
18963
+ const run = runReady(parsed.repoRoot ?? process.cwd(), parsed.phase, parsed.cap);
18964
+ if (run.stdout !== "")
18965
+ process.stdout.write(run.stdout);
18966
+ if (run.stderr !== "")
18967
+ process.stderr.write(run.stderr);
18968
+ process.exitCode = run.exitCode;
18969
+ return;
18970
+ }
18085
18971
  if (cmd === "pace") {
18086
18972
  const parsed = parsePaceArgs(rest);
18087
18973
  if (!parsed.ok) {