halfcycle 0.5.2 → 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
@@ -414,15 +414,15 @@ function readReviewPart(raw) {
414
414
  const name = raw["name"];
415
415
  const built = raw["built"];
416
416
  const decidable = raw["decidable"];
417
- const awaitingAnswer2 = raw["awaitingAnswer"];
417
+ const awaitingAnswer = raw["awaitingAnswer"];
418
418
  const sentBack = raw["sentBack"];
419
419
  if (!isNonEmptyString(part) || !isNonEmptyString(name))
420
420
  return null;
421
421
  if (typeof built !== "boolean" || typeof decidable !== "boolean")
422
422
  return null;
423
- if (typeof awaitingAnswer2 !== "boolean" || typeof sentBack !== "boolean")
423
+ if (typeof awaitingAnswer !== "boolean" || typeof sentBack !== "boolean")
424
424
  return null;
425
- return { part, name, built, decidable, awaitingAnswer: awaitingAnswer2, sentBack };
425
+ return { part, name, built, decidable, awaitingAnswer, sentBack };
426
426
  }
427
427
  function isPlainObject(value) {
428
428
  if (typeof value !== "object" || value === null || Array.isArray(value))
@@ -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)}`
@@ -4503,6 +5107,30 @@ var phaseReviewViewSchema = z20.discriminatedUnion("state", [
4503
5107
  phaseReviewOpenSchema,
4504
5108
  phaseReviewClosedSchema
4505
5109
  ]);
5110
+ function reviewPartAwaitingAnswer(part) {
5111
+ return part.questions.some((q) => q.agentAnswer === void 0);
5112
+ }
5113
+ function reviewPartSentBackIn(part, round) {
5114
+ return part.sentBack.some((s) => s.round === round);
5115
+ }
5116
+ function shownReviewEntryOf(view, shownAt) {
5117
+ const parts = view.parts.map((p) => ({
5118
+ part: p.part,
5119
+ name: p.name,
5120
+ built: p.built,
5121
+ decidable: p.decidable,
5122
+ awaitingAnswer: reviewPartAwaitingAnswer(p),
5123
+ sentBack: reviewPartSentBackIn(p, view.round)
5124
+ }));
5125
+ return {
5126
+ phase: view.phase,
5127
+ number: view.number,
5128
+ round: view.round,
5129
+ reviewVersion: view.reviewVersion,
5130
+ shownAt,
5131
+ parts
5132
+ };
5133
+ }
4506
5134
 
4507
5135
  // ../events/dist/build-record-v3.js
4508
5136
  var BUILD_RECORD_V3_FORMAT = "halfcycle-build-record/v3";
@@ -5146,8 +5774,35 @@ function ownerWaitClosedLineOf(item, question2) {
5146
5774
  }
5147
5775
  var OWNER_WAIT_DECLARATIONS_READ_FAILED = "Halfcycle could not read the decisions that waited for you.";
5148
5776
 
5149
- // ../events/dist/pacing.js
5777
+ // ../events/dist/owner-wait-answer.js
5150
5778
  import { z as z32 } from "zod";
5779
+ var ownerWaitShortIdSchema = z32.string().regex(/^[0-9a-f]{8}$/);
5780
+ var OWNER_WAIT_ANSWER_ACTS = ["approve", "decline"];
5781
+ var ownerWaitAnswerActSchema = z32.enum(OWNER_WAIT_ANSWER_ACTS);
5782
+ var ownerWaitChatRequestSchema = z32.discriminatedUnion("act", [
5783
+ z32.object({ act: z32.literal("approve"), wait: ownerWaitShortIdSchema }).strict(),
5784
+ z32.object({ act: z32.literal("decline"), wait: ownerWaitShortIdSchema, note: scopeNoteWireSchema }).strict()
5785
+ ]);
5786
+ function bareOutcome3(outcome) {
5787
+ return z32.object({ outcome: z32.literal(outcome) }).strict();
5788
+ }
5789
+ var ownerWaitChatResultSchema = z32.discriminatedUnion("outcome", [
5790
+ z32.object({
5791
+ outcome: z32.literal("recorded"),
5792
+ act: ownerWaitAnswerActSchema,
5793
+ question: z32.string().min(1)
5794
+ }).strict(),
5795
+ z32.object({ outcome: z32.literal("refused"), problems: z32.array(systemDescriptionProblemSchema).min(1) }).strict(),
5796
+ bareOutcome3("not-owner"),
5797
+ bareOutcome3("no-owner"),
5798
+ bareOutcome3("not-found"),
5799
+ bareOutcome3("ambiguous"),
5800
+ bareOutcome3("not-offered"),
5801
+ bareOutcome3("unavailable")
5802
+ ]);
5803
+
5804
+ // ../events/dist/pacing.js
5805
+ import { z as z33 } from "zod";
5151
5806
  var CLAUDE_PLANS = Object.freeze([
5152
5807
  Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
5153
5808
  Object.freeze({ id: "max-5x", words: "Max (5\xD7)", startingAgents: 4 }),
@@ -5158,7 +5813,7 @@ var CLAUDE_PLANS = Object.freeze([
5158
5813
  Object.freeze({ id: "not-sure", words: "Not sure", startingAgents: 2 })
5159
5814
  ]);
5160
5815
  var CLAUDE_PLAN_IDS = Object.freeze(CLAUDE_PLANS.map((plan) => plan.id));
5161
- var claudePlanSchema = z32.enum(CLAUDE_PLAN_IDS);
5816
+ var claudePlanSchema = z33.enum(CLAUDE_PLAN_IDS);
5162
5817
  var CLAUDE_PLAN_UNKNOWN = "not-sure";
5163
5818
  function claudePlanOf(id) {
5164
5819
  return CLAUDE_PLANS.find((plan) => plan.id === id) ?? CLAUDE_PLANS.find((plan) => plan.id === CLAUDE_PLAN_UNKNOWN) ?? CLAUDE_PLANS[0];
@@ -5167,21 +5822,21 @@ function claudePlanKnown(id) {
5167
5822
  return id !== null && id !== CLAUDE_PLAN_UNKNOWN;
5168
5823
  }
5169
5824
  var CLAUDE_PLAN_SET_IN = ["setup", "console"];
5170
- var claudePlanSetInSchema = z32.enum(CLAUDE_PLAN_SET_IN);
5825
+ var claudePlanSetInSchema = z33.enum(CLAUDE_PLAN_SET_IN);
5171
5826
  var KEEP_FREE_PERCENTS = [0, 20];
5172
- var keepFreePercentSchema = z32.union([z32.literal(KEEP_FREE_PERCENTS[0]), z32.literal(KEEP_FREE_PERCENTS[1])]);
5827
+ var keepFreePercentSchema = z33.union([z33.literal(KEEP_FREE_PERCENTS[0]), z33.literal(KEEP_FREE_PERCENTS[1])]);
5173
5828
  var PACE_AGENTS_MAX = 20;
5174
5829
  var PACE_WINDOW_KINDS = ["five-hour", "seven-day"];
5175
- var paceWindowKindSchema = z32.enum(PACE_WINDOW_KINDS);
5830
+ var paceWindowKindSchema = z33.enum(PACE_WINDOW_KINDS);
5176
5831
  var PACE_WINDOW_WORDS = Object.freeze({
5177
5832
  "five-hour": "5-hour window",
5178
5833
  "seven-day": "weekly window"
5179
5834
  });
5180
5835
  var PACE_STATES = ["full", "slowing", "holding"];
5181
- var paceStateSchema = z32.enum(PACE_STATES);
5182
- var claudePlanRequestSchema = z32.object({ claudePlan: claudePlanSchema }).strict();
5183
- var keepFreeRequestSchema = z32.object({ keepFreePercent: keepFreePercentSchema }).strict();
5184
- var pacingSettingsSchema = z32.object({
5836
+ var paceStateSchema = z33.enum(PACE_STATES);
5837
+ var claudePlanRequestSchema = z33.object({ claudePlan: claudePlanSchema }).strict();
5838
+ var keepFreeRequestSchema = z33.object({ keepFreePercent: keepFreePercentSchema }).strict();
5839
+ var pacingSettingsSchema = z33.object({
5185
5840
  claudePlan: claudePlanSchema.nullable(),
5186
5841
  claudePlanSetIn: claudePlanSetInSchema.nullable(),
5187
5842
  keepFreePercent: keepFreePercentSchema
@@ -5189,43 +5844,43 @@ var pacingSettingsSchema = z32.object({
5189
5844
  message: "a plan says where it was given, and only a plan does",
5190
5845
  path: ["claudePlanSetIn"]
5191
5846
  });
5192
- var pacingBasisTaskSchema = z32.object({
5847
+ var pacingBasisTaskSchema = z33.object({
5193
5848
  id: buildTaskIdSchema,
5194
- dependsOn: z32.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5195
- budgetMinutes: z32.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
5196
- human: z32.boolean()
5849
+ dependsOn: z33.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5850
+ budgetMinutes: z33.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
5851
+ human: z33.boolean()
5197
5852
  }).strict();
5198
- var pacingBasisSchema = z32.object({
5199
- outcome: z32.literal("basis"),
5853
+ var pacingBasisSchema = z33.object({
5854
+ outcome: z33.literal("basis"),
5200
5855
  claudePlan: claudePlanSchema.nullable(),
5201
5856
  keepFreePercent: keepFreePercentSchema,
5202
- settingsUrl: z32.string().min(1),
5857
+ settingsUrl: z33.string().min(1),
5203
5858
  phase: phaseNameSchema.nullable(),
5204
- tasks: z32.array(pacingBasisTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5205
- medianTaskMinutes: z32.number().nonnegative().nullable(),
5206
- medianTaskTokens: z32.number().nonnegative().nullable()
5859
+ tasks: z33.array(pacingBasisTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5860
+ medianTaskMinutes: z33.number().nonnegative().nullable(),
5861
+ medianTaskTokens: z33.number().nonnegative().nullable()
5207
5862
  }).strict();
5208
- var paceSuggestionSchema = z32.object({
5209
- number: z32.number().int().min(1).max(PACE_AGENTS_MAX),
5210
- minutes: z32.number().int().nonnegative().nullable(),
5211
- pauses: z32.number().int().nonnegative(),
5863
+ var paceSuggestionSchema = z33.object({
5864
+ number: z33.number().int().min(1).max(PACE_AGENTS_MAX),
5865
+ minutes: z33.number().int().nonnegative().nullable(),
5866
+ pauses: z33.number().int().nonnegative(),
5212
5867
  claudePlan: claudePlanSchema.nullable(),
5213
5868
  keepFreePercent: keepFreePercentSchema,
5214
- settingsUrl: z32.string().min(1).nullable(),
5869
+ settingsUrl: z33.string().min(1).nullable(),
5215
5870
  knownWindow: paceWindowKindSchema.nullable()
5216
5871
  }).strict();
5217
5872
  var PACE_NOT_PACED_REASONS = ["not-configured", "unreadable"];
5218
- var paceRunResultSchema = z32.discriminatedUnion("outcome", [
5219
- z32.object({
5220
- outcome: z32.literal("paced"),
5873
+ var paceRunResultSchema = z33.discriminatedUnion("outcome", [
5874
+ z33.object({
5875
+ outcome: z33.literal("paced"),
5221
5876
  pace: paceStateSchema,
5222
- number: z32.number().int().min(1).max(PACE_AGENTS_MAX),
5223
- allowedNow: z32.number().int().nonnegative(),
5877
+ number: z33.number().int().min(1).max(PACE_AGENTS_MAX),
5878
+ allowedNow: z33.number().int().nonnegative(),
5224
5879
  windowKind: paceWindowKindSchema.optional(),
5225
5880
  until: utcIso8601.optional(),
5226
5881
  suggestion: paceSuggestionSchema.optional()
5227
5882
  }).strict(),
5228
- z32.object({ outcome: z32.literal("not-paced"), reason: z32.enum(PACE_NOT_PACED_REASONS) }).strict()
5883
+ z33.object({ outcome: z33.literal("not-paced"), reason: z33.enum(PACE_NOT_PACED_REASONS) }).strict()
5229
5884
  ]);
5230
5885
  function paceAgentsOf(n) {
5231
5886
  return n === 1 ? "1 agent" : `${String(n)} agents`;
@@ -5310,15 +5965,15 @@ function paceAllowedNowOf(k) {
5310
5965
  var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
5311
5966
 
5312
5967
  // ../events/dist/autonomy.js
5313
- import { z as z33 } from "zod";
5968
+ import { z as z34 } from "zod";
5314
5969
  var AUTONOMY_CHANGED_IN = ["console", "chat"];
5315
- var autonomyChangedInSchema = z33.enum(AUTONOMY_CHANGED_IN);
5316
- var autonomySettingsSchema = z33.object({
5317
- on: z33.boolean(),
5970
+ var autonomyChangedInSchema = z34.enum(AUTONOMY_CHANGED_IN);
5971
+ var autonomySettingsSchema = z34.object({
5972
+ on: z34.boolean(),
5318
5973
  // whether builds run on their own
5319
- mayPublish: z33.boolean(),
5974
+ mayPublish: z34.boolean(),
5320
5975
  // whether builds may publish or release without waiting
5321
- maySpend: z33.boolean(),
5976
+ maySpend: z34.boolean(),
5322
5977
  // whether builds may spend money or sign up without waiting
5323
5978
  changedAt: utcIso8601.nullable(),
5324
5979
  // when the setting last changed; null if never
@@ -5328,37 +5983,37 @@ var autonomySettingsSchema = z33.object({
5328
5983
  message: "says when and where it changed, or neither",
5329
5984
  path: ["changedIn"]
5330
5985
  });
5331
- var autonomyChangeRequestSchema = z33.object({
5332
- on: z33.boolean(),
5986
+ var autonomyChangeRequestSchema = z34.object({
5987
+ on: z34.boolean(),
5333
5988
  // the switch
5334
- mayPublish: z33.boolean(),
5989
+ mayPublish: z34.boolean(),
5335
5990
  // the publish box
5336
- maySpend: z33.boolean()
5991
+ maySpend: z34.boolean()
5337
5992
  // the spend box
5338
5993
  }).strict();
5339
5994
  var AUTONOMY_CHAT_ACTS = ["on", "off"];
5340
- var autonomyChatActSchema = z33.enum(AUTONOMY_CHAT_ACTS);
5341
- var autonomyChatRequestSchema = z33.object({
5995
+ var autonomyChatActSchema = z34.enum(AUTONOMY_CHAT_ACTS);
5996
+ var autonomyChatRequestSchema = z34.object({
5342
5997
  act: autonomyChatActSchema
5343
5998
  // what the owner typed
5344
5999
  }).strict();
5345
- var autonomyChatResultSchema = z33.discriminatedUnion("outcome", [
5346
- z33.object({
5347
- outcome: z33.literal("recorded"),
5348
- on: z33.boolean()
6000
+ var autonomyChatResultSchema = z34.discriminatedUnion("outcome", [
6001
+ z34.object({
6002
+ outcome: z34.literal("recorded"),
6003
+ on: z34.boolean()
5349
6004
  // whether builds now run on their own
5350
6005
  }).strict(),
5351
- z33.object({ outcome: z33.literal("not-owner") }).strict(),
5352
- z33.object({ outcome: z33.literal("no-owner") }).strict(),
5353
- z33.object({ outcome: z33.literal("not-offered") }).strict(),
5354
- z33.object({ outcome: z33.literal("unavailable") }).strict()
6006
+ z34.object({ outcome: z34.literal("not-owner") }).strict(),
6007
+ z34.object({ outcome: z34.literal("no-owner") }).strict(),
6008
+ z34.object({ outcome: z34.literal("not-offered") }).strict(),
6009
+ z34.object({ outcome: z34.literal("unavailable") }).strict()
5355
6010
  ]);
5356
- var autonomyStateSchema = z33.object({
5357
- on: z33.boolean(),
6011
+ var autonomyStateSchema = z34.object({
6012
+ on: z34.boolean(),
5358
6013
  // whether builds run on their own
5359
- mayPublish: z33.boolean(),
6014
+ mayPublish: z34.boolean(),
5360
6015
  // whether builds may publish or release without waiting
5361
- maySpend: z33.boolean()
6016
+ maySpend: z34.boolean()
5362
6017
  // whether builds may spend money or sign up without waiting
5363
6018
  }).strict();
5364
6019
  var autonomyStateFileSchema = autonomyStateSchema.extend({
@@ -5367,8 +6022,8 @@ var autonomyStateFileSchema = autonomyStateSchema.extend({
5367
6022
  }).strict();
5368
6023
  var AUTONOMY_STATE_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5369
6024
  var AUTONOMY_ACTIVITY_KINDS = ["went-ahead", "waiting", "approved", "closed", "waiting-on-owner"];
5370
- var autonomyActivityKindSchema = z33.enum(AUTONOMY_ACTIVITY_KINDS);
5371
- var autonomyActivityRowSchema = z33.object({
6025
+ var autonomyActivityKindSchema = z34.enum(AUTONOMY_ACTIVITY_KINDS);
6026
+ var autonomyActivityRowSchema = z34.object({
5372
6027
  kind: autonomyActivityKindSchema,
5373
6028
  // what happened
5374
6029
  phase: scopePhaseSchema,
@@ -5381,6 +6036,8 @@ var autonomyActivityRowSchema = z33.object({
5381
6036
  // whether the agent says it can be undone
5382
6037
  how: statementText(STATEMENT_MAX).optional(),
5383
6038
  // how to undo it, when it can be
6039
+ wait: ownerWaitShortIdSchema.optional(),
6040
+ // the short id the owner types to answer a waiting decision
5384
6041
  at: utcIso8601
5385
6042
  // when it happened
5386
6043
  }).strict().superRefine((row, ctx) => {
@@ -5407,6 +6064,9 @@ var autonomyActivityRowSchema = z33.object({
5407
6064
  });
5408
6065
  }
5409
6066
  }
6067
+ if (row.kind !== "waiting" && row.wait !== void 0) {
6068
+ ctx.addIssue({ code: "custom", message: "only a waiting decision is answered by typing", path: ["wait"] });
6069
+ }
5410
6070
  if (row.kind === "waiting" && (row.undoable !== void 0 || row.how !== void 0)) {
5411
6071
  ctx.addIssue({ code: "custom", message: "a waiting decision has not been made yet", path: ["undoable"] });
5412
6072
  }
@@ -5414,11 +6074,11 @@ var autonomyActivityRowSchema = z33.object({
5414
6074
  ctx.addIssue({ code: "custom", message: "says how to undo it exactly when it can be undone", path: ["how"] });
5415
6075
  }
5416
6076
  });
5417
- var autonomyActivitySchema = z33.object({
5418
- rows: z33.array(autonomyActivityRowSchema)
6077
+ var autonomyActivitySchema = z34.object({
6078
+ rows: z34.array(autonomyActivityRowSchema)
5419
6079
  // newest first
5420
6080
  }).strict();
5421
- var savedIdSchema = z33.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
6081
+ var savedIdSchema = z34.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lower-case letters, digits and single hyphens");
5422
6082
  var SAVED_REF_PREFIX = "refs/halfcycle/saved/";
5423
6083
  function savedRefOf(id) {
5424
6084
  return `${SAVED_REF_PREFIX}${id}`;
@@ -5457,33 +6117,33 @@ function isGitFolderSegment(segment) {
5457
6117
  return t === ".git" || /^git~\d+$/.test(t);
5458
6118
  }
5459
6119
  var SAVED_PATH_KEPT_IN = ["commit", "copy"];
5460
- var savedPathSchema = z33.object({
5461
- path: z33.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
6120
+ var savedPathSchema = z34.object({
6121
+ path: z34.string().max(4096).refine(isProjectRelativePath, { message: "must be a path inside the project" }),
5462
6122
  // the file, relative to the project
5463
- keptIn: z33.enum(SAVED_PATH_KEPT_IN)
6123
+ keptIn: z34.enum(SAVED_PATH_KEPT_IN)
5464
6124
  // in the save's commit, or copied beside this file
5465
6125
  }).strict();
5466
- var savedCommitSchema = z33.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
5467
- var savedBranchSchema = z33.object({
5468
- name: z33.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
6126
+ var savedCommitSchema = z34.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, "must be a full commit hash");
6127
+ var savedBranchSchema = z34.object({
6128
+ name: z34.string().min(1).max(255).refine((n) => !hasControlCharacter(n) && !n.startsWith("-") && !n.includes("..") && n !== "HEAD", {
5469
6129
  message: "must be a branch name"
5470
6130
  }),
5471
6131
  // the branch's name
5472
6132
  commit: savedCommitSchema
5473
6133
  // the commit it pointed at
5474
6134
  }).strict();
5475
- var savedSchema = z33.object({
6135
+ var savedSchema = z34.object({
5476
6136
  id: savedIdSchema,
5477
6137
  // what restore is typed with
5478
6138
  savedAt: utcIso8601,
5479
6139
  // when the copy was made
5480
- ref: z33.string().min(1),
6140
+ ref: z34.string().min(1),
5481
6141
  // where the saved commit is kept
5482
- what: z33.string().min(1).max(2e3),
6142
+ what: z34.string().min(1).max(2e3),
5483
6143
  // what the action would have changed, in words
5484
- paths: z33.array(savedPathSchema),
6144
+ paths: z34.array(savedPathSchema),
5485
6145
  // every saved file
5486
- branches: z33.array(savedBranchSchema)
6146
+ branches: z34.array(savedBranchSchema)
5487
6147
  // every saved line of work
5488
6148
  }).strict().refine((s) => s.ref === savedRefOf(s.id), { message: "must be the ref of this save", path: ["ref"] });
5489
6149
  function autonomyA13(accepted, carried) {
@@ -5493,8 +6153,9 @@ function autonomyA14(count, parts, acceptLine) {
5493
6153
  const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `say "review what was built", then type ${acceptLine} to accept them with those parts, or type the reject line it gives for a task's tests.`;
5494
6154
  return `The phase waits for you: ${String(count)} tests the agents said pass were not run by Halfcycle, so ${namesOf(parts)} wait for you too. Only you can accept or reject those tests. In your own coding agent's session, ` + typed;
5495
6155
  }
5496
- function autonomyA15(count) {
5497
- return `The phase waits for you: ${String(count)} parts were built without an approved scope, and only you can decide them.`;
6156
+ function autonomyA15(count, acceptLine) {
6157
+ const typed = acceptLine === null ? 'say "review what was built" and type the lines it gives.' : `type ${acceptLine} to accept them and close the phase, or say "review what was built" to decide one differently.`;
6158
+ return `The phase waits for you: ${String(count)} parts were built without an approved scope or outside the plan, and only you can decide them. In your own coding agent's session, ${typed}`;
5498
6159
  }
5499
6160
  function namesOf(names) {
5500
6161
  if (names.length <= 1)
@@ -7545,12 +8206,12 @@ function findNotShared(targetRepo, manifest, collidedPaths) {
7545
8206
  if (ignored === null)
7546
8207
  return null;
7547
8208
  const sorted = [...ignored].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
7548
- const named = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
8209
+ const named2 = sorted.find((entry) => entry.path === SETTINGS_REL) ?? sorted[0];
7549
8210
  return {
7550
- path: named.path,
7551
- pattern: named.pattern,
7552
- source: named.source,
7553
- line: named.line,
8211
+ path: named2.path,
8212
+ pattern: named2.pattern,
8213
+ source: named2.source,
8214
+ line: named2.line,
7554
8215
  others: sorted.length - 1
7555
8216
  };
7556
8217
  }
@@ -10403,8 +11064,8 @@ function renderBlock(content, layout, environment) {
10403
11064
  function renderLine(content, environment) {
10404
11065
  const unicode = decideUnicode(environment);
10405
11066
  const dot = unicode ? UNICODE_GLYPHS.dot : ASCII_GLYPHS.dot;
10406
- const named = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
10407
- const raw = `${named} ${dot} ${content.url}`;
11067
+ const named2 = content.version === "" ? "Halfcycle" : `Halfcycle v${content.version}`;
11068
+ const raw = `${named2} ${dot} ${content.url}`;
10408
11069
  return unicode ? raw : toAscii(raw);
10409
11070
  }
10410
11071
  function renderBanner(content, environment = {}) {
@@ -11930,10 +12591,10 @@ function leftForYouLines(left, p) {
11930
12591
  return lines;
11931
12592
  }
11932
12593
  function shownBy(steps) {
11933
- const named = steps.filter((s) => s.name !== void 0).length;
11934
- if (named === 0)
12594
+ const named2 = steps.filter((s) => s.name !== void 0).length;
12595
+ if (named2 === 0)
11935
12596
  return "Each is shown by the method's own label for it:";
11936
- if (named === steps.length)
12597
+ if (named2 === steps.length)
11937
12598
  return "Each is shown by the method's own name for it:";
11938
12599
  return "Each is shown by the method's own name for it, or by its label where the record holds no name:";
11939
12600
  }
@@ -12658,7 +13319,7 @@ function isMissingRoute(method, status, body) {
12658
13319
  import { join as join17 } from "node:path";
12659
13320
 
12660
13321
  // dist/cli-contract.js
12661
- 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"];
12662
13323
  var CONTRACTS = [
12663
13324
  { verb: "install", required: [], conditional: [] },
12664
13325
  { verb: "check-drift", required: [], conditional: [] },
@@ -12668,12 +13329,24 @@ var CONTRACTS = [
12668
13329
  { verb: "plan", required: [], conditional: [] },
12669
13330
  { verb: "review", required: [], conditional: [] },
12670
13331
  { verb: "check-tasks", required: [], conditional: [] },
13332
+ { verb: "ready", required: [], conditional: [] },
12671
13333
  { verb: "uninstall", required: [], conditional: [] },
12672
13334
  { verb: "pace", required: [], conditional: [] },
12673
13335
  { verb: "prove", required: [], conditional: [] },
12674
13336
  { verb: "preview", required: [], conditional: [] },
12675
13337
  { verb: "doctor", required: [], conditional: [] },
12676
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
+ },
12677
13350
  {
12678
13351
  verb: "open-phase",
12679
13352
  required: [
@@ -13400,16 +14073,16 @@ function parseChangeArgs(rest) {
13400
14073
  return notAChangeId("up", change);
13401
14074
  return { ok: true, args: { form: "up", to, target: { kind: "change", sortingId: change }, ...where } };
13402
14075
  }
13403
- const named = phase ?? "";
13404
- if (!isValidPhaseIdentity(named)) {
13405
- 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.
13406
14079
  `, exitCode: 2 };
13407
14080
  }
13408
14081
  if (to === "mini") {
13409
14082
  return { ok: false, line: `halfcycle change up: ${CHANGE_PATH_WORDS["only-up"]} Nothing was sent.
13410
14083
  `, exitCode: 1 };
13411
14084
  }
13412
- 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 } };
13413
14086
  }
13414
14087
  var OBSERVE_CHANGE_UNREACHABLE = { outcome: "not-sent", reason: "unreachable" };
13415
14088
  function readObserveChangeLine(stdout) {
@@ -13882,11 +14555,11 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
13882
14555
  return { ok: false, line: `${p} ${fileRel} is not valid JSON (${why}). Fix it, then run this again. Nothing was sent.
13883
14556
  ` };
13884
14557
  }
13885
- const named = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
13886
- 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)) {
13887
14560
  return {
13888
14561
  ok: false,
13889
- 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.
13890
14563
  `
13891
14564
  };
13892
14565
  }
@@ -14189,20 +14862,14 @@ var CHANGE_WORDS = {
14189
14862
  remove: "planned to be removed",
14190
14863
  "not-in-plan": "not in the plan"
14191
14864
  };
14192
- function awaitingAnswer(part) {
14193
- return part.questions.some((q) => q.agentAnswer === void 0);
14194
- }
14195
- function sentBackInRound(part, round) {
14196
- return part.sentBack.some((s) => s.round === round);
14197
- }
14198
14865
  function partStatus(part, round) {
14199
14866
  const settled = settledStatus(part);
14200
14867
  if (settled !== void 0) {
14201
14868
  return part.givenIn === "autonomy" ? `${settled}, ${DECIDED_BY_AUTONOMY_SENTENCE}` : settled;
14202
14869
  }
14203
- if (awaitingAnswer(part))
14870
+ if (reviewPartAwaitingAnswer(part))
14204
14871
  return "Waiting for the agent's answer";
14205
- if (sentBackInRound(part, round))
14872
+ if (reviewPartSentBackIn(part, round))
14206
14873
  return "Sent back, waiting for the next round";
14207
14874
  return "Waits for your decision";
14208
14875
  }
@@ -14377,22 +15044,7 @@ var AN3_LINE = "halfcycle review: the answer must be one paragraph of at most 1,
14377
15044
  var AN4_LINE = "halfcycle review: the answer looks like it holds a password or key, so it was not sent. Write it again without it.";
14378
15045
  var AN5_LINE = "halfcycle review: the answer was not accepted. Reword it in plain words and run this again.";
14379
15046
  function shownReviewEntry(view, shownAt) {
14380
- const parts = view.parts.map((p) => ({
14381
- part: p.part,
14382
- name: p.name,
14383
- built: p.built,
14384
- decidable: p.decidable,
14385
- awaitingAnswer: awaitingAnswer(p),
14386
- sentBack: sentBackInRound(p, view.round)
14387
- }));
14388
- return {
14389
- phase: view.phase,
14390
- number: view.number,
14391
- round: view.round,
14392
- reviewVersion: view.reviewVersion,
14393
- shownAt,
14394
- parts
14395
- };
15047
+ return shownReviewEntryOf(view, shownAt);
14396
15048
  }
14397
15049
  function writeShownReview(engagementId, entry, home) {
14398
15050
  const written = updateShownRecord(engagementId, (current) => ({
@@ -14683,10 +15335,10 @@ async function openPhaseReview(credential, phase, request, observe, repoRoot, se
14683
15335
  }
14684
15336
  };
14685
15337
  const parsed = openReviewRequestSchema.safeParse(body);
14686
- const named = phaseNameSchema.safeParse(phase);
14687
- if (!parsed.success || !named.success) {
15338
+ const named2 = phaseNameSchema.safeParse(phase);
15339
+ if (!parsed.success || !named2.success) {
14688
15340
  const issues = [
14689
- ...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}`),
14690
15342
  ...parsed.success ? [] : parsed.error.issues.map((i) => `${describeIssuePath(i.path)}: ${i.message}`)
14691
15343
  ];
14692
15344
  throw new CloseRequestInvalid(`that close is not one this platform accepts, so nothing was sent: ${issues.join("; ")}.`);
@@ -14864,7 +15516,7 @@ function closeStepReport(phase, attempt, engagementId) {
14864
15516
  exitCode = 0;
14865
15517
  break;
14866
15518
  case "autonomy-waits":
14867
- stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name), typedAcceptLine(phase)) : autonomyA15(answer.count));
15519
+ stdout.push(answer.reason === "unchecked-claims" ? autonomyA14(answer.count, answer.parts.map((p) => p.name), typedAcceptLine(phase)) : autonomyA15(answer.count, typedAcceptLine(phase)));
14868
15520
  exitCode = 0;
14869
15521
  break;
14870
15522
  default:
@@ -15569,6 +16221,7 @@ function parseCheckTasksArgs(argv) {
15569
16221
  return { ok: false };
15570
16222
  return { ok: true, phase: positionals[0], withTaskList, repoRoot };
15571
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.`;
15572
16225
  function issuePlace(rel, path) {
15573
16226
  return path.length === 0 ? rel : `${rel}.${path.map(String).join(".")}`;
15574
16227
  }
@@ -15625,7 +16278,42 @@ function readSystemParts(repoRoot) {
15625
16278
  ` + problemLines(file.value, checked.problems)
15626
16279
  };
15627
16280
  }
15628
- 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 } };
15629
16317
  }
15630
16318
  function runCheckTasks(repoRoot, phase, withTaskList) {
15631
16319
  const p = "halfcycle check-tasks:";
@@ -15633,7 +16321,7 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
15633
16321
  if (planRead.kind === "missing") {
15634
16322
  return {
15635
16323
  stdout: "",
15636
- 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}
15637
16325
  `,
15638
16326
  exitCode: 2
15639
16327
  };
@@ -15656,16 +16344,207 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
15656
16344
  if (system.kind === "bad") {
15657
16345
  return { stdout: "", stderr: `${p} ${system.line}`, exitCode: 2 };
15658
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;
15659
16353
  const result = checkTaskPlan(plan, {
15660
16354
  phase,
15661
16355
  taskList,
15662
- ...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 } : {}
15663
16359
  });
15664
16360
  const stdout = result.lines.map((line) => `${line.text}
15665
16361
  `).join("");
15666
16362
  return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
15667
16363
  }
15668
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
+
15669
16548
  // dist/prove.js
15670
16549
  import { spawn as spawn2 } from "node:child_process";
15671
16550
  var PROVE_USAGE = `${HALFCYCLE_COMMAND} prove <phase> <task> --label "<label>" --before -- <test command>
@@ -15718,7 +16597,7 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
15718
16597
  }
15719
16598
 
15720
16599
  // dist/uninstall.js
15721
- 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";
15722
16601
  import { homedir as homedir5 } from "node:os";
15723
16602
  import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
15724
16603
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
@@ -15761,7 +16640,7 @@ function readMergedJson(targetRepo, rule) {
15761
16640
  let text;
15762
16641
  let value;
15763
16642
  try {
15764
- text = readFileSync19(path, "utf-8");
16643
+ text = readFileSync20(path, "utf-8");
15765
16644
  } catch {
15766
16645
  text = "";
15767
16646
  }
@@ -15786,7 +16665,7 @@ function validate(options, rules) {
15786
16665
  let manifest = null;
15787
16666
  if (lexists(manifestPath)) {
15788
16667
  try {
15789
- manifest = parseManifest(readFileSync19(manifestPath, "utf-8"));
16668
+ manifest = parseManifest(readFileSync20(manifestPath, "utf-8"));
15790
16669
  } catch (err) {
15791
16670
  const why = err instanceof UnreadableManifestError ? err.message : "it could not be read";
15792
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.`);
@@ -15796,7 +16675,7 @@ function validate(options, rules) {
15796
16675
  let pinReadable = true;
15797
16676
  if (lexists(pinPath)) {
15798
16677
  try {
15799
- const pin = JSON.parse(readFileSync19(pinPath, "utf-8"));
16678
+ const pin = JSON.parse(readFileSync20(pinPath, "utf-8"));
15800
16679
  const id = isPlainObject3(pin) ? pin[PIN_ENGAGEMENT_ID_FIELD] : void 0;
15801
16680
  engagementId = typeof id === "string" && id !== "" ? id : void 0;
15802
16681
  } catch {
@@ -15863,7 +16742,7 @@ function statusLineClaimedFromTree(targetRepo, writeSet) {
15863
16742
  try {
15864
16743
  if (symlinkOnPath(targetRepo, SETTINGS_REL))
15865
16744
  return {};
15866
- const value = JSON.parse(readFileSync19(join22(targetRepo, SETTINGS_REL), "utf-8"));
16745
+ const value = JSON.parse(readFileSync20(join22(targetRepo, SETTINGS_REL), "utf-8"));
15867
16746
  if (!isPlainObject3(value) || !("statusLine" in value))
15868
16747
  return {};
15869
16748
  const hash = canonicalSha256(value["statusLine"]);
@@ -15877,7 +16756,7 @@ function noRecord(targetRepo, writeSet) {
15877
16756
  const claim = (rel, header, lines) => {
15878
16757
  let text;
15879
16758
  try {
15880
- text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync19(join22(targetRepo, rel), "utf-8");
16759
+ text = symlinkOnPath(targetRepo, rel) ? "" : readFileSync20(join22(targetRepo, rel), "utf-8");
15881
16760
  } catch {
15882
16761
  text = "";
15883
16762
  }
@@ -16159,7 +17038,7 @@ function removeSelfBuildCrewRoster(targetRepo, lists, name) {
16159
17038
  const abs = join22(targetRepo, rel);
16160
17039
  let bytes;
16161
17040
  try {
16162
- bytes = readFileSync19(abs);
17041
+ bytes = readFileSync20(abs);
16163
17042
  } catch {
16164
17043
  return;
16165
17044
  }
@@ -16186,7 +17065,7 @@ function removeSelfBuildCrewProfile(targetRepo, rel, text, lists, name) {
16186
17065
  const abs = join22(targetRepo, rel);
16187
17066
  let bytes;
16188
17067
  try {
16189
- bytes = readFileSync19(abs);
17068
+ bytes = readFileSync20(abs);
16190
17069
  } catch {
16191
17070
  return;
16192
17071
  }
@@ -16297,17 +17176,17 @@ function uninstall(options) {
16297
17176
  const manifest = checked.manifest ?? noRecord(targetRepo, writeSet);
16298
17177
  const deleteWhenEmpty = (created) => created || checked.manifest === null;
16299
17178
  const deleteCodexWhenEmpty = (record2) => record2 === void 0 || deleteWhenEmpty(record2.created);
16300
- const named = /* @__PURE__ */ new Set();
17179
+ const named2 = /* @__PURE__ */ new Set();
16301
17180
  const name = (list2, text, path) => {
16302
17181
  list2.push(text);
16303
17182
  if (path !== void 0)
16304
- named.add(path);
17183
+ named2.add(path);
16305
17184
  };
16306
17185
  const settingsEmptyGoes = deleteWhenEmpty(manifest.settings.created);
16307
17186
  applyMergedJson(targetRepo, rules.settings, checked.settings, settingsEmptyGoes, result, name, (value) => {
16308
17187
  const { changed, refused: refused3 } = unmergeSettings(value, manifest.settings, writeSet);
16309
- for (const note of refused3)
16310
- name(result.leftAlone, note);
17188
+ for (const note2 of refused3)
17189
+ name(result.leftAlone, note2);
16311
17190
  return { changed, adopted: manifest.settings.adopted };
16312
17191
  });
16313
17192
  const mcpRecord = manifest.mcp ?? BEFORE_UNKNOWN_MCP;
@@ -16367,17 +17246,17 @@ function uninstall(options) {
16367
17246
  removeInstalledFile(targetRepo, BUNDLE_PIN_REL, recorded.get(BUNDLE_PIN_REL), writeSet, result, name);
16368
17247
  try {
16369
17248
  rmSync6(join22(targetRepo, INSTALL_MANIFEST_REL), { force: true });
16370
- named.add(INSTALL_MANIFEST_REL);
17249
+ named2.add(INSTALL_MANIFEST_REL);
16371
17250
  } catch {
16372
17251
  name(result.failed, INSTALL_MANIFEST_REL, INSTALL_MANIFEST_REL);
16373
17252
  }
16374
17253
  removeEmptyDir(targetRepo, ".halfcycle", writeSet, result, name);
16375
17254
  } else {
16376
- named.add(BUNDLE_PIN_REL);
16377
- named.add(INSTALL_MANIFEST_REL);
17255
+ named2.add(BUNDLE_PIN_REL);
17256
+ named2.add(INSTALL_MANIFEST_REL);
16378
17257
  }
16379
17258
  for (const path of filesUnder(targetRepo, ".halfcycle")) {
16380
- if (!named.has(path))
17259
+ if (!named2.has(path))
16381
17260
  name(result.leftAlone, path, path);
16382
17261
  }
16383
17262
  const exitCode = result.failed.length > 0 || credentialFailed || credential.kind === "no-project-id" ? 1 : 0;
@@ -16417,7 +17296,7 @@ function removeCodexConfigBlock(targetRepo, record2, deleteWhenEmpty, lists, nam
16417
17296
  try {
16418
17297
  if (!lstatSync3(path).isFile())
16419
17298
  return;
16420
- text = readFileSync19(path, "utf-8");
17299
+ text = readFileSync20(path, "utf-8");
16421
17300
  } catch {
16422
17301
  return;
16423
17302
  }
@@ -16466,7 +17345,7 @@ function removeInstalledFile(targetRepo, path, entry, writeSet, lists, name) {
16466
17345
  if (!writeSet.ownedDirs.has(path.slice(0, path.lastIndexOf("/")))) {
16467
17346
  let bytes;
16468
17347
  try {
16469
- bytes = readFileSync19(abs);
17348
+ bytes = readFileSync20(abs);
16470
17349
  } catch {
16471
17350
  name(lists.failed, path, path);
16472
17351
  return;
@@ -16508,7 +17387,7 @@ function trimAppendedFile(targetRepo, rec, deleteWhenEmpty, rules, lists, name)
16508
17387
  const path = join22(targetRepo, rel);
16509
17388
  let before;
16510
17389
  try {
16511
- before = readFileSync19(path, "utf-8");
17390
+ before = readFileSync20(path, "utf-8");
16512
17391
  } catch {
16513
17392
  return [];
16514
17393
  }
@@ -16633,7 +17512,7 @@ function renderUninstallReport(result, home) {
16633
17512
  }
16634
17513
 
16635
17514
  // dist/banner-facts.js
16636
- import { readFileSync as readFileSync20 } from "node:fs";
17515
+ import { readFileSync as readFileSync21 } from "node:fs";
16637
17516
  import { basename as basename4, join as join23, resolve as resolve6 } from "node:path";
16638
17517
  var BRAND_URL = "halfcycle.ai";
16639
17518
  var TAGLINE = [
@@ -16647,7 +17526,7 @@ var TAGLINE_NARROW = [
16647
17526
  ];
16648
17527
  function bundleVersion() {
16649
17528
  try {
16650
- 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"));
16651
17530
  return typeof pkg.version === "string" ? pkg.version : void 0;
16652
17531
  } catch {
16653
17532
  return void 0;
@@ -16885,7 +17764,7 @@ async function runPace(repoRoot, start, deps) {
16885
17764
 
16886
17765
  // dist/restore.js
16887
17766
  import { spawnSync as spawnSync3 } from "node:child_process";
16888
- 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";
16889
17768
  import { homedir as homedir6 } from "node:os";
16890
17769
  import { join as join24, resolve as resolve7 } from "node:path";
16891
17770
  var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
@@ -16962,9 +17841,9 @@ function printable(text) {
16962
17841
  }
16963
17842
  function restoreOne(root, folder, refs, rawId) {
16964
17843
  const valid = savedIdSchema.safeParse(rawId);
16965
- const named = valid.success ? `"${rawId}"` : "with that name";
17844
+ const named2 = valid.success ? `"${rawId}"` : "with that name";
16966
17845
  if (!valid.success || !refs.has(rawId)) {
16967
- 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}`);
16968
17847
  }
16969
17848
  const id = valid.data;
16970
17849
  const saved = readSaved(folder, id);
@@ -17071,7 +17950,7 @@ function sameAs(abs, st, content) {
17071
17950
  try {
17072
17951
  if ("link" in content)
17073
17952
  return st.isSymbolicLink() && readlinkSync(abs) === content.link;
17074
- return st.isFile() && readFileSync21(abs).equals(content.bytes);
17953
+ return st.isFile() && readFileSync22(abs).equals(content.bytes);
17075
17954
  } catch {
17076
17955
  return false;
17077
17956
  }
@@ -17094,7 +17973,7 @@ function restoreCopy(root, saveDir, rel, out) {
17094
17973
  if (st !== null && st.isSymbolicLink())
17095
17974
  return place(root, rel, { link: readlinkSync(src) }, out);
17096
17975
  if (st !== null && st.isFile())
17097
- 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);
17098
17977
  } catch {
17099
17978
  }
17100
17979
  out.left.push(`${rel}: the saved copy does not hold it`);
@@ -17192,7 +18071,7 @@ function restoreLines(root, ref, saved, out) {
17192
18071
  }
17193
18072
  function pinnedEngagement(root) {
17194
18073
  try {
17195
- 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"));
17196
18075
  return isEngagementId(pin.engagementId) ? pin.engagementId : null;
17197
18076
  } catch {
17198
18077
  return null;
@@ -17223,7 +18102,7 @@ function readSaved(folder, id) {
17223
18102
  if (fst === null || !fst.isFile())
17224
18103
  return null;
17225
18104
  try {
17226
- const parsed = savedSchema.safeParse(JSON.parse(readFileSync21(file, "utf-8")));
18105
+ const parsed = savedSchema.safeParse(JSON.parse(readFileSync22(file, "utf-8")));
17227
18106
  return parsed.success && parsed.data.id === id ? parsed.data : null;
17228
18107
  } catch {
17229
18108
  return null;
@@ -17272,7 +18151,7 @@ function pruneAtRestore(root, folder, refs, now) {
17272
18151
  }
17273
18152
  function markedRepository(dir) {
17274
18153
  try {
17275
- 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();
17276
18155
  return text === "" ? null : text;
17277
18156
  } catch {
17278
18157
  return null;
@@ -17376,6 +18255,10 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
17376
18255
  run a task's test command on this machine, once before the change and once after, and record what it saw
17377
18256
  ${CHECK_TASKS_USAGE}
17378
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
17379
18262
  ${CHANGE_USAGE}
17380
18263
  sort a change you are asked for into a small change, a mini phase or a full phase; move it up;
17381
18264
  end a small change and write its record into docs/build-records/changes/
@@ -18046,6 +18929,45 @@ halfcycle build-record: until ${obtained.stampFailure.path} can be written, the
18046
18929
  process.exitCode = run.exitCode;
18047
18930
  return;
18048
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
+ }
18049
18971
  if (cmd === "pace") {
18050
18972
  const parsed = parsePaceArgs(rest);
18051
18973
  if (!parsed.ok) {