halfcycle 0.6.0 → 0.7.1

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
@@ -510,6 +510,8 @@ var HOOK_REMEDY = `Run ${SIGN_IN_COMMAND} in the project folder to sign in.`;
510
510
  import { z as z2 } from "zod";
511
511
  var TASK_PLAN_FILE = "docs/build-records/task-plan.json";
512
512
  var TASK_LIST_FILE = "docs/build-records/build-tasks.json";
513
+ var CRITERIA_RECORD_FILE = "docs/build-records/criteria-check.json";
514
+ var AGENT_NAME_MAX = 32;
513
515
  var TASK_PLAN_TASKS_MAX = 500;
514
516
  var TASK_PLAN_ID_MAX = 64;
515
517
  var TASK_PLAN_BUILDS_NONE = "none";
@@ -518,6 +520,9 @@ var TASK_PLAN_SPECS_NONE = "none";
518
520
  var EXAMPLE_SPEC = "order-intake";
519
521
  var SPEC_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
520
522
  var CITATION = /^([a-z0-9][a-z0-9-]{0,63}) AC((?:0|[1-9][0-9]*)[a-z]?)$/;
523
+ function isCriterionCitation(text) {
524
+ return CITATION.test(text);
525
+ }
521
526
  var TASK_PLAN_KINDS = ["build", "review", "spec", "walk", "check"];
522
527
  var TASK_PLAN_INTERFACE_KINDS = [
523
528
  "route",
@@ -531,6 +536,7 @@ var TASK_PLAN_INTERFACE_KINDS = [
531
536
  var CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
532
537
  var planText = z2.string().refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" });
533
538
  var taskIdSchema = z2.string().min(1).max(TASK_PLAN_ID_MAX).refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" });
539
+ var agentNameText = z2.string().min(1).max(AGENT_NAME_MAX).refine((text) => !CONTROL_CHARACTER.test(text), { message: "holds a control character" });
534
540
  var taskPlanTaskSchema = z2.object({
535
541
  id: taskIdSchema,
536
542
  kind: z2.enum(TASK_PLAN_KINDS),
@@ -545,8 +551,25 @@ var taskPlanTaskSchema = z2.object({
545
551
  ships: z2.enum(["deploy", "release"]).optional(),
546
552
  builds: z2.union([z2.literal(TASK_PLAN_BUILDS_NONE), z2.array(planText)]).optional(),
547
553
  criteria: z2.union([z2.literal(TASK_PLAN_CRITERIA_NONE), z2.array(planText)]).optional(),
548
- whyOutsidePlan: planText.optional()
554
+ whyOutsidePlan: planText.optional(),
555
+ agent: agentNameText.optional(),
556
+ cheapFirst: z2.literal(true).optional(),
557
+ criteriaCheck: z2.literal(true).optional()
549
558
  }).strict().superRefine((task, ctx) => {
559
+ if (task.criteriaCheck !== void 0 && (task.kind !== "check" || task.wholeProject !== void 0)) {
560
+ ctx.addIssue({
561
+ code: "custom",
562
+ path: ["criteriaCheck"],
563
+ message: "only a check task that is not a whole-project check is the check against the acceptance criteria"
564
+ });
565
+ }
566
+ if (task.agent !== void 0 && task.heldBy !== void 0) {
567
+ ctx.addIssue({
568
+ code: "custom",
569
+ path: ["agent"],
570
+ message: "a task a person holds names no crew member"
571
+ });
572
+ }
550
573
  if (task.reviews !== void 0 && task.kind !== "review") {
551
574
  ctx.addIssue({
552
575
  code: "custom",
@@ -582,6 +605,7 @@ var taskListFileSchema = z2.union([
582
605
  z2.object({ phase: planText, tasks: z2.array(taskListRowSchema) }).loose(),
583
606
  z2.array(z2.unknown())
584
607
  ]);
608
+ var PHASE_PLAN_PROBLEMS_SHOWN = 3;
585
609
  var taskPlanLineText = {
586
610
  O1: (a, b, x) => `"${a}" and "${b}" could run at the same time and both change ${x}. Make one wait for the other: add "${a}" to the dependsOn of "${b}", or the other way round.`,
587
611
  O2: (a) => `"${a}" does not say what it touches. List the files and interfaces it changes.`,
@@ -595,6 +619,8 @@ var taskPlanLineText = {
595
619
  O6: (phase, asked) => `The plan is for "${phase}", not "${asked}".`,
596
620
  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
621
  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".`,
622
+ O8NoSuchPart: (a, parts, file, partIds) => parts.length === 1 ? `"${a}" builds "${parts[0]}", which is not a part of the system: ${file} has no part with that id. Use an id from ${file}: ${partIds.join(", ")}.` : `"${a}" builds ${parts.map((x) => `"${x}"`).join(", ")}, which are not parts of the system: ${file} has no part with those ids. Use ids from ${file}: ${partIds.join(", ")}.`,
623
+ O8Unusable: (file, why) => `${file} is there but could not be used, so no task was checked for building a part outside this phase's plan: ${why}. Fix it, then run this again.`,
598
624
  C1Form: (a, x, first) => `"${a}" names "${x}", which is not a criterion. Write it as <spec name> AC<number>, for example "${first} AC1".`,
599
625
  C1Number: (a, x, folder, slug, n) => `"${a}" names "${x}", but ${folder}/${slug}.md has no AC${n}.`,
600
626
  C2: (a, x) => `"${a}" names "${x}", which was retired. Remove it, or name the criterion that replaced it.`,
@@ -604,7 +630,7 @@ var taskPlanLineText = {
604
630
  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
631
  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
632
  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.`,
633
+ B1: (s, t, x, more) => `"${s}" and "${t}" both own ${x}${more === 0 ? "" : more === 1 ? " and 1 more path" : ` and ${more} more paths`}. 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
634
  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
635
  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
636
  N3: (s) => `"${s}" has no Boundary section, so what it owns was not checked.`,
@@ -615,6 +641,19 @@ var taskPlanLineText = {
615
641
  K4: (a, b) => `"${a}" is a hand check but waits for "${b}", so it would run late. A hand check waits only for the work it checks.`,
616
642
  K5: (a) => `"${a}" releases to users and no walk comes before it. Add a walk that waits for every deploy before it, and make "${a}" wait for that walk.`,
617
643
  K6: (a, b, c) => `"${b}" is the walk before "${a}" but does not wait for the deploy "${c}". Make "${b}" wait for "${c}".`,
644
+ V1: () => 'The plan has no check of the finished phase against its acceptance criteria. Add a task of kind "check" with "criteriaCheck": true, held by a crew member who builds none of this phase, that waits for every build task.',
645
+ V2: (w, c) => `"${w}" is a walk, and it does not wait for the check of the phase against its acceptance criteria. Add "${c}" to its dependsOn.`,
646
+ V3Unordered: (c, b) => `"${c}" checks the finished phase, but "${b}" could still be running while it does. Add "${b}" to the dependsOn of "${c}", or, if "${b}" fixes what "${c}" found, name it in ${CRITERIA_RECORD_FILE} and make it wait for "${c}".`,
647
+ V3Ships: (b, c) => `"${b}" ships to users and waits for "${c}", the check of the finished phase. A deploy or a release comes before that check, so the check sees what is live: add "${b}" to the dependsOn of "${c}" and stop "${b}" waiting for "${c}".`,
648
+ V3Unchecked: (b, c) => `"${b}" waits for "${c}", the check of the finished phase, and no later check waits for "${b}", so no check sees what it builds. Add "${b}" to the dependsOn of "${c}" and stop "${b}" waiting for "${c}", or add a check of the phase that waits for "${b}".`,
649
+ V4: (x) => `"${x}" does not say which crew member holds it. Add "agent" with that member's callsign.`,
650
+ V5: (c, m, b) => `"${c}" checks the finished phase, but ${m} also holds "${b}", which builds part of it. Give "${c}" to a crew member who holds no build task in this phase.`,
651
+ V6Walk: (c, n, ids, w) => `${CRITERIA_RECORD_FILE} says "${c}" turned ${n} criteria into fix tasks: ${ids.join(", ")}. "${w}" must wait for a new check that waits for each of them: add a task of kind "check" with "criteriaCheck": true that waits for ${ids.join(", ")}, and add it to the dependsOn of "${w}".`,
652
+ V6Missing: (x) => `${CRITERIA_RECORD_FILE} names "${x}" as a fix task, and the plan has no build task "${x}". Add it, citing the criterion it fixes.`,
653
+ E1: (crit) => `${crit} is an edge case, and no task writes its test before it is built. Add a build task, held by a crew member who builds none of ${crit}, that writes the test and names ${crit} in its criteria, and make each task that builds ${crit} wait for it.`,
654
+ E2: (t, crit, m, b) => `"${t}" writes the test for ${crit}, an edge case, but ${m} also holds "${b}", which builds it. Give "${t}" to a crew member who builds none of ${crit}.`,
655
+ J1: (a) => `"${a}" says "cheapFirst", but it is not a build task. Remove "cheapFirst".`,
656
+ J2: (a, m) => `"${a}" says "cheapFirst", but ${m} is not on the standard tier. Only a standard-tier task can start on the cheaper tier. Remove "cheapFirst" or give "${a}" to a standard-tier crew member.`,
618
657
  P: () => "The plan is clear: no two tasks that could run at once change the same thing, and every check is in place."
619
658
  };
620
659
  function isMalformedPlanPath(path) {
@@ -683,6 +722,31 @@ function firstSharedItem(a, b) {
683
722
  }
684
723
  return null;
685
724
  }
725
+ function sharedPaths(a, b) {
726
+ const out = [];
727
+ const seen = /* @__PURE__ */ new Set();
728
+ const add = (item) => {
729
+ if (seen.has(item.key))
730
+ return;
731
+ seen.add(item.key);
732
+ out.push(item.shown);
733
+ };
734
+ for (const file of a.files) {
735
+ if (b.fileKeys.has(file.key))
736
+ add(file);
737
+ if (file.key.endsWith("/")) {
738
+ for (const other of b.files) {
739
+ if (other.key.startsWith(file.key))
740
+ add(other);
741
+ }
742
+ }
743
+ for (const folder of b.folders) {
744
+ if (file.key.startsWith(folder.key))
745
+ add(file);
746
+ }
747
+ }
748
+ return out;
749
+ }
686
750
  function waitsFor(tasks) {
687
751
  const n = tasks.length;
688
752
  const positionsOf = /* @__PURE__ */ new Map();
@@ -820,10 +884,10 @@ function firstDifference(plan, list2) {
820
884
  const listIds = new Set(list2.map((task) => task.id));
821
885
  for (const task of plan)
822
886
  if (!listIds.has(task.id))
823
- return `"${task.id}"`;
887
+ return `"${task.id}" is in the plan but not in the task list`;
824
888
  for (const task of list2)
825
889
  if (!planIds.has(task.id))
826
- return `"${task.id}"`;
890
+ return `"${task.id}" is in the task list but not in the plan`;
827
891
  const listDeps = /* @__PURE__ */ new Map();
828
892
  for (const task of list2) {
829
893
  const deps = listDeps.get(task.id) ?? /* @__PURE__ */ new Set();
@@ -840,15 +904,17 @@ function firstDifference(plan, list2) {
840
904
  }
841
905
  for (const task of plan) {
842
906
  const theirs = listDeps.get(task.id);
843
- for (const dep of task.dependsOn)
907
+ for (const dep of task.dependsOn) {
844
908
  if (!theirs.has(dep))
845
- return `"${task.id}" \u2192 "${dep}"`;
909
+ return `in the plan "${task.id}" waits for "${dep}", and in the task list it does not`;
910
+ }
846
911
  }
847
912
  for (const task of list2) {
848
913
  const ours = planDeps.get(task.id);
849
- for (const dep of task.dependsOn)
914
+ for (const dep of task.dependsOn) {
850
915
  if (!ours.has(dep))
851
- return `"${task.id}" \u2192 "${dep}"`;
916
+ return `in the task list "${task.id}" waits for "${dep}", and in the plan it does not`;
917
+ }
852
918
  }
853
919
  return null;
854
920
  }
@@ -898,8 +964,8 @@ var CRITERIA_HEADING = /^##\s+Acceptance criteria/;
898
964
  var SECTION_HEADING = /^##\s/;
899
965
  var ANY_HEADING = /^#{1,6}\s/;
900
966
  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}/;
967
+ var EXPLICIT_NUMBER = /^\*\*AC((?:0|[1-9][0-9]*)[a-z]?)( \(edge case\))?(?: —|\.|:)/;
968
+ var RETIRED_LEAD = /^\*\*AC(?:0|[1-9][0-9]*)[a-z]?(?: \(edge case\))? — retired \d{4}-\d{2}-\d{2}/;
903
969
  function readSpecCriteria(markdown) {
904
970
  const lines = markdown.split(/\r?\n/);
905
971
  const start = lines.findIndex((line) => CRITERIA_HEADING.test(line));
@@ -933,14 +999,19 @@ function readSpecCriteria(markdown) {
933
999
  const explicit = EXPLICIT_NUMBER.exec(item);
934
1000
  return {
935
1001
  number: explicit ? explicit[1] : String(i + 1),
936
- retired: RETIRED_LEAD.test(item)
1002
+ retired: RETIRED_LEAD.test(item),
1003
+ kind: explicit?.[2] !== void 0 ? "edge" : "normal"
937
1004
  };
938
1005
  });
939
1006
  }
940
- function liveNumbers(criteria) {
1007
+ function liveCriterionNumbers(criteria) {
941
1008
  const live = new Set(criteria.filter((c) => !c.retired).map((c) => c.number));
942
1009
  return distinct(criteria.map((c) => c.number)).filter((n) => live.has(n));
943
1010
  }
1011
+ function liveEdgeCriterionNumbers(criteria) {
1012
+ const edge = new Set(criteria.filter((c) => !c.retired && c.kind === "edge").map((c) => c.number));
1013
+ return liveCriterionNumbers(criteria).filter((n) => edge.has(n));
1014
+ }
944
1015
  function specLines(plan, specFiles) {
945
1016
  const folder = specFiles.folder;
946
1017
  if (plan.specs === TASK_PLAN_SPECS_NONE)
@@ -968,7 +1039,7 @@ function specLines(plan, specFiles) {
968
1039
  const criteria = criteriaOf.get(slug) ?? null;
969
1040
  if (criteria === null)
970
1041
  continue;
971
- for (const n of liveNumbers(criteria)) {
1042
+ for (const n of liveCriterionNumbers(criteria)) {
972
1043
  if (!covered.has(`${slug} AC${n}`))
973
1044
  lines.push(refusal("C3", taskPlanLineText.C3(slug, n)));
974
1045
  }
@@ -1007,7 +1078,7 @@ function citationLine(taskId, citation, plan, specFiles, criteriaOf) {
1007
1078
  return refusal("C2", taskPlanLineText.C2(taskId, citation));
1008
1079
  return null;
1009
1080
  }
1010
- function shownOwnsText(text) {
1081
+ function shownFileText(text) {
1011
1082
  return text.replace(/[\u0000-\u001f\u007f-\u009f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
1012
1083
  }
1013
1084
  function ownedPathsBySpec(plan, specFiles) {
@@ -1046,9 +1117,9 @@ function ownershipLines(plan, specFiles) {
1046
1117
  continue;
1047
1118
  for (const item of boundary.owns) {
1048
1119
  if (item.kind === "bad") {
1049
- lines.push(refusal("B2", taskPlanLineText.B2(slug, shownOwnsText(item.text))));
1120
+ lines.push(refusal("B2", taskPlanLineText.B2(slug, shownFileText(item.text))));
1050
1121
  } else if (isMalformedOwnsPath(item.path)) {
1051
- lines.push(refusal("B2", taskPlanLineText.B2(slug, shownOwnsText(item.path))));
1122
+ lines.push(refusal("B2", taskPlanLineText.B2(slug, shownFileText(item.path))));
1052
1123
  }
1053
1124
  }
1054
1125
  }
@@ -1065,9 +1136,10 @@ function ownershipLines(plan, specFiles) {
1065
1136
  });
1066
1137
  for (let i = 0; i < owned.length; i += 1) {
1067
1138
  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)));
1139
+ const shared = sharedPaths(owned[i].items, owned[j].items);
1140
+ if (shared.length > 0) {
1141
+ lines.push(refusal("B1", taskPlanLineText.B1(owned[i].slug, owned[j].slug, shared[0], shared.length - 1)));
1142
+ }
1071
1143
  }
1072
1144
  }
1073
1145
  return lines;
@@ -1232,6 +1304,173 @@ function placementLines(tasks, before) {
1232
1304
  });
1233
1305
  return lines;
1234
1306
  }
1307
+ function isCriteriaCheck(task) {
1308
+ return task.kind === "check" && task.criteriaCheck === true;
1309
+ }
1310
+ function lacksCriteriaCheck(tasks) {
1311
+ return tasks.some((task) => task.kind === "build") && !tasks.some(isCriteriaCheck);
1312
+ }
1313
+ function mustNameHolder(task) {
1314
+ return mustNameParts(task) || isCriteriaCheck(task);
1315
+ }
1316
+ function criteriaCheckLines(tasks, before, record2) {
1317
+ const lines = [];
1318
+ const waits = (x, y) => hasBit(before[x], y);
1319
+ const positions = (keep) => {
1320
+ const out = [];
1321
+ tasks.forEach((task, i) => {
1322
+ if (keep(task))
1323
+ out.push(i);
1324
+ });
1325
+ return out;
1326
+ };
1327
+ const builds = positions((task) => task.kind === "build");
1328
+ const checks = positions(isCriteriaCheck);
1329
+ const walks = positions((task) => task.kind === "walk");
1330
+ const releases = positions((task) => task.ships === "release");
1331
+ const exempt = (w) => releases.some((r) => waits(r, w));
1332
+ if (lacksCriteriaCheck(tasks)) {
1333
+ lines.push(refusal("V1", taskPlanLineText.V1()));
1334
+ } else {
1335
+ const last = [...checks].reverse().find((c) => !checks.some((other) => other !== c && waits(other, c)));
1336
+ const named2 = last ?? checks[checks.length - 1];
1337
+ for (const w of walks) {
1338
+ if (exempt(w) || checks.some((c) => waits(w, c)) || named2 === void 0)
1339
+ continue;
1340
+ lines.push(refusal("V2", taskPlanLineText.V2(tasks[w].id, tasks[named2].id)));
1341
+ }
1342
+ const fixes = new Set(record2?.fixTasks ?? []);
1343
+ for (const c of checks) {
1344
+ const cid = tasks[c].id;
1345
+ for (const b of builds) {
1346
+ const bid = tasks[b].id;
1347
+ const checkWaits = waits(c, b);
1348
+ const taskWaits = waits(b, c);
1349
+ if (!checkWaits && !taskWaits) {
1350
+ lines.push(refusal("V3", taskPlanLineText.V3Unordered(cid, bid)));
1351
+ } else if (taskWaits && !fixes.has(bid)) {
1352
+ if (tasks[b].ships !== void 0) {
1353
+ lines.push(refusal("V3", taskPlanLineText.V3Ships(bid, cid)));
1354
+ } else if (!checks.some((other) => other !== c && waits(other, b))) {
1355
+ lines.push(refusal("V3", taskPlanLineText.V3Unchecked(bid, cid)));
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+ for (const c of checks) {
1361
+ const holder = tasks[c].agent;
1362
+ if (holder === void 0)
1363
+ continue;
1364
+ const b = builds.find((p) => tasks[p].agent === holder);
1365
+ if (b !== void 0)
1366
+ lines.push(refusal("V5", taskPlanLineText.V5(tasks[c].id, holder, tasks[b].id)));
1367
+ }
1368
+ }
1369
+ if (record2 !== void 0) {
1370
+ const buildOf = /* @__PURE__ */ new Map();
1371
+ for (const b of builds)
1372
+ if (!buildOf.has(tasks[b].id))
1373
+ buildOf.set(tasks[b].id, b);
1374
+ const ids = distinct(record2.fixTasks);
1375
+ for (const id of ids) {
1376
+ if (!buildOf.has(id))
1377
+ lines.push(refusal("V6", taskPlanLineText.V6Missing(id)));
1378
+ }
1379
+ if (record2.fixTasks.length > 0) {
1380
+ const heldBehindFixes = (c) => tasks[c].id !== record2.check && ids.every((id) => {
1381
+ const f = buildOf.get(id);
1382
+ return f !== void 0 && waits(c, f);
1383
+ });
1384
+ for (const w of walks) {
1385
+ if (exempt(w) || checks.some((c) => waits(w, c) && heldBehindFixes(c)))
1386
+ continue;
1387
+ lines.push(refusal("V6", taskPlanLineText.V6Walk(record2.check, record2.fixTasks.length, ids, tasks[w].id)));
1388
+ }
1389
+ }
1390
+ }
1391
+ return lines;
1392
+ }
1393
+ function edgeCriteriaAt(plan, specFiles, before) {
1394
+ const criteriaOf = specCriteriaBySlug(specFiles);
1395
+ const out = [];
1396
+ for (const slug of planSpecNamesToRead(plan)) {
1397
+ const criteria = criteriaOf.get(slug) ?? null;
1398
+ if (criteria === null)
1399
+ continue;
1400
+ for (const n of liveEdgeCriterionNumbers(criteria)) {
1401
+ const criterion = `${slug} AC${n}`;
1402
+ const builders = [];
1403
+ plan.tasks.forEach((task, i) => {
1404
+ if (task.kind === "build" && Array.isArray(task.criteria) && task.criteria.includes(criterion)) {
1405
+ builders.push(i);
1406
+ }
1407
+ });
1408
+ const testTask = builders.length < 2 ? void 0 : builders.find((t) => builders.every((b) => b === t || hasBit(before[b], t)));
1409
+ out.push({ criterion, slug, builders, testTask: testTask ?? null });
1410
+ }
1411
+ }
1412
+ return out;
1413
+ }
1414
+ function edgeCriterionTests(plan, specFiles) {
1415
+ const { before } = waitsFor(plan.tasks);
1416
+ return edgeCriteriaAt(plan, specFiles, before).map((edge) => ({
1417
+ criterion: edge.criterion,
1418
+ slug: edge.slug,
1419
+ builders: edge.builders.map((i) => plan.tasks[i].id),
1420
+ testTask: edge.testTask === null ? null : plan.tasks[edge.testTask].id
1421
+ }));
1422
+ }
1423
+ function edgeCaseLines(plan, specFiles, before) {
1424
+ const tasks = plan.tasks;
1425
+ const lines = [];
1426
+ for (const edge of edgeCriteriaAt(plan, specFiles, before)) {
1427
+ if (edge.builders.length === 0)
1428
+ continue;
1429
+ if (edge.testTask === null) {
1430
+ lines.push(refusal("E1", taskPlanLineText.E1(edge.criterion)));
1431
+ continue;
1432
+ }
1433
+ const t = edge.testTask;
1434
+ const holder = tasks[t].agent;
1435
+ if (holder === void 0)
1436
+ continue;
1437
+ const b = edge.builders.find((p) => p !== t && tasks[p].agent === holder);
1438
+ if (b !== void 0) {
1439
+ lines.push(refusal("E2", taskPlanLineText.E2(tasks[t].id, edge.criterion, holder, tasks[b].id)));
1440
+ }
1441
+ }
1442
+ return lines;
1443
+ }
1444
+ function phasePlanUnusableWhy(unusable, asked) {
1445
+ switch (unusable.kind) {
1446
+ case "unreadable":
1447
+ return "it could not be opened";
1448
+ case "not-json":
1449
+ return shownFileText(`it is not valid JSON (${unusable.error})`);
1450
+ case "other-phase":
1451
+ return shownFileText(`it is the plan for "${unusable.phase}", not "${asked}"`);
1452
+ case "problems": {
1453
+ const shown2 = unusable.problems.slice(0, PHASE_PLAN_PROBLEMS_SHOWN).map((problem) => `${problem.path === "" ? "the plan" : problem.path} ${problem.message}`);
1454
+ const more = unusable.problems.length - shown2.length;
1455
+ if (shown2.length === 0)
1456
+ return "it does not pass the plan's own check";
1457
+ return shownFileText(more > 0 ? `${shown2.join("; ")}; and ${more} more` : shown2.join("; "));
1458
+ }
1459
+ }
1460
+ }
1461
+ function cheapFirstLine(task, crew) {
1462
+ if (task.cheapFirst !== true)
1463
+ return null;
1464
+ if (task.kind !== "build")
1465
+ return refusal("J1", taskPlanLineText.J1(task.id));
1466
+ if (crew === void 0 || task.agent === void 0)
1467
+ return null;
1468
+ const holder = task.agent;
1469
+ const member = crew.find((m) => m.callsign === holder);
1470
+ if (member !== void 0 && member.tier === "standard")
1471
+ return null;
1472
+ return refusal("J2", taskPlanLineText.J2(task.id, holder));
1473
+ }
1235
1474
  function checkTaskPlan(plan, request) {
1236
1475
  if (plan.phase !== request.phase) {
1237
1476
  return { lines: [refusal("O6", taskPlanLineText.O6(plan.phase, request.phase))], refused: true };
@@ -1241,6 +1480,10 @@ function checkTaskPlan(plan, request) {
1241
1480
  const system = request.system;
1242
1481
  const criteriaOf = specCriteriaBySlug(request.specFiles);
1243
1482
  const phasePlan = request.phasePlan;
1483
+ const noCriteriaCheck = lacksCriteriaCheck(tasks);
1484
+ if (phasePlan !== void 0 && "unusable" in phasePlan) {
1485
+ lines.push(refusal("O8", taskPlanLineText.O8Unusable(phasePlan.file, phasePlanUnusableWhy(phasePlan.unusable, request.phase))));
1486
+ }
1244
1487
  const ids = new Set(tasks.map((task) => task.id));
1245
1488
  const seen = /* @__PURE__ */ new Set();
1246
1489
  const reportedTwice = /* @__PURE__ */ new Set();
@@ -1281,9 +1524,16 @@ function checkTaskPlan(plan, request) {
1281
1524
  if (system !== void 0 && system.partIds.length > 0 && mustNameParts(task) && namesNoPart(task)) {
1282
1525
  lines.push(refusal("O7", taskPlanLineText.O7(task.id, system.file, system.partIds)));
1283
1526
  }
1284
- if (phasePlan !== void 0) {
1527
+ if (phasePlan !== void 0 && "partIds" in phasePlan) {
1285
1528
  for (const found of partsNotInPlan([task], phasePlan.partIds)) {
1286
- lines.push(refusal("O8", taskPlanLineText.O8(found.id, found.parts, phasePlan.file)));
1529
+ const known = new Set(system?.partIds ?? found.parts);
1530
+ const noSuchPart = found.parts.filter((part) => !known.has(part));
1531
+ const outside = found.parts.filter((part) => known.has(part));
1532
+ if (system !== void 0 && noSuchPart.length > 0) {
1533
+ lines.push(refusal("O8", taskPlanLineText.O8NoSuchPart(found.id, noSuchPart, system.file, system.partIds)));
1534
+ }
1535
+ if (outside.length > 0)
1536
+ lines.push(refusal("O8", taskPlanLineText.O8(found.id, outside, phasePlan.file)));
1287
1537
  }
1288
1538
  }
1289
1539
  if (mustNameParts(task) && namesNoCriterion(task)) {
@@ -1296,6 +1546,12 @@ function checkTaskPlan(plan, request) {
1296
1546
  lines.push(line);
1297
1547
  }
1298
1548
  }
1549
+ if (!noCriteriaCheck && mustNameHolder(task) && task.agent === void 0) {
1550
+ lines.push(refusal("V4", taskPlanLineText.V4(task.id)));
1551
+ }
1552
+ const cheap = cheapFirstLine(task, request.crew);
1553
+ if (cheap !== null)
1554
+ lines.push(cheap);
1299
1555
  }
1300
1556
  lines.push(...specLines(plan, request.specFiles));
1301
1557
  const { before, cycles } = waitsFor(tasks);
@@ -1331,12 +1587,160 @@ function checkTaskPlan(plan, request) {
1331
1587
  }
1332
1588
  lines.push(...ownershipNotes(plan, request.specFiles));
1333
1589
  lines.push(...placementLines(tasks, before));
1590
+ lines.push(...criteriaCheckLines(tasks, before, request.criteriaRecord));
1591
+ lines.push(...edgeCaseLines(plan, request.specFiles, before));
1334
1592
  const refused3 = lines.some((line) => line.refuses);
1335
1593
  if (!refused3)
1336
1594
  lines.push({ code: "P", text: taskPlanLineText.P(), refuses: false });
1337
1595
  return { lines, refused: refused3 };
1338
1596
  }
1339
1597
 
1598
+ // ../core/dist/criteria-record.js
1599
+ var criteriaRecordLineText = {
1600
+ M1: (file, p, q) => `${file} is for "${p}", but the plan is for "${q}". Write the record for this phase.`,
1601
+ M2: (file, x) => `${file} is from "${x}", which is not a check of the phase against its criteria in the plan. Name the check task that made it.`,
1602
+ M3: (file, m, c, k) => `${file} is by ${m}, but "${c}" is held by ${k}. The record names the crew member who holds the check.`,
1603
+ M4: (crit) => `${crit} has no row. Check it, and add it as met with what shows it, or as a fix with the gap and the fix task.`,
1604
+ M5: (file, x, folder) => `${file} has a row for "${x}", which is not a live criterion of this phase's features in ${folder}. Remove it, or correct it.`,
1605
+ M6: (crit) => `${crit} has more than one row. Keep one.`,
1606
+ M7: (crit, x) => `${crit} is fixed by "${x}", but the plan has no build task "${x}" that names ${crit} in its criteria. Add the task, or name it there.`,
1607
+ M8Unmarked: (crit, folder, slug) => `${crit} is not marked an edge case in ${folder}/${slug}.md, so its row does not carry "edge".`,
1608
+ M8Marked: (crit, folder, slug) => `${crit} is marked an edge case in ${folder}/${slug}.md, so its row carries "edge": true.`,
1609
+ M9Evidence: (crit, t) => `${crit} is an edge case, so it is met only by the test written in "${t}", run on a task that builds it. Give its evidence as a test with "writtenIn": "${t}" and the task that built it.`,
1610
+ M9NoTestTask: (crit) => `${crit} is an edge case, and the plan has no task that writes its test, so it cannot be counted as met. Run check-tasks and add that task.`,
1611
+ P: (met, fix) => `The record covers every criterion of this phase's features: ${met} met, ${fix} to fix.`
1612
+ };
1613
+ function criteriaRecordMismatch(record2, plan) {
1614
+ if (record2.subject.phase !== plan.phase)
1615
+ return "phase";
1616
+ if (!plan.tasks.some((task) => task.id === record2.check && isCriteriaCheck(task)))
1617
+ return "check";
1618
+ return null;
1619
+ }
1620
+ function liveCriteria(specFiles) {
1621
+ const out = [];
1622
+ const seen = /* @__PURE__ */ new Set();
1623
+ for (const entry of specFiles.entries) {
1624
+ if (entry.criteria === null || seen.has(entry.slug))
1625
+ continue;
1626
+ seen.add(entry.slug);
1627
+ for (const n of liveCriterionNumbers(entry.criteria))
1628
+ out.push(`${entry.slug} AC${n}`);
1629
+ }
1630
+ return out;
1631
+ }
1632
+ function liveEdgeCriteria(specFiles) {
1633
+ const out = /* @__PURE__ */ new Set();
1634
+ const seen = /* @__PURE__ */ new Set();
1635
+ for (const entry of specFiles.entries) {
1636
+ if (entry.criteria === null || seen.has(entry.slug))
1637
+ continue;
1638
+ seen.add(entry.slug);
1639
+ for (const n of liveEdgeCriterionNumbers(entry.criteria))
1640
+ out.add(`${entry.slug} AC${n}`);
1641
+ }
1642
+ return out;
1643
+ }
1644
+ function slugOf(criterion) {
1645
+ return criterion.slice(0, criterion.lastIndexOf(" AC"));
1646
+ }
1647
+ function refusal2(code, text) {
1648
+ return { code, text, refuses: true };
1649
+ }
1650
+ function edgeRowLines(row, isEdge, testTask, plan, folder) {
1651
+ const crit = row.criterion;
1652
+ const lines = [];
1653
+ const marked = row.edge === true;
1654
+ if (marked && !isEdge)
1655
+ lines.push(refusal2("M8", criteriaRecordLineText.M8Unmarked(crit, folder, slugOf(crit))));
1656
+ if (!marked && isEdge)
1657
+ lines.push(refusal2("M8", criteriaRecordLineText.M8Marked(crit, folder, slugOf(crit))));
1658
+ if (!isEdge || row.status !== "met")
1659
+ return lines;
1660
+ if (testTask === null) {
1661
+ lines.push(refusal2("M9", criteriaRecordLineText.M9NoTestTask(crit)));
1662
+ return lines;
1663
+ }
1664
+ const evidence = row.evidence;
1665
+ const builtBy = (id) => id !== testTask && plan.tasks.some((task) => task.id === id && task.kind === "build" && Array.isArray(task.criteria) && task.criteria.includes(crit));
1666
+ const held = evidence.kind === "test" && evidence.writtenIn === testTask && builtBy(evidence.task);
1667
+ if (!held)
1668
+ lines.push(refusal2("M9", criteriaRecordLineText.M9Evidence(crit, testTask)));
1669
+ return lines;
1670
+ }
1671
+ function checkCriteriaRecord(record2, plan, request) {
1672
+ const { file, specFiles } = request;
1673
+ let met = 0;
1674
+ let fix = 0;
1675
+ for (const row of record2.rows) {
1676
+ if (row.status === "met")
1677
+ met += 1;
1678
+ else
1679
+ fix += 1;
1680
+ }
1681
+ const answer = (lines2) => {
1682
+ const refused3 = lines2.some((line) => line.refuses);
1683
+ return {
1684
+ lines: refused3 ? lines2 : [{ code: "P", text: criteriaRecordLineText.P(met, fix), refuses: false }],
1685
+ refused: refused3,
1686
+ met,
1687
+ fix
1688
+ };
1689
+ };
1690
+ const mismatch = criteriaRecordMismatch(record2, plan);
1691
+ if (mismatch === "phase") {
1692
+ return answer([refusal2("M1", criteriaRecordLineText.M1(file, record2.subject.phase, plan.phase))]);
1693
+ }
1694
+ const lines = [];
1695
+ if (mismatch === "check") {
1696
+ lines.push(refusal2("M2", criteriaRecordLineText.M2(file, record2.check)));
1697
+ } else {
1698
+ const holder = plan.tasks.find((task) => task.id === record2.check)?.agent;
1699
+ if (holder !== void 0 && holder !== record2.by) {
1700
+ lines.push(refusal2("M3", criteriaRecordLineText.M3(file, record2.by, record2.check, holder)));
1701
+ }
1702
+ }
1703
+ const live = liveCriteria(specFiles);
1704
+ const liveSet = new Set(live);
1705
+ const edges = liveEdgeCriteria(specFiles);
1706
+ const testTaskOf = /* @__PURE__ */ new Map();
1707
+ for (const edge of edgeCriterionTests(plan, specFiles)) {
1708
+ if (!testTaskOf.has(edge.criterion))
1709
+ testTaskOf.set(edge.criterion, edge.testTask);
1710
+ }
1711
+ const named2 = /* @__PURE__ */ new Set();
1712
+ const told = /* @__PURE__ */ new Set();
1713
+ for (const row of record2.rows) {
1714
+ const crit = row.criterion;
1715
+ if (!liveSet.has(crit)) {
1716
+ if (!told.has(crit))
1717
+ lines.push(refusal2("M5", criteriaRecordLineText.M5(file, crit, specFiles.folder)));
1718
+ told.add(crit);
1719
+ named2.add(crit);
1720
+ continue;
1721
+ }
1722
+ if (named2.has(crit)) {
1723
+ if (!told.has(crit))
1724
+ lines.push(refusal2("M6", criteriaRecordLineText.M6(crit)));
1725
+ told.add(crit);
1726
+ }
1727
+ named2.add(crit);
1728
+ lines.push(...edgeRowLines(row, edges.has(crit), testTaskOf.get(crit) ?? null, plan, specFiles.folder));
1729
+ }
1730
+ for (const row of record2.rows) {
1731
+ if (row.status !== "fix" || !liveSet.has(row.criterion))
1732
+ continue;
1733
+ const fixes = plan.tasks.some((task) => task.id === row.task && task.kind === "build" && Array.isArray(task.criteria) && task.criteria.includes(row.criterion));
1734
+ if (!fixes)
1735
+ lines.push(refusal2("M7", criteriaRecordLineText.M7(row.criterion, row.task)));
1736
+ }
1737
+ for (const crit of live) {
1738
+ if (!named2.has(crit))
1739
+ lines.push(refusal2("M4", criteriaRecordLineText.M4(crit)));
1740
+ }
1741
+ return answer(lines);
1742
+ }
1743
+
1340
1744
  // ../core/dist/spec-boundary.js
1341
1745
  var SPEC_BOUNDARY_OWNS_LABEL = "**Owns.**";
1342
1746
  var SPEC_BOUNDARY_LABELS = [
@@ -1345,6 +1749,7 @@ var SPEC_BOUNDARY_LABELS = [
1345
1749
  "**Allowed dependencies.**",
1346
1750
  "**Revalidation triggers.**"
1347
1751
  ];
1752
+ var REPEATED_OWNS_TEXT = "a second **Owns.** label: only the first list is read, so keep every path under one **Owns.** label";
1348
1753
  var BOUNDARY_HEADING = /^## Boundary[ \t]*$/;
1349
1754
  var SECTION_HEADING2 = /^## /;
1350
1755
  var FENCE = /^ {0,3}(`{3,}|~{3,})/;
@@ -1416,9 +1821,13 @@ function readSpecBoundary(markdown) {
1416
1821
  const trimmed = line.trimEnd();
1417
1822
  if (SPEC_BOUNDARY_LABELS.includes(trimmed)) {
1418
1823
  closeList();
1419
- if (trimmed === SPEC_BOUNDARY_OWNS_LABEL && !ownsSeen) {
1420
- ownsSeen = true;
1421
- listOpen = true;
1824
+ if (trimmed === SPEC_BOUNDARY_OWNS_LABEL) {
1825
+ if (ownsSeen) {
1826
+ owns.push({ kind: "bad", text: REPEATED_OWNS_TEXT });
1827
+ } else {
1828
+ ownsSeen = true;
1829
+ listOpen = true;
1830
+ }
1422
1831
  }
1423
1832
  continue;
1424
1833
  }
@@ -1823,8 +2232,8 @@ import { z as z5 } from "zod";
1823
2232
  // ../events/dist/human-agent.js
1824
2233
  import { z as z4 } from "zod";
1825
2234
  var HUMAN_AGENT = "Human";
1826
- var AGENT_NAME_MAX = 32;
1827
- var agentNameSchema = z4.string().min(1).max(AGENT_NAME_MAX);
2235
+ var AGENT_NAME_MAX2 = AGENT_NAME_MAX;
2236
+ var agentNameSchema = z4.string().min(1).max(AGENT_NAME_MAX2);
1828
2237
 
1829
2238
  // ../events/dist/state.js
1830
2239
  var utcIso8601 = z5.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/, "must be a UTC ISO-8601 timestamp ending in Z");
@@ -2323,7 +2732,7 @@ function isMissingKey(raw, path) {
2323
2732
  }
2324
2733
  return isRecord(parent) && !(String(path[path.length - 1]) in parent);
2325
2734
  }
2326
- function shapeProblemsOf(schema, raw, undeclared) {
2735
+ function shapeProblemsOf(schema, raw, undeclared, required = {}) {
2327
2736
  const parsed = schema.safeParse(raw);
2328
2737
  if (parsed.success)
2329
2738
  return [];
@@ -2334,7 +2743,11 @@ function shapeProblemsOf(schema, raw, undeclared) {
2334
2743
  problems.push({ path: dotted([...issue.path, key]), message: undeclared });
2335
2744
  }
2336
2745
  } else if (isMissingKey(raw, issue.path)) {
2337
- problems.push({ path: dotted(issue.path), message: "is required" });
2746
+ const path = dotted(issue.path);
2747
+ if (Object.hasOwn(required, path))
2748
+ problems.push({ path, message: required[path] });
2749
+ else
2750
+ problems.push({ path: dotted(issue.path), message: "is required" });
2338
2751
  } else {
2339
2752
  problems.push({ path: dotted(issue.path), message: issue.message });
2340
2753
  }
@@ -2417,8 +2830,11 @@ var systemDescriptionProblemSchema = z7.object({
2417
2830
  path: z7.string(),
2418
2831
  message: z7.string()
2419
2832
  }).strict();
2833
+ var CONNECTIONS_REQUIRED = "is required; an empty list, [], is fine when no part connects to another";
2420
2834
  function shapeProblems(raw) {
2421
- return shapeProblemsOf(systemDescriptionSchema, raw, "is not a field a description has");
2835
+ return shapeProblemsOf(systemDescriptionSchema, raw, "is not a field a description has", {
2836
+ connections: CONNECTIONS_REQUIRED
2837
+ });
2422
2838
  }
2423
2839
  function quoted(value) {
2424
2840
  return typeof value === "string" ? shown(value) : "another part";
@@ -3889,9 +4305,54 @@ var workCallRecordSchema = z15.object({
3889
4305
  message: "says how to undo it exactly when it can be undone",
3890
4306
  path: ["how"]
3891
4307
  });
4308
+ var WORK_CRITERIA_ROWS_MAX = 500;
4309
+ var workCriterionSchema = agentWorkText.refine((t) => isCriterionCitation(t), {
4310
+ message: "must name a criterion as its spec name, a space, then AC and its number, such as order-intake AC1"
4311
+ });
4312
+ var workCriteriaTestEvidenceSchema = z15.object({
4313
+ kind: z15.literal("test"),
4314
+ task: buildTaskIdSchema,
4315
+ label: agentWorkText,
4316
+ writtenIn: buildTaskIdSchema.optional()
4317
+ }).strict();
4318
+ var workCriteriaPathEvidenceSchema = z15.object({
4319
+ kind: z15.literal("path"),
4320
+ path: agentWorkText
4321
+ }).strict();
4322
+ var workCriteriaWalkEvidenceSchema = z15.object({
4323
+ kind: z15.literal("walk"),
4324
+ leg: agentWorkText
4325
+ }).strict();
4326
+ var workCriteriaEvidenceSchema = z15.discriminatedUnion("kind", [
4327
+ workCriteriaTestEvidenceSchema,
4328
+ workCriteriaPathEvidenceSchema,
4329
+ workCriteriaWalkEvidenceSchema
4330
+ ]);
4331
+ var workCriteriaMetRowSchema = z15.object({
4332
+ criterion: workCriterionSchema,
4333
+ edge: z15.literal(true).optional(),
4334
+ status: z15.literal("met"),
4335
+ evidence: workCriteriaEvidenceSchema
4336
+ }).strict();
4337
+ var workCriteriaFixRowSchema = z15.object({
4338
+ criterion: workCriterionSchema,
4339
+ edge: z15.literal(true).optional(),
4340
+ status: z15.literal("fix"),
4341
+ gap: agentWorkText,
4342
+ task: buildTaskIdSchema
4343
+ }).strict();
4344
+ var workCriteriaRowSchema = z15.discriminatedUnion("status", [workCriteriaMetRowSchema, workCriteriaFixRowSchema]);
4345
+ var workCriteriaRecordSchema = z15.object({
4346
+ record: z15.literal("criteria"),
4347
+ subject: workPhaseSubjectSchema,
4348
+ check: buildTaskIdSchema,
4349
+ by: agentNameSchema,
4350
+ rows: z15.array(workCriteriaRowSchema).min(1, { message: "must carry at least one criterion" }).max(WORK_CRITERIA_ROWS_MAX, { message: `can carry at most ${WORK_CRITERIA_ROWS_MAX} criteria` })
4351
+ }).strict();
3892
4352
  var workRecordSchema = z15.discriminatedUnion("record", [
3893
4353
  workReviewRecordSchema,
3894
4354
  workOutcomesRecordSchema,
4355
+ workCriteriaRecordSchema,
3895
4356
  workClaimsRecordSchema,
3896
4357
  workDiagnosisRecordSchema,
3897
4358
  workCallRecordSchema
@@ -3938,7 +4399,8 @@ var WORK_REFUSAL_STATUS = Object.freeze({
3938
4399
  held: 409,
3939
4400
  "attempt-limit": 409,
3940
4401
  "same-attempt": 409,
3941
- "call-not-recorded": 409
4402
+ "call-not-recorded": 409,
4403
+ "other-version": 409
3942
4404
  });
3943
4405
  var WORK_REFUSALS = Object.freeze(Object.keys(WORK_REFUSAL_STATUS));
3944
4406
  var workRefusalSchema = z15.enum(WORK_REFUSALS);
@@ -5803,58 +6265,78 @@ var ownerWaitChatResultSchema = z32.discriminatedUnion("outcome", [
5803
6265
 
5804
6266
  // ../events/dist/pacing.js
5805
6267
  import { z as z33 } from "zod";
5806
- var CLAUDE_PLANS = Object.freeze([
5807
- Object.freeze({ id: "pro", words: "Pro", startingAgents: 2 }),
5808
- Object.freeze({ id: "max-5x", words: "Max (5\xD7)", startingAgents: 4 }),
5809
- Object.freeze({ id: "max-20x", words: "Max (20\xD7)", startingAgents: 8 }),
5810
- Object.freeze({ id: "team", words: "Team", startingAgents: 3 }),
5811
- Object.freeze({ id: "enterprise", words: "Enterprise", startingAgents: 3 }),
5812
- Object.freeze({ id: "api-key", words: "I pay for what I use, with an API key", startingAgents: 8 }),
5813
- Object.freeze({ id: "not-sure", words: "Not sure", startingAgents: 2 })
5814
- ]);
5815
- var CLAUDE_PLAN_IDS = Object.freeze(CLAUDE_PLANS.map((plan) => plan.id));
5816
- var claudePlanSchema = z33.enum(CLAUDE_PLAN_IDS);
5817
- var CLAUDE_PLAN_UNKNOWN = "not-sure";
5818
- function claudePlanOf(id) {
5819
- return CLAUDE_PLANS.find((plan) => plan.id === id) ?? CLAUDE_PLANS.find((plan) => plan.id === CLAUDE_PLAN_UNKNOWN) ?? CLAUDE_PLANS[0];
5820
- }
5821
- function claudePlanKnown(id) {
5822
- return id !== null && id !== CLAUDE_PLAN_UNKNOWN;
5823
- }
5824
- var CLAUDE_PLAN_SET_IN = ["setup", "console"];
5825
- var claudePlanSetInSchema = z33.enum(CLAUDE_PLAN_SET_IN);
5826
- var KEEP_FREE_PERCENTS = [0, 20];
5827
- var keepFreePercentSchema = z33.union([z33.literal(KEEP_FREE_PERCENTS[0]), z33.literal(KEEP_FREE_PERCENTS[1])]);
6268
+ var KEEP_FREE_MAX = 90;
6269
+ var keepFreePercentSchema = z33.number().int().min(0).max(KEEP_FREE_MAX);
6270
+ var KEEP_FREE_DEFAULT = 20;
5828
6271
  var PACE_AGENTS_MAX = 20;
5829
- var PACE_WINDOW_KINDS = ["five-hour", "seven-day"];
5830
- var paceWindowKindSchema = z33.enum(PACE_WINDOW_KINDS);
5831
- var PACE_WINDOW_WORDS = Object.freeze({
5832
- "five-hour": "5-hour window",
5833
- "seven-day": "weekly window"
6272
+ var FIVE_HOUR_WINDOW_MINUTES = 300;
6273
+ var SEVEN_DAY_WINDOW_MINUTES = 10080;
6274
+ var PACE_WINDOW_MINUTES_MAX = 525600;
6275
+ var MINUTE_KEY = /^([1-9][0-9]{0,6})-minute$/;
6276
+ function isPaceWindowKind(value) {
6277
+ if (value === "five-hour" || value === "seven-day")
6278
+ return true;
6279
+ if (typeof value !== "string")
6280
+ return false;
6281
+ const match = MINUTE_KEY.exec(value);
6282
+ if (match === null)
6283
+ return false;
6284
+ const minutes = Number(match[1]);
6285
+ return minutes <= PACE_WINDOW_MINUTES_MAX && minutes !== FIVE_HOUR_WINDOW_MINUTES && minutes !== SEVEN_DAY_WINDOW_MINUTES;
6286
+ }
6287
+ var paceWindowKindSchema = z33.custom((value) => isPaceWindowKind(value), {
6288
+ message: "not a usage window key"
5834
6289
  });
6290
+ function paceWindowMinutesOf(kind) {
6291
+ if (kind === "five-hour")
6292
+ return FIVE_HOUR_WINDOW_MINUTES;
6293
+ if (kind === "seven-day")
6294
+ return SEVEN_DAY_WINDOW_MINUTES;
6295
+ return Number(MINUTE_KEY.exec(kind)?.[1] ?? 0);
6296
+ }
6297
+ function paceWindowWordsOf(kind) {
6298
+ const minutes = paceWindowMinutesOf(kind);
6299
+ if (minutes === FIVE_HOUR_WINDOW_MINUTES)
6300
+ return "5-hour window";
6301
+ if (minutes === SEVEN_DAY_WINDOW_MINUTES)
6302
+ return "weekly window";
6303
+ if (minutes % 1440 === 0)
6304
+ return `${String(minutes / 1440)}-day window`;
6305
+ if (minutes % 60 === 0)
6306
+ return `${String(minutes / 60)}-hour window`;
6307
+ return `${String(minutes)}-minute window`;
6308
+ }
6309
+ function paceShareWordsOf(keepFreePercent) {
6310
+ return keepFreePercent === 20 ? "a fifth" : `${String(keepFreePercent)}%`;
6311
+ }
5835
6312
  var PACE_STATES = ["full", "slowing", "holding"];
5836
6313
  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({
5840
- claudePlan: claudePlanSchema.nullable(),
5841
- claudePlanSetIn: claudePlanSetInSchema.nullable(),
5842
- keepFreePercent: keepFreePercentSchema
5843
- }).strict().refine((s) => s.claudePlan === null === (s.claudePlanSetIn === null), {
5844
- message: "a plan says where it was given, and only a plan does",
5845
- path: ["claudePlanSetIn"]
5846
- });
6314
+ var PACE_SHARE_GIVEN_IN = ["chat", "agent", "relayed"];
6315
+ var paceShareGivenInSchema = z33.enum(PACE_SHARE_GIVEN_IN);
6316
+ var paceShareRequestSchema = z33.object({ keepFreePercent: keepFreePercentSchema, givenIn: paceShareGivenInSchema }).strict();
6317
+ var paceShareResultSchema = z33.discriminatedUnion("outcome", [
6318
+ z33.object({
6319
+ outcome: z33.literal("recorded"),
6320
+ keepFreePercent: keepFreePercentSchema,
6321
+ moreAllowance: z33.boolean().optional()
6322
+ }).strict(),
6323
+ z33.object({ outcome: z33.literal("needs-the-person"), keepFreePercent: keepFreePercentSchema }).strict(),
6324
+ z33.object({ outcome: z33.literal("not-owner") }).strict(),
6325
+ z33.object({ outcome: z33.literal("no-owner") }).strict(),
6326
+ z33.object({ outcome: z33.literal("unavailable") }).strict()
6327
+ ]);
5847
6328
  var pacingBasisTaskSchema = z33.object({
5848
6329
  id: buildTaskIdSchema,
5849
6330
  dependsOn: z33.array(buildTaskIdSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5850
6331
  budgetMinutes: z33.number().int().min(1).max(BUILD_TASK_BUDGET_MAX_MINUTES).optional(),
5851
- human: z33.boolean()
6332
+ human: z33.boolean(),
6333
+ kind: buildTaskKindSchema.optional()
5852
6334
  }).strict();
5853
6335
  var pacingBasisSchema = z33.object({
5854
6336
  outcome: z33.literal("basis"),
5855
- claudePlan: claudePlanSchema.nullable(),
6337
+ claudePlan: z33.string().nullable().optional(),
5856
6338
  keepFreePercent: keepFreePercentSchema,
5857
- settingsUrl: z33.string().min(1),
6339
+ settingsUrl: z33.string().optional(),
5858
6340
  phase: phaseNameSchema.nullable(),
5859
6341
  tasks: z33.array(pacingBasisTaskSchema).max(BUILD_SNAPSHOT_TASK_LIMIT),
5860
6342
  medianTaskMinutes: z33.number().nonnegative().nullable(),
@@ -5864,12 +6346,21 @@ var paceSuggestionSchema = z33.object({
5864
6346
  number: z33.number().int().min(1).max(PACE_AGENTS_MAX),
5865
6347
  minutes: z33.number().int().nonnegative().nullable(),
5866
6348
  pauses: z33.number().int().nonnegative(),
5867
- claudePlan: claudePlanSchema.nullable(),
5868
6349
  keepFreePercent: keepFreePercentSchema,
5869
- settingsUrl: z33.string().min(1).nullable(),
5870
- knownWindow: paceWindowKindSchema.nullable()
6350
+ knownWindow: paceWindowKindSchema.nullable(),
6351
+ expectedTokens: z33.number().int().nonnegative().optional(),
6352
+ limitTokens: z33.number().int().positive().optional(),
6353
+ multiple: z33.number().positive().nullable().optional(),
6354
+ firstGuess: z33.boolean().optional(),
6355
+ limitSetBy: z33.enum(["default", "person", "other"]).optional()
5871
6356
  }).strict();
5872
6357
  var PACE_NOT_PACED_REASONS = ["not-configured", "unreadable"];
6358
+ var limitHeldSchema = z33.object({
6359
+ used: z33.number().int().nonnegative(),
6360
+ limit: z33.number().int().positive(),
6361
+ raiseBy: z33.number().int().nonnegative(),
6362
+ working: z33.number().int().nonnegative()
6363
+ }).strict();
5873
6364
  var paceRunResultSchema = z33.discriminatedUnion("outcome", [
5874
6365
  z33.object({
5875
6366
  outcome: z33.literal("paced"),
@@ -5878,83 +6369,93 @@ var paceRunResultSchema = z33.discriminatedUnion("outcome", [
5878
6369
  allowedNow: z33.number().int().nonnegative(),
5879
6370
  windowKind: paceWindowKindSchema.optional(),
5880
6371
  until: utcIso8601.optional(),
5881
- suggestion: paceSuggestionSchema.optional()
6372
+ keepFreePercent: keepFreePercentSchema.optional(),
6373
+ suggestion: paceSuggestionSchema.optional(),
6374
+ limit: z33.literal("reached").optional(),
6375
+ limitHeld: limitHeldSchema.optional()
5882
6376
  }).strict(),
5883
6377
  z33.object({ outcome: z33.literal("not-paced"), reason: z33.enum(PACE_NOT_PACED_REASONS) }).strict()
5884
6378
  ]);
6379
+ function tokenCountWordsOf(n) {
6380
+ if (n < 1e3)
6381
+ return String(Math.max(0, Math.round(n)));
6382
+ const rounded = Number(n.toPrecision(2));
6383
+ const [unit, word] = rounded >= 1e9 ? [1e9, "billion"] : rounded >= 1e6 ? [1e6, "million"] : [1e3, "thousand"];
6384
+ return `${String(Number((rounded / unit).toPrecision(2)))} ${word}`;
6385
+ }
5885
6386
  function paceAgentsOf(n) {
5886
6387
  return n === 1 ? "1 agent" : `${String(n)} agents`;
5887
6388
  }
5888
6389
  var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
6390
+ var MONTH_NAMES = [
6391
+ "January",
6392
+ "February",
6393
+ "March",
6394
+ "April",
6395
+ "May",
6396
+ "June",
6397
+ "July",
6398
+ "August",
6399
+ "September",
6400
+ "October",
6401
+ "November",
6402
+ "December"
6403
+ ];
6404
+ var SIX_DAYS_MS = 6 * 24 * 60 * 60 * 1e3;
5889
6405
  function paceClockOf(at, now) {
5890
6406
  const hours = at.getHours();
5891
6407
  const clock = `${String(hours % 12 === 0 ? 12 : hours % 12)}:${String(at.getMinutes()).padStart(2, "0")} ${hours < 12 ? "am" : "pm"}`;
5892
6408
  const sameDay = at.getFullYear() === now.getFullYear() && at.getMonth() === now.getMonth() && at.getDate() === now.getDate();
5893
- return sameDay ? clock : `${DAY_NAMES[at.getDay()] ?? ""} ${clock}`;
5894
- }
5895
- var CLAUDE_PLAN_QUESTION = [
5896
- "Which Claude plan do you use? Halfcycle paces builds so they leave part of your allowance for your own work.",
5897
- CLAUDE_PLANS.map((plan, i) => `${String(i + 1)}) ${plan.words}`).join(" "),
5898
- `Type a number, or press Enter for "${claudePlanOf(CLAUDE_PLAN_UNKNOWN).words}":`
5899
- ].join("\n");
5900
- function claudePlanFromAnswer(answer) {
5901
- const trimmed = answer.trim();
5902
- if (trimmed === "")
5903
- return CLAUDE_PLAN_UNKNOWN;
5904
- if (!/^[1-9][0-9]*$/.test(trimmed))
5905
- return null;
5906
- return CLAUDE_PLANS[Number(trimmed) - 1]?.id ?? null;
5907
- }
5908
- function claudePlanRememberedText(plan, settingsUrl) {
5909
- return `Using the plan you gave before: ${claudePlanOf(plan).words}. You can change it on your settings page, not by typing in chat: ${settingsUrl}`;
6409
+ if (sameDay)
6410
+ return clock;
6411
+ const day = DAY_NAMES[at.getDay()] ?? "";
6412
+ if (at.getTime() - now.getTime() > SIX_DAYS_MS) {
6413
+ return `${day} ${String(at.getDate())} ${MONTH_NAMES[at.getMonth()] ?? ""} ${clock}`;
6414
+ }
6415
+ return `${day} ${clock}`;
5910
6416
  }
5911
6417
  function paceP1Of(n, minutes) {
5912
6418
  const first = `Halfcycle suggests running ${paceAgentsOf(n)} at once.`;
5913
6419
  return minutes === null ? first : `${first} The work should take about ${timeDurationLabelOf(minutes * 60)}.`;
5914
6420
  }
5915
6421
  var PACE_NO_BUILD_PLAN = "Halfcycle has no build plan for this phase yet, so it cannot say how long the work will take.";
5916
- function paceP2Of(plan) {
5917
- return `This is a cautious start for your ${claudePlanOf(plan).words} plan. It leaves room for your own work.`;
5918
- }
5919
- function paceP3Of(window) {
5920
- return `This keeps about a fifth of each ${PACE_WINDOW_WORDS[window]} free for your own work.`;
5921
- }
5922
- function paceP4Of(n, settingsUrl) {
5923
- const first = `Halfcycle does not know which Claude plan you use, so it starts with ${String(n)}.`;
5924
- return settingsUrl === null ? first : `${first} You can say which on your settings page, not by typing in chat: ${settingsUrl}`;
6422
+ var PACE_P2 = "This is a careful start, so your allowance lasts. It leaves room for your own work.";
6423
+ function paceP3Of(keepFreePercent) {
6424
+ return `This keeps about ${paceShareWordsOf(keepFreePercent)} of each usage window free for your own work.`;
5925
6425
  }
6426
+ var PACE_P4 = 'Say "faster" for one more agent at once, or "run 5 agents" for a number. More agents use your allowance faster.';
5926
6427
  var PACE_P5 = "This build may use all of your allowance. It will not slow down to leave room for you.";
5927
6428
  function paceP6Of(k) {
5928
6429
  return `It will probably pause ${k === 1 ? "once" : `${String(k)} times`} while your usage window resets.`;
5929
6430
  }
5930
- function paceP7Of(settingsUrl) {
5931
- const first = "Say a different number to change it.";
5932
- return settingsUrl === null ? first : `${first} To let builds use all of your allowance, change it on your settings page, not by typing in chat: ${settingsUrl}`;
5933
- }
5934
- function paceStartLinesOf(s) {
6431
+ var PACE_P7 = 'Or say how much of your allowance to keep free, like "keep 30% free", or how much this build may use, like "use up to 70%".';
6432
+ var PACE_P8 = 'For these to work, run "npx halfcycle" once more in this project.';
6433
+ function paceStartLinesOf(s, promptScriptBehind2 = false) {
5935
6434
  const lines = [paceP1Of(s.number, s.minutes)];
5936
6435
  if (s.minutes === null)
5937
6436
  lines.push(PACE_NO_BUILD_PLAN);
5938
- if (s.keepFreePercent !== 0) {
5939
- if (s.knownWindow !== null)
5940
- lines.push(paceP3Of(s.knownWindow));
5941
- else if (s.claudePlan !== null && claudePlanKnown(s.claudePlan))
5942
- lines.push(paceP2Of(s.claudePlan));
5943
- }
5944
- if (!claudePlanKnown(s.claudePlan))
5945
- lines.push(paceP4Of(s.number, s.settingsUrl));
5946
6437
  if (s.keepFreePercent === 0)
5947
6438
  lines.push(PACE_P5);
6439
+ else
6440
+ lines.push(s.knownWindow !== null ? paceP3Of(s.keepFreePercent) : PACE_P2);
6441
+ if (s.limitTokens !== void 0 && s.expectedTokens !== void 0) {
6442
+ lines.push(limitL1Of(s.expectedTokens, s.limitTokens, s.multiple ?? null, s.limitSetBy ?? "default"));
6443
+ if (s.firstGuess === true)
6444
+ lines.push(LIMIT_L1B);
6445
+ } else
6446
+ lines.push(LIMIT_L1C);
5948
6447
  if (s.pauses > 0)
5949
6448
  lines.push(paceP6Of(s.pauses));
5950
- lines.push(paceP7Of(s.settingsUrl));
6449
+ lines.push(PACE_P4, PACE_P7, LIMIT_L2);
6450
+ if (promptScriptBehind2)
6451
+ lines.push(PACE_P8);
5951
6452
  return lines;
5952
6453
  }
5953
- function paceS1Of(window, k, until, now) {
5954
- return `Halfcycle is slowing this build so you keep about a fifth of this ${PACE_WINDOW_WORDS[window]} for your own work. It will run at most ${paceAgentsOf(k)} at once until ${paceClockOf(until, now)}.`;
6454
+ function paceS1Of(window, k, until, now, keepFreePercent) {
6455
+ return `Halfcycle is slowing this build so you keep about ${paceShareWordsOf(keepFreePercent)} of this ${paceWindowWordsOf(window)} for your own work. It will run at most ${paceAgentsOf(k)} at once until ${paceClockOf(until, now)}.`;
5955
6456
  }
5956
- function paceS2Of(window, k, until, now) {
5957
- const head = `Halfcycle has paused new work in this build until ${paceClockOf(until, now)}, when your ${PACE_WINDOW_WORDS[window]} resets, so you keep about a fifth of it for your own work.`;
6457
+ function paceS2Of(window, k, until, now, keepFreePercent) {
6458
+ const head = `Halfcycle has paused new work in this build until ${paceClockOf(until, now)}, when your ${paceWindowWordsOf(window)} resets, so you keep about ${paceShareWordsOf(keepFreePercent)} of it for your own work.`;
5958
6459
  if (k === 0)
5959
6460
  return head;
5960
6461
  return k === 1 ? `${head} The 1 agent already working will finish its task.` : `${head} The ${String(k)} agents already working will finish their tasks.`;
@@ -5963,6 +6464,97 @@ function paceAllowedNowOf(k) {
5963
6464
  return k === 0 ? "Start no more agents now." : `Up to ${String(k)} more ${k === 1 ? "agent" : "agents"} can start now.`;
5964
6465
  }
5965
6466
  var PACE_AGENTS_TOO_MANY = `Run at most ${String(PACE_AGENTS_MAX)} agents at once.`;
6467
+ var LIMIT_HISTORY_MAX = 20;
6468
+ var LIMIT_PARTS = ["half", "three-quarters", "nine-tenths", "limit"];
6469
+ var limitPartSchema = z33.enum(LIMIT_PARTS);
6470
+ var LIMIT_SET_BY = ["default", "person", "other"];
6471
+ var limitSetBySchema = z33.enum(LIMIT_SET_BY);
6472
+ var LIMIT_TYPED_BY = ["person", "not-attended", "cannot-tell"];
6473
+ var limitTypedBySchema = z33.enum(LIMIT_TYPED_BY);
6474
+ var limitHistoryEntrySchema = z33.object({
6475
+ at: utcIso8601,
6476
+ kind: z33.enum(["raised", "set", "stopped"]),
6477
+ limit: z33.number().int().positive(),
6478
+ by: limitTypedBySchema,
6479
+ relayed: z33.literal(true).optional()
6480
+ }).strict();
6481
+ var PHASE_LIMIT_FILENAME = "limit.json";
6482
+ var phaseLimitFileSchema = z33.object({
6483
+ phase: phaseNameSchema,
6484
+ expected: z33.number().int().nonnegative(),
6485
+ expectedMinutes: z33.number().nonnegative().nullable(),
6486
+ limit: z33.number().int().positive(),
6487
+ multiple: z33.number().positive().nullable(),
6488
+ setBy: limitSetBySchema,
6489
+ firstGuess: z33.boolean(),
6490
+ shown: z33.array(limitPartSchema).max(LIMIT_PARTS.length),
6491
+ awaiting: z33.boolean(),
6492
+ history: z33.array(limitHistoryEntrySchema).max(LIMIT_HISTORY_MAX)
6493
+ }).strict();
6494
+ function phaseLimitFileOf(raw, phase) {
6495
+ const parsed = phaseLimitFileSchema.safeParse(raw);
6496
+ if (!parsed.success)
6497
+ return null;
6498
+ return phase === null || parsed.data.phase === phase ? parsed.data : null;
6499
+ }
6500
+ var phaseUsageFileSchema = z33.object({
6501
+ phase: phaseNameSchema,
6502
+ openedAt: utcIso8601,
6503
+ tokens: z33.number().int().nonnegative(),
6504
+ at: utcIso8601
6505
+ }).strict();
6506
+ function multipleTextOf(multiple) {
6507
+ return String(Math.round(multiple * 100) / 100);
6508
+ }
6509
+ function multipleWordsOf(multiple) {
6510
+ return multiple === 2 ? "twice" : `${multipleTextOf(multiple)} times`;
6511
+ }
6512
+ function paceWhenOf(at) {
6513
+ const hours = at.getHours();
6514
+ const clock = `${String(hours % 12 === 0 ? 12 : hours % 12)}:${String(at.getMinutes()).padStart(2, "0")} ${hours < 12 ? "am" : "pm"}`;
6515
+ return `${String(at.getDate())} ${MONTH_NAMES[at.getMonth()] ?? ""}, ${clock}`;
6516
+ }
6517
+ function limitL1Of(expected, limit, multiple, setBy) {
6518
+ const head = `This phase is expected to use about ${tokenCountWordsOf(expected)} tokens.`;
6519
+ const l = tokenCountWordsOf(limit);
6520
+ if (multiple !== null && setBy !== "other") {
6521
+ return `${head} Halfcycle will stop new work and ask you at ${multipleWordsOf(multiple)} that, ${l} tokens.`;
6522
+ }
6523
+ if (setBy === "person")
6524
+ return `${head} You set the limit at ${l} tokens; Halfcycle will stop new work and ask you there.`;
6525
+ return `${head} The limit is ${l} tokens; Halfcycle will stop new work and ask there.`;
6526
+ }
6527
+ var LIMIT_L1B = "That is a first guess: this project has not finished enough work for Halfcycle to go on.";
6528
+ var LIMIT_L1C = "Halfcycle cannot say how much the work will use, so it has set no usage limit yet.";
6529
+ var LIMIT_L2 = 'To change the limit, say "limit it to 1.5 times" or "limit it to 300 million".';
6530
+ function limitReachedLineOf(used, limit, working, raiseBy) {
6531
+ const head = `This phase has reached its usage limit: ${tokenCountWordsOf(used)} of ${tokenCountWordsOf(limit)} tokens. No new work will start.`;
6532
+ const finish = working === 0 ? "" : working === 1 ? " The 1 agent already working will finish its task." : ` The ${String(working)} agents already working will finish their tasks.`;
6533
+ return `${head}${finish} Say "keep going" to raise the limit by about ${tokenCountWordsOf(raiseBy)} tokens and carry on, or "stop" to leave the phase paused here. Nothing is lost either way.`;
6534
+ }
6535
+ var LIMIT_C1_NONE = "No usage limit was set: this phase had no build plan.";
6536
+ function limitCloseLineOf(file) {
6537
+ const l = tokenCountWordsOf(file.limit);
6538
+ const t = tokenCountWordsOf(file.expected);
6539
+ if (file.setBy === "default")
6540
+ return `Usage limit: ${l} tokens, from an expected ${t}.`;
6541
+ if (file.setBy === "person")
6542
+ return `Usage limit: ${l} tokens, set by you. Expected: ${t}.`;
6543
+ return `Usage limit: ${l} tokens. Expected: ${t}.`;
6544
+ }
6545
+ function limitHistoryLineOf(entry) {
6546
+ if (entry.relayed === true)
6547
+ return null;
6548
+ const when = paceWhenOf(new Date(entry.at));
6549
+ const you = entry.by === "person";
6550
+ if (entry.kind === "stopped") {
6551
+ return you ? `On ${when} you stopped the phase at its limit. Nothing was lost.` : `On ${when} the phase was stopped at its limit. Nothing was lost.`;
6552
+ }
6553
+ const l = tokenCountWordsOf(entry.limit);
6554
+ if (!you)
6555
+ return `On ${when} the limit was set to ${l} tokens.`;
6556
+ return entry.kind === "raised" ? `On ${when} you said "keep going": the limit went up to ${l} tokens.` : `On ${when} you set the limit to ${l} tokens.`;
6557
+ }
5966
6558
 
5967
6559
  // ../events/dist/autonomy.js
5968
6560
  import { z as z34 } from "zod";
@@ -6186,6 +6778,107 @@ function recordedClaimLine(claim) {
6186
6778
  }
6187
6779
  var NO_UNCHECKED_CLAIMS_LINE = "No agent said a check passes that Halfcycle never ran.";
6188
6780
 
6781
+ // ../events/dist/relayed-choices.js
6782
+ import { z as z35 } from "zod";
6783
+ var RELAYED_CHOICES_FILENAME = "relayed.json";
6784
+ var RELAYED_SAID_MAX = 80;
6785
+ var relayedChoiceSchema = z35.object({
6786
+ at: utcIso8601,
6787
+ said: z35.string().min(1).max(RELAYED_SAID_MAX)
6788
+ }).strict();
6789
+ var relayedChoicesFileSchema = z35.object({
6790
+ phase: phaseNameSchema,
6791
+ entries: z35.array(relayedChoiceSchema)
6792
+ }).strict();
6793
+ function relayedChoiceLineOf(entry) {
6794
+ return `On ${paceWhenOf(new Date(entry.at))} "${entry.said}" reached Halfcycle through Codex, which cannot show who typed it. Halfcycle could not confirm that you typed it, and it let this phase use more of your allowance.`;
6795
+ }
6796
+
6797
+ // ../events/dist/tier-figures.js
6798
+ import { z as z36 } from "zod";
6799
+ var TIER_ORDER = ["fast", "standard", "deep"];
6800
+ function tierRankOf(tier) {
6801
+ return TIER_ORDER.indexOf(tier);
6802
+ }
6803
+ function tierAboveOf(tier) {
6804
+ return TIER_ORDER[Math.min(tierRankOf(tier) + 1, TIER_ORDER.length - 1)];
6805
+ }
6806
+ function hasCheaperRole(member) {
6807
+ return member.role === "builder" && member.tier === "standard";
6808
+ }
6809
+ function cheaperRoleNameOf(callsign) {
6810
+ return `${callsign.toLowerCase()}fast`;
6811
+ }
6812
+ function routeCheaperClaudeLineOf(holder, task, model) {
6813
+ return `Start ${holder} on ${task} on the cheaper tier: give the Agent tool the model "${model}". Tell the reviewer the builder tier is fast.`;
6814
+ }
6815
+ function routeCheaperCodexLineOf(holder, task) {
6816
+ return `Start ${holder} on ${task} on the cheaper tier: use the agent type "${cheaperRoleNameOf(holder)}". Tell the reviewer the builder tier is fast.`;
6817
+ }
6818
+ function routeStartLineOf(holder, task, tier) {
6819
+ return `Start ${holder} on ${task}. Tell the reviewer the builder tier is ${tier}.`;
6820
+ }
6821
+ function routeStartUnknownLineOf(holder, task) {
6822
+ return `Start ${holder} on ${task}.`;
6823
+ }
6824
+ function routeNoSuchTaskLineOf(task) {
6825
+ return `"${task}" is not a task in the plan. Check the task id, then run this again.`;
6826
+ }
6827
+ var routingRecordSchema = z36.object({
6828
+ phase: z36.string().min(1).max(200),
6829
+ cheapStarted: z36.array(z36.string().min(1).max(200)).max(2e3)
6830
+ }).strict();
6831
+ var PHASE_MODEL_NAME_MAX = 64;
6832
+ var phaseModelsFileSchema = z36.object({
6833
+ phase: z36.string().min(1).max(200),
6834
+ openedAt: z36.string().min(1).max(64),
6835
+ tasks: z36.record(z36.string().min(1).max(200), z36.record(z36.string().min(1).max(PHASE_MODEL_NAME_MAX), z36.number().int().min(0)))
6836
+ }).strict();
6837
+ function inAttemptOrder(attempts) {
6838
+ return [...attempts].sort((a, b) => a.attempt - b.attempt);
6839
+ }
6840
+ function tierFiguresOf(work) {
6841
+ const finishedCheaper = [];
6842
+ const movedUp = [];
6843
+ const hadCheaperAttempt = [];
6844
+ for (const task of work.tasks) {
6845
+ const attempts = inAttemptOrder(task.attempts);
6846
+ if (attempts.length === 0)
6847
+ continue;
6848
+ if (task.standing === "passed" && attempts.every((a) => a.builderTier === "fast"))
6849
+ finishedCheaper.push(task.task);
6850
+ if (attempts.length >= 2 && tierRankOf(attempts[attempts.length - 1].builderTier) > tierRankOf(attempts[0].builderTier)) {
6851
+ movedUp.push(task.task);
6852
+ }
6853
+ if (attempts.some((a) => a.builderTier === "fast"))
6854
+ hadCheaperAttempt.push(task.task);
6855
+ }
6856
+ return { finishedCheaper, movedUp, hadCheaperAttempt };
6857
+ }
6858
+ function tasksWordOf(n) {
6859
+ return n === 1 ? "1 task" : `${String(n)} tasks`;
6860
+ }
6861
+ function tierCountsLineOf(figures) {
6862
+ if (figures.hadCheaperAttempt.length === 0)
6863
+ return "No task started on the cheaper tier in this phase.";
6864
+ return `The reviews recorded ${tasksWordOf(figures.finishedCheaper.length)} finishing on the cheaper tier and ${String(figures.movedUp.length)} moving up to a stronger one.`;
6865
+ }
6866
+ var NO_EXPECTED_LINE = "No expected time or usage was recorded for this phase.";
6867
+ function expectedActualLineOf(expected, actual) {
6868
+ const took = `The phase took ${timeDurationLabelOf(actual.seconds)} and ${tokenCountWordsOf(actual.tokens)} tokens.`;
6869
+ if (expected.minutes === null)
6870
+ return `Expected about ${tokenCountWordsOf(expected.tokens)} tokens. ${took}`;
6871
+ return `Expected ${timeDurationLabelOf(expected.minutes * 60)} and about ${tokenCountWordsOf(expected.tokens)} tokens. ${took}`;
6872
+ }
6873
+ function joinedOf(names) {
6874
+ if (names.length <= 1)
6875
+ return names.join("");
6876
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
6877
+ }
6878
+ function seenModelsLineOf(task, models) {
6879
+ return `Seen in your coding agent's own records: ${task} ran on ${joinedOf(models)}.`;
6880
+ }
6881
+
6189
6882
  // dist/engagement-credential.js
6190
6883
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
6191
6884
  import { platform as platform2 } from "node:os";
@@ -7864,8 +8557,9 @@ function crewProfileBody(role, group, tier) {
7864
8557
  return `${lines.join("\n")}
7865
8558
  `;
7866
8559
  }
7867
- function crewProfileDescription(member) {
7868
- return `${member.callsign}, ${CREW_ROLE_WORDS[member.role]} (${member.discipline}), a member of this project's build crew.`;
8560
+ function crewProfileDescription(member, cheaper = false) {
8561
+ const attempt = cheaper ? " (cheaper attempt)" : "";
8562
+ return `${member.callsign}, ${CREW_ROLE_WORDS[member.role]} (${member.discipline})${attempt}, a member of this project's build crew.`;
7869
8563
  }
7870
8564
  function plainYamlValue(value) {
7871
8565
  if (/[\r\n]|: | #|^[-?:,[\]{}#&*!|>'"%@`\s]|\s$/.test(value)) {
@@ -7886,17 +8580,19 @@ function claudeCrewProfile(member) {
7886
8580
  crewProfileBody(member.role, member.group, member.tier)
7887
8581
  ].join("\n");
7888
8582
  }
7889
- function codexCrewProfile(member) {
7890
- const { model, effort } = AGENT_PROFILES.codex.tierModels[member.tier];
8583
+ function codexCrewProfile(member, cheaper = false) {
8584
+ const tier = cheaper ? "fast" : member.tier;
8585
+ const { model, effort } = AGENT_PROFILES.codex.tierModels[tier];
7891
8586
  if (effort === void 0)
7892
- throw new Error(`[halfcycle] Codex's ${member.tier} tier names no reasoning effort.`);
7893
- const body = crewProfileBody(member.role, member.group, member.tier);
8587
+ throw new Error(`[halfcycle] Codex's ${tier} tier names no reasoning effort.`);
8588
+ const body = crewProfileBody(member.role, member.group, tier);
7894
8589
  if (/[\\"]|[\u0000-\u0009\u000b-\u001f\u007f]/.test(body)) {
7895
8590
  throw new Error("[halfcycle] a crew profile body holds a character a Codex role cannot carry as written.");
7896
8591
  }
8592
+ const name = cheaper ? cheaperRoleNameOf(member.callsign) : member.callsign.toLowerCase();
7897
8593
  return [
7898
- `name = ${tomlBasicString(member.callsign.toLowerCase())}`,
7899
- `description = ${tomlBasicString(crewProfileDescription(member))}`,
8594
+ `name = ${tomlBasicString(name)}`,
8595
+ `description = ${tomlBasicString(crewProfileDescription(member, cheaper))}`,
7900
8596
  `model = ${tomlBasicString(model)}`,
7901
8597
  `model_reasoning_effort = ${tomlBasicString(effort)}`,
7902
8598
  `developer_instructions = """`,
@@ -7907,7 +8603,10 @@ function codexCrewProfile(member) {
7907
8603
  function renderCrewProfiles(roster = CREW_ROSTER) {
7908
8604
  return [
7909
8605
  ...roster.map((member) => ({ rel: crewProfileRel("claude-code", member.callsign), text: claudeCrewProfile(member) })),
7910
- ...roster.filter((member) => member.group !== "reviewer").map((member) => ({ rel: crewProfileRel("codex", member.callsign), text: codexCrewProfile(member) }))
8606
+ ...roster.filter((member) => member.group !== "reviewer").flatMap((member) => [
8607
+ { rel: crewProfileRel("codex", member.callsign), text: codexCrewProfile(member) },
8608
+ ...hasCheaperRole(member) ? [{ rel: crewProfileRel("codex", `${member.callsign}FAST`), text: codexCrewProfile(member, true) }] : []
8609
+ ])
7911
8610
  ];
7912
8611
  }
7913
8612
  var __filename = fileURLToPath(import.meta.url);
@@ -8779,14 +9478,19 @@ ${GENERATED_USER_PROMPT_SCOPE_HEADER} the Halfcycle installer, now also for the
8779
9478
  # <phase>/<part>", "question <phase>/<part>: <words>", "carry <phase>/<part>:
8780
9479
  # <words>", "drop <phase>/<part>: <words>", "reject <phase>/<task>: <words>",
8781
9480
  # "autonomy on" or "autonomy off". Also lets the person who is typing let one
8782
- # step Halfcycle paused run once, by typing "allow" as the whole prompt.
9481
+ # step Halfcycle paused run once, by typing "allow" as the whole prompt, and change
9482
+ # how a build is paced, by typing "faster", "run 5 agents", "keep 30% free" or
9483
+ # "use up to 70%" as the whole prompt, and answer a phase's usage limit by
9484
+ # typing "keep going", "stop", "limit it to 1.5 times" or "limit it to 300
9485
+ # million" as the whole prompt.
8783
9486
  #
8784
9487
  # AN ORDINARY PROMPT STARTS NOTHING. This script reads the hook input once and
8785
9488
  # looks only at the start of the prompt's value. Unless the prompt begins, after
8786
9489
  # any whitespace (typed, or escaped in the input), with "approve ", "send back ",
8787
9490
  # "accept ", "question ", "carry ", "drop ", "reject " or "autonomy ", in any case,
8788
- # or is "allow" and nothing else but whitespace, it exits 0 with no output, and
8789
- # Node never starts. A quote inside any other field arrives
9491
+ # or is "allow", one of the five pacing phrases or one of the usage-limit phrases
9492
+ # and nothing else but whitespace,
9493
+ # it exits 0 with no output, and Node never starts. A quote inside any other field arrives
8790
9494
  # escaped, so no other field (the folder the session is in, for one) can read as
8791
9495
  # the start of the prompt.
8792
9496
  #
@@ -8812,7 +9516,7 @@ IFS= read -r -d '' HC_INPUT || true
8812
9516
  # \\u escape, or as the UTF-8 bytes of a Unicode space. Compared byte for byte.
8813
9517
  HC_LEAD='[[:space:]]|\\\\[nrtf]|\\\\u(000[9a-d]|0020|00a0|1680|200[0-9a]|202[89f]|205f|3000|feff)'
8814
9518
  HC_LEAD="$HC_LEAD|"$'\\xc2\\xa0|\\xe1\\x9a\\x80|\\xe2\\x80[\\x80-\\x8a\\xa8\\xa9\\xaf]|\\xe2\\x81\\x9f|\\xe3\\x80\\x80|\\xef\\xbb\\xbf'
8815
- if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy) |allow($HC_LEAD)*\\")"; then
9519
+ if ! printf '%s' "$HC_INPUT" | LC_ALL=C grep -Eiq "\\"prompt\\"[[:space:]]*:[[:space:]]*\\"($HC_LEAD)*((approve|send back|accept|question|carry|drop|reject|autonomy|decline) |allow($HC_LEAD)*\\"|(faster|go faster|run [0-9]{1,3} agents?|keep [0-9]{1,3}% free|use up to [0-9]{1,3}%|keep going|stop|stop here|limit it to [0-9]{1,9}(\\.[0-9]{1,2})? (times|million|billion|million tokens|billion tokens))($HC_LEAD)*\\")"; then
8816
9520
  exit 0
8817
9521
  fi
8818
9522
 
@@ -10236,14 +10940,14 @@ async function bindLoopback(env = process.env, engagementId) {
10236
10940
  else
10237
10941
  pending = callback;
10238
10942
  }, engagementId));
10239
- const bindProblem = await new Promise((resolve8) => {
10943
+ const bindProblem = await new Promise((resolve9) => {
10240
10944
  const onError = (err) => {
10241
- resolve8(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
10945
+ resolve9(`${err.code ?? "bind failed"} on ${LOOPBACK_REDIRECT_HOST}:${wanted.port}`);
10242
10946
  };
10243
10947
  server.once("error", onError);
10244
10948
  server.listen({ host: LOOPBACK_REDIRECT_HOST, port: wanted.port }, () => {
10245
10949
  server.removeListener("error", onError);
10246
- resolve8(null);
10950
+ resolve9(null);
10247
10951
  });
10248
10952
  });
10249
10953
  if (bindProblem !== null) {
@@ -10259,9 +10963,9 @@ async function bindLoopback(env = process.env, engagementId) {
10259
10963
  bound: true,
10260
10964
  target: { port: address.port, state },
10261
10965
  boundAddress: address.address,
10262
- waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve8) => {
10966
+ waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve9) => {
10263
10967
  if (pending !== null) {
10264
- resolve8(pending);
10968
+ resolve9(pending);
10265
10969
  return;
10266
10970
  }
10267
10971
  const startedAt = Date.now();
@@ -10272,7 +10976,7 @@ async function bindLoopback(env = process.env, engagementId) {
10272
10976
  clearTimeout(timer);
10273
10977
  deliver = null;
10274
10978
  abandon = null;
10275
- resolve8(value);
10979
+ resolve9(value);
10276
10980
  };
10277
10981
  const timer = setTimeout(() => finish(null), Math.max(0, timeoutMs));
10278
10982
  deliver = finish;
@@ -10286,9 +10990,9 @@ async function bindLoopback(env = process.env, engagementId) {
10286
10990
  };
10287
10991
  }
10288
10992
  function closeServer(server) {
10289
- return new Promise((resolve8) => {
10993
+ return new Promise((resolve9) => {
10290
10994
  server.closeAllConnections();
10291
- server.close(() => resolve8());
10995
+ server.close(() => resolve9());
10292
10996
  });
10293
10997
  }
10294
10998
 
@@ -10311,7 +11015,7 @@ function defaultWrite(text) {
10311
11015
  process.stdout.write(text);
10312
11016
  }
10313
11017
  function defaultSleep(ms) {
10314
- return new Promise((resolve8) => setTimeout(resolve8, ms));
11018
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
10315
11019
  }
10316
11020
  function browserOpenCommand(os, url) {
10317
11021
  if (os === "darwin")
@@ -10358,12 +11062,12 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
10358
11062
  };
10359
11063
  }
10360
11064
  const { command, args: args2, windowsVerbatimArguments } = browserOpenCommand(os, url);
10361
- return new Promise((resolve8) => {
11065
+ return new Promise((resolve9) => {
10362
11066
  let child;
10363
11067
  try {
10364
11068
  child = spawnImpl(command, args2, { stdio: "ignore", env, windowsVerbatimArguments });
10365
11069
  } catch {
10366
- resolve8({ opened: false, reason: `${command} could not be run` });
11070
+ resolve9({ opened: false, reason: `${command} could not be run` });
10367
11071
  return;
10368
11072
  }
10369
11073
  let settled = false;
@@ -10372,7 +11076,7 @@ async function openBrowser(url, env = process.env, os = platform3(), spawnImpl =
10372
11076
  return;
10373
11077
  settled = true;
10374
11078
  clearTimeout(timer);
10375
- resolve8(attempt);
11079
+ resolve9(attempt);
10376
11080
  };
10377
11081
  const timer = setTimeout(() => {
10378
11082
  child.unref();
@@ -10415,8 +11119,8 @@ async function startDeviceAuth(serviceUrl) {
10415
11119
  }
10416
11120
  const body = await res.json().catch(() => null);
10417
11121
  if (res.status !== 201) {
10418
- const refusal2 = tryRefusal(body);
10419
- throw new SignInRefused(refusal2?.status ?? String(res.status), refusal2?.message ?? `Sign-in could not be started: ${url} answered ${res.status}.`);
11122
+ const refusal3 = tryRefusal(body);
11123
+ throw new SignInRefused(refusal3?.status ?? String(res.status), refusal3?.message ?? `Sign-in could not be started: ${url} answered ${res.status}.`);
10420
11124
  }
10421
11125
  const parsed = deviceAuthStartResponseSchema.safeParse(body);
10422
11126
  if (!parsed.success) {
@@ -10517,17 +11221,17 @@ async function pollBesideLoopback(serviceUrl, started, wake, deps) {
10517
11221
  continue;
10518
11222
  }
10519
11223
  if (sleep === void 0) {
10520
- await new Promise((resolve8) => {
10521
- const timer = setTimeout(resolve8, started.pollIntervalMs);
11224
+ await new Promise((resolve9) => {
11225
+ const timer = setTimeout(resolve9, started.pollIntervalMs);
10522
11226
  interrupt = () => {
10523
11227
  clearTimeout(timer);
10524
- resolve8();
11228
+ resolve9();
10525
11229
  };
10526
11230
  });
10527
11231
  } else {
10528
- await new Promise((resolve8) => {
10529
- interrupt = resolve8;
10530
- void sleep(started.pollIntervalMs).then(resolve8);
11232
+ await new Promise((resolve9) => {
11233
+ interrupt = resolve9;
11234
+ void sleep(started.pollIntervalMs).then(resolve9);
10531
11235
  });
10532
11236
  }
10533
11237
  interrupt = null;
@@ -10715,9 +11419,9 @@ async function requestEngagementValues(method, url, requestBody, credential, sha
10715
11419
  }
10716
11420
  if (!res.ok) {
10717
11421
  const detail = await res.text().catch(() => "");
10718
- const refusal2 = res.status === 404 ? void 0 : planeRefusal(detail);
10719
- if (refusal2) {
10720
- throw new CreateEngagementRefused(res.status, refusal2.message, refusal2.error, refusal2.reason, `[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${refusal2.message} ${shape.nothingHappened}.`);
11422
+ const refusal3 = res.status === 404 ? void 0 : planeRefusal(detail);
11423
+ if (refusal3) {
11424
+ throw new CreateEngagementRefused(res.status, refusal3.message, refusal3.error, refusal3.reason, `[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${refusal3.message} ${shape.nothingHappened}.`);
10721
11425
  }
10722
11426
  const wrongOrigin = res.status === 404 ? `That address answered, but it does not serve ${shape.route} \u2014 so it is not the Halfcycle service. ` : "";
10723
11427
  throw new Error(`[halfcycle] ${shape.action} failed: ${url} returned ${res.status}. ${wrongOrigin}${detail ? `Response: ${detail.slice(0, 300)}. ` : ""}${shape.nothingHappened}. ` + CONTROL_ORIGIN_HINT);
@@ -11149,9 +11853,9 @@ function wrongAccountVisibility(act) {
11149
11853
  async function askLine(question2) {
11150
11854
  const rl = createInterface({ input: process.stdin, output: process.stdout });
11151
11855
  try {
11152
- return await new Promise((resolve8) => {
11153
- rl.once("close", () => resolve8(""));
11154
- rl.question(question2).then(resolve8, () => resolve8(""));
11856
+ return await new Promise((resolve9) => {
11857
+ rl.once("close", () => resolve9(""));
11858
+ rl.question(question2).then(resolve9, () => resolve9(""));
11155
11859
  });
11156
11860
  } finally {
11157
11861
  rl.close();
@@ -11418,10 +12122,10 @@ var SYSTEM_FOLDERS = [
11418
12122
  "C:\\Program Files",
11419
12123
  "C:\\Program Files (x86)"
11420
12124
  ];
11421
- function refuseSystemFolder(targetRepo, platform6 = process.platform) {
12125
+ function refuseSystemFolder(targetRepo, platform5 = process.platform) {
11422
12126
  const key = (folder) => {
11423
12127
  const real = realOrLiteral(folder);
11424
- return platform6 === "win32" ? real.toLowerCase() : real;
12128
+ return platform5 === "win32" ? real.toLowerCase() : real;
11425
12129
  };
11426
12130
  const target = key(targetRepo);
11427
12131
  if (SYSTEM_FOLDERS.some((folder) => key(folder) === target)) {
@@ -11571,9 +12275,9 @@ var WORDS = {
11571
12275
  planFailed: "halfcycle: setting up this folder would stop on a file that could not be read. Nothing has been written; fix that file, then run the preview again."
11572
12276
  };
11573
12277
  function runPreview(targetRepo, options) {
11574
- const refusal2 = previewRefusal(targetRepo, options);
11575
- if (refusal2 !== void 0)
11576
- return { refusal: refusal2 };
12278
+ const refusal3 = previewRefusal(targetRepo, options);
12279
+ if (refusal3 !== void 0)
12280
+ return { refusal: refusal3 };
11577
12281
  let plan;
11578
12282
  try {
11579
12283
  plan = planInstall({ targetRepo, env: options.env, home: options.home });
@@ -11999,105 +12703,6 @@ function isObject(value) {
11999
12703
  return typeof value === "object" && value !== null && !Array.isArray(value);
12000
12704
  }
12001
12705
 
12002
- // dist/claude-plan.js
12003
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
12004
- import { platform as platform4 } from "node:os";
12005
- import { dirname as dirname5, join as join11 } from "node:path";
12006
- var CLAUDE_PLAN_FILENAME = "claude-plan.json";
12007
- var PLAN_QUESTION_TRIES = 3;
12008
- var SEND_TIMEOUT_MS = 5e3;
12009
- var SETTINGS_PAGE_SEGMENT = "settings";
12010
- function claudePlanPath(home) {
12011
- return join11(halfcycleHome(home), CLAUDE_PLAN_FILENAME);
12012
- }
12013
- function readRememberedClaudePlan(home) {
12014
- try {
12015
- const parsed = claudePlanRequestSchema.safeParse(JSON.parse(readFileSync11(claudePlanPath(home), "utf-8")));
12016
- return parsed.success ? parsed.data.claudePlan : void 0;
12017
- } catch {
12018
- return void 0;
12019
- }
12020
- }
12021
- function rememberClaudePlan(plan, home) {
12022
- const path = claudePlanPath(home);
12023
- try {
12024
- mkdirSync6(dirname5(path), { recursive: true, mode: 448 });
12025
- writeFileSync6(path, `${JSON.stringify(claudePlanRequestSchema.parse({ claudePlan: plan }))}
12026
- `, { mode: 384 });
12027
- if (platform4() !== "win32")
12028
- chmodSync3(path, 384);
12029
- return true;
12030
- } catch {
12031
- return false;
12032
- }
12033
- }
12034
- function settingsPageUrl(consoleUrl) {
12035
- if (consoleUrl === void 0 || consoleUrl.trim() === "")
12036
- return void 0;
12037
- return `${consoleUrl.replace(/\/+$/, "")}/${SETTINGS_PAGE_SEGMENT}`;
12038
- }
12039
- async function sendClaudePlan(serviceUrl, engagementId, credential, plan, fetchImpl = fetch) {
12040
- const url = `${serviceUrl.replace(/\/+$/, "")}/engagements/${encodeURIComponent(engagementId)}/claude-plan`;
12041
- const controller = new AbortController();
12042
- const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
12043
- try {
12044
- const res = await fetchImpl(url, {
12045
- method: "PUT",
12046
- headers: { authorization: `Bearer ${credential}`, "content-type": "application/json" },
12047
- body: JSON.stringify(claudePlanRequestSchema.parse({ claudePlan: plan })),
12048
- signal: controller.signal
12049
- });
12050
- return res.ok;
12051
- } catch {
12052
- return false;
12053
- } finally {
12054
- clearTimeout(timer);
12055
- }
12056
- }
12057
- function accountCredential(serviceUrl, env, home) {
12058
- const fromEnv = env["HALFCYCLE_TOKEN"]?.trim();
12059
- if (fromEnv)
12060
- return fromEnv;
12061
- return readStoredCredential(serviceUrl, home)?.credential;
12062
- }
12063
- async function setupClaudePlan(input, deps) {
12064
- try {
12065
- if (!input.ownsProject)
12066
- return { kind: "not-owner" };
12067
- if (!deps.interactive())
12068
- return { kind: "no-person" };
12069
- const env = deps.env ?? process.env;
12070
- const send2 = async (plan2) => {
12071
- const credential = accountCredential(input.serviceUrl, env, deps.home);
12072
- if (credential === void 0)
12073
- return false;
12074
- return sendClaudePlan(input.serviceUrl, input.engagementId, credential, plan2, deps.fetchImpl);
12075
- };
12076
- const remembered = readRememberedClaudePlan(deps.home);
12077
- if (remembered !== void 0) {
12078
- const sent = await send2(remembered);
12079
- const settingsUrl = settingsPageUrl(input.consoleUrl);
12080
- if (sent && settingsUrl !== void 0) {
12081
- deps.write(`[halfcycle] ${claudePlanRememberedText(remembered, settingsUrl)}
12082
- `);
12083
- }
12084
- return { kind: "remembered", plan: remembered, sent };
12085
- }
12086
- if (deps.assumeYes === true)
12087
- return { kind: "no-person" };
12088
- let plan = null;
12089
- for (let tries = 0; tries < PLAN_QUESTION_TRIES && plan === null; tries++) {
12090
- plan = claudePlanFromAnswer(await deps.ask(`${CLAUDE_PLAN_QUESTION} `));
12091
- }
12092
- if (plan === null)
12093
- return { kind: "no-answer" };
12094
- rememberClaudePlan(plan, deps.home);
12095
- return { kind: "answered", plan, sent: await send2(plan) };
12096
- } catch {
12097
- return { kind: "failed" };
12098
- }
12099
- }
12100
-
12101
12706
  // dist/probe-mcp.js
12102
12707
  var PROBE_TIMEOUT_MS = 5e3;
12103
12708
  async function probeMcpOrigin(mcpUrl, token) {
@@ -12153,12 +12758,12 @@ async function probeMcpOrigin(mcpUrl, token) {
12153
12758
  // dist/claude-version-probe.js
12154
12759
  import { execFileSync as execFileSync3 } from "node:child_process";
12155
12760
  import { existsSync as existsSync8, realpathSync as realpathSync4 } from "node:fs";
12156
- import { delimiter as delimiter2, join as join12 } from "node:path";
12761
+ import { delimiter as delimiter2, join as join11 } from "node:path";
12157
12762
  function execVersionCommand(command, timeoutMs, deps) {
12158
- const platform6 = deps.platform ?? process.platform;
12763
+ const platform5 = deps.platform ?? process.platform;
12159
12764
  const exec = deps.exec ?? execFileSync3;
12160
12765
  try {
12161
- return platform6 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
12766
+ return platform5 === "win32" ? exec("cmd.exe", ["/d", "/s", "/c", `"${command} --version"`], {
12162
12767
  encoding: "utf-8",
12163
12768
  timeout: timeoutMs,
12164
12769
  stdio: ["ignore", "pipe", "ignore"],
@@ -12195,7 +12800,7 @@ var INSTALL_METHOD_RULES = [
12195
12800
  ];
12196
12801
  function probeClaudeCodeInstallMethod(deps = {}) {
12197
12802
  const env = deps.env ?? process.env;
12198
- const platform6 = deps.platform ?? process.platform;
12803
+ const platform5 = deps.platform ?? process.platform;
12199
12804
  const exists = deps.exists ?? existsSync8;
12200
12805
  const realpath = deps.realpath ?? ((file) => {
12201
12806
  try {
@@ -12204,11 +12809,11 @@ function probeClaudeCodeInstallMethod(deps = {}) {
12204
12809
  return file;
12205
12810
  }
12206
12811
  });
12207
- const names = platform6 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
12812
+ const names = platform5 === "win32" ? ["claude.exe", "claude.cmd"] : ["claude"];
12208
12813
  const dirs = (env["PATH"] ?? "").split(delimiter2).filter((dir) => dir !== "");
12209
12814
  for (const dir of dirs) {
12210
12815
  for (const name of names) {
12211
- const file = join12(dir, name);
12816
+ const file = join11(dir, name);
12212
12817
  if (!exists(file))
12213
12818
  continue;
12214
12819
  const real = realpath(file);
@@ -12243,8 +12848,8 @@ function codexInstallLine(codex) {
12243
12848
  }
12244
12849
 
12245
12850
  // dist/build-record/write.js
12246
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "node:fs";
12247
- import { basename as basename2, join as join13, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
12851
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "node:fs";
12852
+ import { basename as basename2, join as join12, resolve as resolve3, relative as relative2, isAbsolute as isAbsolute3 } from "node:path";
12248
12853
 
12249
12854
  // dist/build-record/render-v2.js
12250
12855
  var HIDDEN = /[\p{Cc}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/u;
@@ -12830,7 +13435,7 @@ function renderBuildRecordV2Markdown(record2) {
12830
13435
  }
12831
13436
 
12832
13437
  // dist/build-record/pair-io.js
12833
- import { closeSync, openSync, readFileSync as readFileSync12, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
13438
+ import { closeSync, openSync, readFileSync as readFileSync11, renameSync as renameSync2, rmSync as rmSync3, writeSync } from "node:fs";
12834
13439
  function createExclusiveOnDisk(path, data) {
12835
13440
  const bytes = typeof data === "string" ? Buffer.from(data, "utf-8") : data;
12836
13441
  const fd = openSync(path, "wx");
@@ -12861,7 +13466,7 @@ function isEexist(err) {
12861
13466
  }
12862
13467
  function readIfPresent(path) {
12863
13468
  try {
12864
- return readFileSync12(path, "utf-8");
13469
+ return readFileSync11(path, "utf-8");
12865
13470
  } catch {
12866
13471
  return null;
12867
13472
  }
@@ -12893,7 +13498,7 @@ function replaceOne(io, tmp, finalPath, text) {
12893
13498
  }
12894
13499
 
12895
13500
  // dist/build-record/write.js
12896
- var BUILD_RECORD_DIR = join13("docs", "build-records");
13501
+ var BUILD_RECORD_DIR = join12("docs", "build-records");
12897
13502
  function assertInsideOutDir(outDir, candidate, identity) {
12898
13503
  const rel = relative2(outDir, resolve3(candidate));
12899
13504
  const isPlainFilename = rel !== "" && rel !== ".." && !isAbsolute3(rel) && rel === basename2(rel);
@@ -12931,8 +13536,8 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
12931
13536
  }
12932
13537
  const outDir = resolve3(repoRoot, BUILD_RECORD_DIR);
12933
13538
  const segment = renderPhaseSegment(phase);
12934
- const jsonPath = join13(outDir, `${segment}.json`);
12935
- const mdPath = join13(outDir, `${segment}.md`);
13539
+ const jsonPath = join12(outDir, `${segment}.json`);
13540
+ const mdPath = join12(outDir, `${segment}.md`);
12936
13541
  const jsonTmp = `${jsonPath}.tmp`;
12937
13542
  const mdTmp = `${mdPath}.tmp`;
12938
13543
  for (const candidate of [jsonPath, mdPath, jsonTmp, mdTmp]) {
@@ -12940,7 +13545,7 @@ function writeRecordPair(repoRoot, phase, rawRecord, record2, io = NODE_RECORD_P
12940
13545
  }
12941
13546
  const jsonText = serialiseServedRecord(rawRecord);
12942
13547
  const mdText = renderBuildRecordV2Markdown(record2);
12943
- mkdirSync7(outDir, { recursive: true });
13548
+ mkdirSync6(outDir, { recursive: true });
12944
13549
  if (createPairExclusive(io, jsonPath, jsonText, mdPath, mdText) === "created") {
12945
13550
  return { kind: "created", jsonPath, mdPath };
12946
13551
  }
@@ -12978,14 +13583,16 @@ import { join as join20 } from "node:path";
12978
13583
 
12979
13584
  // dist/close-phase.js
12980
13585
  import { execFile, execFileSync as execFileSync5 } from "node:child_process";
13586
+ import { readFileSync as readFileSync16 } from "node:fs";
13587
+ import { join as join19 } from "node:path";
12981
13588
 
12982
13589
  // dist/plan.js
12983
- import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
12984
- import { join as join19 } from "node:path";
13590
+ import { existsSync as existsSync11, readFileSync as readFileSync15 } from "node:fs";
13591
+ import { join as join18 } from "node:path";
12985
13592
 
12986
13593
  // dist/draw/write.js
12987
- import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync13, readSync, realpathSync as realpathSync5, rmSync as rmSync5 } from "node:fs";
12988
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join14, relative as relative3, resolve as resolve4, sep } from "node:path";
13594
+ import { closeSync as closeSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync12, readSync, realpathSync as realpathSync5, rmSync as rmSync5 } from "node:fs";
13595
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join13, relative as relative3, resolve as resolve4, sep } from "node:path";
12989
13596
  var DOCS_DIR = "docs";
12990
13597
  var DIAGRAMS_DIR = "diagrams";
12991
13598
  var DIAGRAM_DIR_REL = `${DOCS_DIR}/${DIAGRAMS_DIR}`;
@@ -13041,9 +13648,9 @@ function embedImageLink(base, doc) {
13041
13648
  }
13042
13649
  function diagramPaths(repoRoot, base) {
13043
13650
  const root = resolve4(repoRoot);
13044
- const dir = join14(root, DOCS_DIR, DIAGRAMS_DIR);
13651
+ const dir = join13(root, DOCS_DIR, DIAGRAMS_DIR);
13045
13652
  const names = fileNames(base);
13046
- const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join14(dir, names[which])]));
13653
+ const file = Object.fromEntries(DIAGRAM_FILES.map((which) => [which, join13(dir, names[which])]));
13047
13654
  return { repoRoot: root, dir, file };
13048
13655
  }
13049
13656
  function diagramRel(base, which) {
@@ -13078,7 +13685,7 @@ function checkDiagramPlace(paths) {
13078
13685
  }
13079
13686
  let ancestor = paths.dir;
13080
13687
  while (lstatOrNull2(ancestor) === null) {
13081
- const parent = dirname6(ancestor);
13688
+ const parent = dirname5(ancestor);
13082
13689
  if (parent === ancestor)
13083
13690
  break;
13084
13691
  ancestor = parent;
@@ -13138,7 +13745,7 @@ function readExistingSet(paths) {
13138
13745
  if (!stat.isFile())
13139
13746
  return { ok: false, inTheWay: which, temp: false };
13140
13747
  try {
13141
- found[which] = readFileSync13(paths.file[which]);
13748
+ found[which] = readFileSync12(paths.file[which]);
13142
13749
  } catch {
13143
13750
  return { ok: false, inTheWay: which, temp: false };
13144
13751
  }
@@ -13241,7 +13848,7 @@ function writeDiagramSet(repoRoot, base, set, io = NODE_RECORD_PAIR_IO) {
13241
13848
  if (changed.length === 0)
13242
13849
  return { kind: "up-to-date" };
13243
13850
  try {
13244
- mkdirSync8(paths.dir, { recursive: true });
13851
+ mkdirSync7(paths.dir, { recursive: true });
13245
13852
  } catch {
13246
13853
  return { kind: "failed", file: changed[0], removed: [] };
13247
13854
  }
@@ -13316,10 +13923,10 @@ function isMissingRoute(method, status, body) {
13316
13923
  }
13317
13924
 
13318
13925
  // dist/change.js
13319
- import { join as join17 } from "node:path";
13926
+ import { join as join16 } from "node:path";
13320
13927
 
13321
13928
  // dist/cli-contract.js
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"];
13929
+ var CLI_VERBS = ["install", "check-drift", "build-record", "open-phase", "close-phase", "system", "draw", "plan", "review", "check-tasks", "check-criteria", "check-report", "ready", "uninstall", "change", "pace", "route", "prove", "preview", "doctor", "restore"];
13323
13930
  var CONTRACTS = [
13324
13931
  { verb: "install", required: [], conditional: [] },
13325
13932
  { verb: "check-drift", required: [], conditional: [] },
@@ -13329,9 +13936,11 @@ var CONTRACTS = [
13329
13936
  { verb: "plan", required: [], conditional: [] },
13330
13937
  { verb: "review", required: [], conditional: [] },
13331
13938
  { verb: "check-tasks", required: [], conditional: [] },
13939
+ { verb: "check-criteria", required: [], conditional: [] },
13332
13940
  { verb: "ready", required: [], conditional: [] },
13333
13941
  { verb: "uninstall", required: [], conditional: [] },
13334
13942
  { verb: "pace", required: [], conditional: [] },
13943
+ { verb: "route", required: [], conditional: [] },
13335
13944
  { verb: "prove", required: [], conditional: [] },
13336
13945
  { verb: "preview", required: [], conditional: [] },
13337
13946
  { verb: "doctor", required: [], conditional: [] },
@@ -13526,12 +14135,12 @@ function placeholderRefusal(verb, tokens) {
13526
14135
 
13527
14136
  // dist/system-report.js
13528
14137
  import { execFileSync as execFileSync4 } from "node:child_process";
13529
- import { readFileSync as readFileSync14 } from "node:fs";
13530
- import { join as join15 } from "node:path";
14138
+ import { readFileSync as readFileSync13 } from "node:fs";
14139
+ import { join as join14 } from "node:path";
13531
14140
  function readDescriptionFile(repoRoot) {
13532
14141
  let text;
13533
14142
  try {
13534
- text = readFileSync14(join15(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
14143
+ text = readFileSync13(join14(repoRoot, SYSTEM_DESCRIPTION_REL), "utf-8");
13535
14144
  } catch (err) {
13536
14145
  const code = err.code;
13537
14146
  return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
@@ -13706,9 +14315,9 @@ async function checkAndReportDescription(repoRoot, deps, verb, beforeSend = () =
13706
14315
  }
13707
14316
  const unowned = unownedPatterns(checked.description, deps.listTracked(repoRoot));
13708
14317
  const warnings = unowned === null ? "" : unownedPatternLines(unowned, verb);
13709
- const refusal2 = beforeSend(checked.description);
13710
- if (refusal2 !== null)
13711
- return { kind: "stopped", stderr: warnings + refusal2 };
14318
+ const refusal3 = beforeSend(checked.description);
14319
+ if (refusal3 !== null)
14320
+ return { kind: "stopped", stderr: warnings + refusal3 };
13712
14321
  const credentialRead = readCredentialOrLine(repoRoot, deps, verb);
13713
14322
  if (!credentialRead.ok)
13714
14323
  return { kind: "stopped", stderr: warnings + credentialRead.line };
@@ -13755,8 +14364,8 @@ async function runSystem(repoRoot, deps) {
13755
14364
  }
13756
14365
 
13757
14366
  // dist/build-record/write-change.js
13758
- import { existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
13759
- import { basename as basename3, isAbsolute as isAbsolute5, join as join16, relative as relative4, resolve as resolve5 } from "node:path";
14367
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8 } from "node:fs";
14368
+ import { basename as basename3, isAbsolute as isAbsolute5, join as join15, relative as relative4, resolve as resolve5 } from "node:path";
13760
14369
 
13761
14370
  // dist/build-record/render-change.js
13762
14371
  function counted2(n, one, many) {
@@ -13901,7 +14510,7 @@ function renderChangeRecordMarkdown(record2) {
13901
14510
  }
13902
14511
 
13903
14512
  // dist/build-record/write-change.js
13904
- var CHANGE_RECORD_DIR = join16("docs", "build-records", "changes");
14513
+ var CHANGE_RECORD_DIR = join15("docs", "build-records", "changes");
13905
14514
  var InvalidChangeIdError = class extends Error {
13906
14515
  id;
13907
14516
  constructor(id) {
@@ -13921,15 +14530,15 @@ function writeChangeRecordPair(repoRoot, sortingId, rawRecord, record2, io = NOD
13921
14530
  if (!checked.success)
13922
14531
  throw new InvalidChangeIdError(sortingId);
13923
14532
  const outDir = resolve5(repoRoot, CHANGE_RECORD_DIR);
13924
- const jsonPath = join16(outDir, `${checked.data}.json`);
13925
- const mdPath = join16(outDir, `${checked.data}.md`);
14533
+ const jsonPath = join15(outDir, `${checked.data}.json`);
14534
+ const mdPath = join15(outDir, `${checked.data}.md`);
13926
14535
  const mdTmp = `${mdPath}.tmp`;
13927
14536
  for (const candidate of [jsonPath, mdPath, mdTmp]) {
13928
14537
  assertInsideChangeDir(outDir, candidate, sortingId);
13929
14538
  }
13930
14539
  const jsonText = serialiseServedRecord(rawRecord);
13931
14540
  const mdText = renderChangeRecordMarkdown(record2);
13932
- mkdirSync9(outDir, { recursive: true });
14541
+ mkdirSync8(outDir, { recursive: true });
13933
14542
  if (createPairExclusive(io, jsonPath, jsonText, mdPath, mdText) === "created") {
13934
14543
  return { kind: "created", jsonPath, mdPath };
13935
14544
  }
@@ -14173,9 +14782,9 @@ async function readChangeRecord(deps, credential, sortingId) {
14173
14782
  }
14174
14783
  if (isMissingRoute("GET", status, raw))
14175
14784
  return { kind: "no-route" };
14176
- const refusal2 = changeRefusalErrorSchema.safeParse(raw);
14177
- if (refusal2.success)
14178
- return { kind: "refused", line: refusal2.data.message };
14785
+ const refusal3 = changeRefusalErrorSchema.safeParse(raw);
14786
+ if (refusal3.success)
14787
+ return { kind: "refused", line: refusal3.data.message };
14179
14788
  if (status === 404) {
14180
14789
  const gone = wireErrorSchema.safeParse(raw);
14181
14790
  if (gone.success && gone.data.error === "EngagementNotFound") {
@@ -14198,7 +14807,7 @@ function writePairLines(deps, repoRoot, sortingId, found) {
14198
14807
  };
14199
14808
  }
14200
14809
  return {
14201
- stdout: `[halfcycle] Its record is in ${join17(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
14810
+ stdout: `[halfcycle] Its record is in ${join16(CHANGE_RECORD_DIR, `${sortingId}.md`)}.
14202
14811
  `,
14203
14812
  stderr: "",
14204
14813
  ok: true
@@ -14437,27 +15046,27 @@ function runChange(repoRoot, args2, deps) {
14437
15046
  }
14438
15047
 
14439
15048
  // dist/shown-record.js
14440
- import { chmodSync as chmodSync4, mkdirSync as mkdirSync10, readFileSync as readFileSync15, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
14441
- import { platform as platform5 } from "node:os";
14442
- import { join as join18 } from "node:path";
15049
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
15050
+ import { platform as platform4 } from "node:os";
15051
+ import { join as join17 } from "node:path";
14443
15052
  function updateShownRecord(engagementId, update, home) {
14444
15053
  let temp = null;
14445
15054
  try {
14446
15055
  const dir = engagementStateDir(engagementId, home);
14447
- const path = join18(dir, SHOWN_PLANS_FILENAME);
15056
+ const path = join17(dir, SHOWN_PLANS_FILENAME);
14448
15057
  let current;
14449
15058
  try {
14450
- current = parseShownRecord(readFileSync15(path, "utf-8"));
15059
+ current = parseShownRecord(readFileSync14(path, "utf-8"));
14451
15060
  } catch {
14452
15061
  current = { entries: [], reviews: [] };
14453
15062
  }
14454
15063
  const next = update(current);
14455
15064
  const body = serialiseShownRecord(next);
14456
- mkdirSync10(dir, { recursive: true, mode: 448 });
14457
- temp = join18(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
14458
- writeFileSync7(temp, body, { mode: 384 });
14459
- if (platform5() !== "win32")
14460
- chmodSync4(temp, 384);
15065
+ mkdirSync9(dir, { recursive: true, mode: 448 });
15066
+ temp = join17(dir, `.${SHOWN_PLANS_FILENAME}.${process.pid}.tmp`);
15067
+ writeFileSync6(temp, body, { mode: 384 });
15068
+ if (platform4() !== "win32")
15069
+ chmodSync3(temp, 384);
14461
15070
  renameSync3(temp, path);
14462
15071
  return { ok: true, record: next };
14463
15072
  } catch {
@@ -14531,7 +15140,7 @@ function readAndCheckPlan(repoRoot, phase, description, verb) {
14531
15140
  const fileRel = planFileRel(phase);
14532
15141
  let text;
14533
15142
  try {
14534
- text = readFileSync16(join19(repoRoot, fileRel), "utf-8");
15143
+ text = readFileSync15(join18(repoRoot, fileRel), "utf-8");
14535
15144
  } catch (err) {
14536
15145
  const code = err.code;
14537
15146
  if (code === "ENOENT" || code === "ENOTDIR") {
@@ -14687,7 +15296,7 @@ function removalOwedLines(verb, parts) {
14687
15296
  function redrawLine(repoRoot, phase) {
14688
15297
  let exists;
14689
15298
  try {
14690
- exists = existsSync11(join19(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
15299
+ exists = existsSync11(join18(repoRoot, DIAGRAM_DIR_REL, embedImageName(phaseDiagramBase(phase))));
14691
15300
  } catch {
14692
15301
  return null;
14693
15302
  }
@@ -15274,7 +15883,7 @@ function readObserveLine(stdout) {
15274
15883
  function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback) {
15275
15884
  if (binary instanceof Error)
15276
15885
  return Promise.resolve(fallback);
15277
- return new Promise((resolve8) => {
15886
+ return new Promise((resolve9) => {
15278
15887
  try {
15279
15888
  execFile(process.execPath, [binary, ...args2], {
15280
15889
  cwd: repoRoot,
@@ -15284,10 +15893,10 @@ function runRunnerChild(binary, args2, credential, repoRoot, readLine, fallback)
15284
15893
  windowsHide: true
15285
15894
  }, (err, stdout) => {
15286
15895
  const stopped = err !== null && err.killed === true;
15287
- resolve8(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
15896
+ resolve9(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
15288
15897
  });
15289
15898
  } catch {
15290
- resolve8(fallback);
15899
+ resolve9(fallback);
15291
15900
  }
15292
15901
  });
15293
15902
  }
@@ -15630,6 +16239,101 @@ function wentAheadCloseLines(calls) {
15630
16239
  function callsListedLine(n) {
15631
16240
  return `${String(n)} calls made without asking are listed in the review.`;
15632
16241
  }
16242
+ var PHASE_MODELS_FILENAME = "phase-models.json";
16243
+ var PACING_FOLDER = "pacing";
16244
+ function pacingFile(engagementId, name, home) {
16245
+ try {
16246
+ return join19(engagementStateDir(engagementId, home), PACING_FOLDER, name);
16247
+ } catch {
16248
+ return null;
16249
+ }
16250
+ }
16251
+ function readPhaseLimitFile(engagementId, phase, home) {
16252
+ const path = pacingFile(engagementId, PHASE_LIMIT_FILENAME, home);
16253
+ if (path === null)
16254
+ return null;
16255
+ let text;
16256
+ try {
16257
+ text = readFileSync16(path, "utf-8");
16258
+ } catch (err) {
16259
+ return err.code === "ENOENT" ? "missing" : null;
16260
+ }
16261
+ try {
16262
+ return phaseLimitFileOf(JSON.parse(text), phase);
16263
+ } catch {
16264
+ return null;
16265
+ }
16266
+ }
16267
+ function readPhaseExpected(engagementId, phase, home) {
16268
+ const file = readPhaseLimitFile(engagementId, phase, home);
16269
+ if (file === null || file === "missing")
16270
+ return null;
16271
+ return { tokens: file.expected, minutes: file.expectedMinutes };
16272
+ }
16273
+ function phaseLimitLines(engagementId, phase, home) {
16274
+ const file = readPhaseLimitFile(engagementId, phase, home);
16275
+ if (file === null)
16276
+ return [];
16277
+ if (file === "missing")
16278
+ return [LIMIT_C1_NONE];
16279
+ const lines = [limitCloseLineOf(file)];
16280
+ for (const entry of file.history) {
16281
+ const line = limitHistoryLineOf(entry);
16282
+ if (line !== null)
16283
+ lines.push(line);
16284
+ }
16285
+ return lines;
16286
+ }
16287
+ function relayedChoiceLines(engagementId, phase, home) {
16288
+ const path = pacingFile(engagementId, RELAYED_CHOICES_FILENAME, home);
16289
+ if (path === null)
16290
+ return [];
16291
+ try {
16292
+ const parsed = relayedChoicesFileSchema.safeParse(JSON.parse(readFileSync16(path, "utf-8")));
16293
+ if (!parsed.success || parsed.data.phase !== phase)
16294
+ return [];
16295
+ return parsed.data.entries.map((entry) => relayedChoiceLineOf(entry));
16296
+ } catch {
16297
+ return [];
16298
+ }
16299
+ }
16300
+ function readPhaseModels(engagementId, phase, openedAt, home) {
16301
+ const path = pacingFile(engagementId, PHASE_MODELS_FILENAME, home);
16302
+ if (path === null)
16303
+ return null;
16304
+ try {
16305
+ const parsed = phaseModelsFileSchema.safeParse(JSON.parse(readFileSync16(path, "utf-8")));
16306
+ if (!parsed.success || parsed.data.phase !== phase)
16307
+ return null;
16308
+ if (openedAt !== null && parsed.data.openedAt !== openedAt)
16309
+ return null;
16310
+ return parsed.data.tasks;
16311
+ } catch {
16312
+ return null;
16313
+ }
16314
+ }
16315
+ function usageTokensOf(usage2) {
16316
+ return usage2.inputTokens + usage2.outputTokens + usage2.cacheWriteTokens + usage2.cacheReadTokens;
16317
+ }
16318
+ function tierFigureLines(work, expected, summary, models) {
16319
+ const lines = [];
16320
+ const figures = work === null ? null : tierFiguresOf(work);
16321
+ if (figures !== null)
16322
+ lines.push(tierCountsLineOf(figures));
16323
+ if (expected === null)
16324
+ lines.push(NO_EXPECTED_LINE);
16325
+ else if (summary !== null) {
16326
+ lines.push(expectedActualLineOf(expected, { seconds: summary.elapsedSeconds, tokens: usageTokensOf(summary.usage) }));
16327
+ }
16328
+ if (figures !== null && models !== null) {
16329
+ for (const task of figures.hadCheaperAttempt) {
16330
+ const seen = Object.keys(models[task] ?? {});
16331
+ if (seen.length > 0)
16332
+ lines.push(seenModelsLineOf(task, seen));
16333
+ }
16334
+ }
16335
+ return lines;
16336
+ }
15633
16337
 
15634
16338
  // dist/open-phase.js
15635
16339
  function readMethodUpdate(value) {
@@ -16087,9 +16791,9 @@ async function runDraw(repoRoot, picture, deps) {
16087
16791
  const checked = readAndCheckPlan(repoRoot, phase, description, "draw");
16088
16792
  if (!checked.ok)
16089
16793
  return checked.line;
16090
- const refusal2 = localRefusal(repoRoot, base);
16091
- if (refusal2 !== null)
16092
- return refusal2;
16794
+ const refusal3 = localRefusal(repoRoot, base);
16795
+ if (refusal3 !== null)
16796
+ return refusal3;
16093
16797
  checkedPlan = checked.plan;
16094
16798
  return null;
16095
16799
  };
@@ -16191,7 +16895,7 @@ async function writeDrawOutcome(repoRoot, deps, outcome, target, warnings, canno
16191
16895
 
16192
16896
  // dist/check-tasks.js
16193
16897
  import { readFileSync as readFileSync18 } from "node:fs";
16194
- import { join as join21 } from "node:path";
16898
+ import { join as join21, resolve as resolve6 } from "node:path";
16195
16899
  var CHECK_TASKS_USAGE = `${HALFCYCLE_COMMAND} check-tasks <phase> [--with-task-list] [--repo <root>]`;
16196
16900
  function parseCheckTasksArgs(argv) {
16197
16901
  const positionals = [];
@@ -16222,13 +16926,16 @@ function parseCheckTasksArgs(argv) {
16222
16926
  return { ok: true, phase: positionals[0], withTaskList, repoRoot };
16223
16927
  }
16224
16928
  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.`;
16929
+ function planCrew() {
16930
+ return CREW_ROSTER.map((member) => ({ callsign: member.callsign, tier: member.tier }));
16931
+ }
16225
16932
  function issuePlace(rel, path) {
16226
16933
  return path.length === 0 ? rel : `${rel}.${path.map(String).join(".")}`;
16227
16934
  }
16228
16935
  function readJsonFile(repoRoot, rel, parse) {
16229
16936
  let text;
16230
16937
  try {
16231
- text = readFileSync18(join21(repoRoot, rel), "utf-8");
16938
+ text = readFileSync18(resolve6(repoRoot, rel), "utf-8");
16232
16939
  } catch (err) {
16233
16940
  const code = err.code;
16234
16941
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -16284,15 +16991,32 @@ function readPhasePlanParts(repoRoot, phase, description) {
16284
16991
  if (!isValidPhaseIdentity(phase))
16285
16992
  return void 0;
16286
16993
  const file = planFileRel(phase);
16994
+ let text;
16995
+ try {
16996
+ text = readFileSync18(join21(repoRoot, file), "utf-8");
16997
+ } catch (err) {
16998
+ const code = err.code;
16999
+ if (code === "ENOENT" || code === "ENOTDIR")
17000
+ return void 0;
17001
+ return { file, unusable: { kind: "unreadable" } };
17002
+ }
16287
17003
  let raw;
16288
17004
  try {
16289
- raw = JSON.parse(readFileSync18(join21(repoRoot, file), "utf-8"));
16290
- } catch {
16291
- return void 0;
17005
+ raw = JSON.parse(text);
17006
+ } catch (err) {
17007
+ return { file, unusable: { kind: "not-json", error: err instanceof Error ? err.message : String(err) } };
17008
+ }
17009
+ const named2 = typeof raw === "object" && raw !== null ? raw["phase"] : void 0;
17010
+ if (isValidPhaseIdentity(named2) && renderPhaseSegment(named2) !== renderPhaseSegment(phase)) {
17011
+ return { file, unusable: { kind: "other-phase", phase: String(named2) } };
16292
17012
  }
16293
17013
  const checked = checkPhasePlan(raw, description);
16294
- if (!checked.ok || renderPhaseSegment(checked.plan.phase) !== renderPhaseSegment(phase))
16295
- return void 0;
17014
+ if (!checked.ok) {
17015
+ return {
17016
+ file,
17017
+ unusable: { kind: "problems", problems: checked.problems.map((p) => ({ path: p.path, message: p.message })) }
17018
+ };
17019
+ }
16296
17020
  return { file, partIds: checked.plan.changes.map((change) => change.part) };
16297
17021
  }
16298
17022
  var SPEC_FOLDER = LAYER_ARTEFACT_PATHS.l4[0];
@@ -16315,6 +17039,24 @@ function readPlanSpecFiles(repoRoot, plan) {
16315
17039
  }
16316
17040
  return { kind: "ok", specFiles: { folder: SPEC_FOLDER, entries } };
16317
17041
  }
17042
+ function readCriteriaRecordFile(repoRoot, rel = CRITERIA_RECORD_FILE) {
17043
+ return readJsonFile(repoRoot, rel, (raw) => workCriteriaRecordSchema.safeParse(raw));
17044
+ }
17045
+ function readCriteriaRecord(repoRoot, plan) {
17046
+ const read = readCriteriaRecordFile(repoRoot);
17047
+ if (read.kind === "missing")
17048
+ return { kind: "ok", record: void 0 };
17049
+ if (read.kind === "bad")
17050
+ return { kind: "bad", line: read.line };
17051
+ const record2 = read.value;
17052
+ if (criteriaRecordMismatch(record2, plan) !== null)
17053
+ return { kind: "ok", record: void 0 };
17054
+ const fixTasks = [];
17055
+ for (const row of record2.rows)
17056
+ if (row.status === "fix")
17057
+ fixTasks.push(row.task);
17058
+ return { kind: "ok", record: { check: record2.check, fixTasks } };
17059
+ }
16318
17060
  function runCheckTasks(repoRoot, phase, withTaskList) {
16319
17061
  const p = "halfcycle check-tasks:";
16320
17062
  const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
@@ -16347,6 +17089,11 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
16347
17089
  const specs = readPlanSpecFiles(repoRoot, plan);
16348
17090
  if (specs.kind === "bad") {
16349
17091
  return { stdout: "", stderr: `${p} ${specs.line}
17092
+ `, exitCode: 2 };
17093
+ }
17094
+ const criteria = readCriteriaRecord(repoRoot, plan);
17095
+ if (criteria.kind === "bad") {
17096
+ return { stdout: "", stderr: `${p} ${criteria.line}
16350
17097
  `, exitCode: 2 };
16351
17098
  }
16352
17099
  const phasePlan = system.kind === "ok" ? readPhasePlanParts(repoRoot, phase, system.description) : void 0;
@@ -16354,14 +17101,67 @@ function runCheckTasks(repoRoot, phase, withTaskList) {
16354
17101
  phase,
16355
17102
  taskList,
16356
17103
  specFiles: specs.specFiles,
17104
+ ...criteria.record !== void 0 ? { criteriaRecord: criteria.record } : {},
16357
17105
  ...system.kind === "ok" ? { system: { file: SYSTEM_DESCRIPTION_REL, partIds: system.partIds } } : {},
16358
- ...phasePlan !== void 0 ? { phasePlan } : {}
17106
+ ...phasePlan !== void 0 ? { phasePlan } : {},
17107
+ crew: planCrew()
16359
17108
  });
16360
17109
  const stdout = result.lines.map((line) => `${line.text}
16361
17110
  `).join("");
16362
17111
  return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
16363
17112
  }
16364
17113
 
17114
+ // dist/check-criteria.js
17115
+ var CHECK_CRITERIA_USAGE = `${HALFCYCLE_COMMAND} check-criteria [<record file>] [--repo <root>]`;
17116
+ function parseCheckCriteriaArgs(argv) {
17117
+ const positionals = [];
17118
+ let repoRoot;
17119
+ for (let i = 0; i < argv.length; i++) {
17120
+ const arg = argv[i];
17121
+ if (arg === "--repo") {
17122
+ const value = argv[i + 1];
17123
+ if (repoRoot !== void 0 || value === void 0 || value === "" || value.startsWith("-"))
17124
+ return { ok: false };
17125
+ repoRoot = value;
17126
+ i++;
17127
+ continue;
17128
+ }
17129
+ if (arg.startsWith("-") || arg === "")
17130
+ return { ok: false };
17131
+ positionals.push(arg);
17132
+ }
17133
+ if (positionals.length > 1)
17134
+ return { ok: false };
17135
+ return { ok: true, file: positionals[0], repoRoot };
17136
+ }
17137
+ function missingRecordLine(file) {
17138
+ return `there is no ${file} in this project yet. Check the finished phase against each acceptance criterion of its features, write what you found there, then run this again.`;
17139
+ }
17140
+ function runCheckCriteria(repoRoot, file) {
17141
+ const p = "halfcycle check-criteria:";
17142
+ const fail = (line) => ({ stdout: "", stderr: `${p} ${line}
17143
+ `, exitCode: 2 });
17144
+ const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
17145
+ if (planRead.kind === "missing")
17146
+ return fail(MISSING_PLAN_LINE);
17147
+ if (planRead.kind === "bad")
17148
+ return fail(planRead.line);
17149
+ const plan = planRead.value;
17150
+ const recordFile = file ?? CRITERIA_RECORD_FILE;
17151
+ const recordRead = readCriteriaRecordFile(repoRoot, recordFile);
17152
+ if (recordRead.kind === "missing")
17153
+ return fail(missingRecordLine(recordFile));
17154
+ if (recordRead.kind === "bad")
17155
+ return fail(recordRead.line);
17156
+ const specs = readPlanSpecFiles(repoRoot, plan);
17157
+ if (specs.kind === "bad")
17158
+ return fail(specs.line);
17159
+ const result = checkCriteriaRecord(recordRead.value, plan, { file: recordFile, specFiles: specs.specFiles });
17160
+ const stdout = result.lines.map((line) => `${line.text}
17161
+ `).join("");
17162
+ return { stdout, stderr: "", exitCode: result.refused ? 1 : 0 };
17163
+ }
17164
+
16365
17165
  // dist/check-report.js
16366
17166
  import { readFileSync as readFileSync19 } from "node:fs";
16367
17167
  var CHECK_REPORT_USAGE = `${HALFCYCLE_COMMAND} check-report <file | -> --head <sha>`;
@@ -16386,8 +17186,17 @@ function parseCheckReportArgs(argv) {
16386
17186
  return { ok: false };
16387
17187
  return { ok: true, source: positionals[0], head };
16388
17188
  }
17189
+ var SHORT_COMMIT = /^[0-9a-fA-F]{1,39}$/;
16389
17190
  function runCheckReport(source, head, deps = {}) {
16390
17191
  const p = "halfcycle check-report:";
17192
+ if (SHORT_COMMIT.test(head)) {
17193
+ return {
17194
+ stdout: "",
17195
+ stderr: `${p} --head ${head} is a short commit id. Give the full 40-character commit id, then run this again.
17196
+ `,
17197
+ exitCode: 2
17198
+ };
17199
+ }
16391
17200
  let body;
16392
17201
  try {
16393
17202
  body = source === "-" ? (deps.readStdin ?? (() => readFileSync19(0, "utf-8")))() : readFileSync19(source, "utf-8");
@@ -16514,7 +17323,17 @@ function runReady(repoRoot, phase, cap) {
16514
17323
  if (specs.kind === "bad")
16515
17324
  return failed2(`${p} ${specs.line}
16516
17325
  `);
16517
- const check = checkTaskPlan(plan, { phase, taskList: { file: list2 }, specFiles: specs.specFiles });
17326
+ const criteria = readCriteriaRecord(repoRoot, plan);
17327
+ if (criteria.kind === "bad")
17328
+ return failed2(`${p} ${criteria.line}
17329
+ `);
17330
+ const check = checkTaskPlan(plan, {
17331
+ phase,
17332
+ taskList: { file: list2 },
17333
+ specFiles: specs.specFiles,
17334
+ ...criteria.record !== void 0 ? { criteriaRecord: criteria.record } : {},
17335
+ crew: planCrew()
17336
+ });
16518
17337
  if (check.refused) {
16519
17338
  return { stdout: check.lines.map((line) => `${line.text}
16520
17339
  `).join(""), stderr: "", exitCode: 1 };
@@ -16569,13 +17388,13 @@ function splitProveInvocation(words) {
16569
17388
  function runProveChild(binary, credential, repoRoot, words, deps = {}) {
16570
17389
  const out = deps.stdout ?? process.stdout;
16571
17390
  const err = deps.stderr ?? process.stderr;
16572
- return new Promise((resolve8) => {
17391
+ return new Promise((resolve9) => {
16573
17392
  let settled = false;
16574
17393
  const finish = (code) => {
16575
17394
  if (settled)
16576
17395
  return;
16577
17396
  settled = true;
16578
- resolve8(code);
17397
+ resolve9(code);
16579
17398
  };
16580
17399
  let child;
16581
17400
  try {
@@ -16597,9 +17416,9 @@ function runProveChild(binary, credential, repoRoot, words, deps = {}) {
16597
17416
  }
16598
17417
 
16599
17418
  // dist/uninstall.js
16600
- import { lstatSync as lstatSync3, readFileSync as readFileSync20, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync8 } from "node:fs";
17419
+ import { lstatSync as lstatSync3, readFileSync as readFileSync20, readdirSync, rmSync as rmSync6, rmdirSync, writeFileSync as writeFileSync7 } from "node:fs";
16601
17420
  import { homedir as homedir5 } from "node:os";
16602
- import { dirname as dirname7, join as join22, relative as relative6, sep as sep2 } from "node:path";
17421
+ import { dirname as dirname6, join as join22, relative as relative6, sep as sep2 } from "node:path";
16603
17422
  var REMOVE_CREDENTIAL_FLAG = "--remove-credential";
16604
17423
  function parseUninstallArgs(args2) {
16605
17424
  let removeCredential = false;
@@ -16700,7 +17519,7 @@ function validate(options, rules) {
16700
17519
  }
16701
17520
  if (engagementId !== void 0) {
16702
17521
  const expectedParent = join22(home ?? homedir5(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME);
16703
- if (dirname7(engagementStateDir(engagementId, home)) !== expectedParent) {
17522
+ if (dirname6(engagementStateDir(engagementId, home)) !== expectedParent) {
16704
17523
  throw new Refusal(`the project id in ${BUNDLE_PIN_REL} does not name a folder under ~/.halfcycle/engagements. Nothing was removed.`);
16705
17524
  }
16706
17525
  }
@@ -16731,7 +17550,7 @@ function writeBack(targetRepo, rel, current, next, lists, name) {
16731
17550
  rmSync6(path);
16732
17551
  name(lists.removed, rel, rel);
16733
17552
  } else {
16734
- writeFileSync8(path, next, "utf-8");
17553
+ writeFileSync7(path, next, "utf-8");
16735
17554
  name(lists.restored, rel, rel);
16736
17555
  }
16737
17556
  } catch {
@@ -17513,7 +18332,7 @@ function renderUninstallReport(result, home) {
17513
18332
 
17514
18333
  // dist/banner-facts.js
17515
18334
  import { readFileSync as readFileSync21 } from "node:fs";
17516
- import { basename as basename4, join as join23, resolve as resolve6 } from "node:path";
18335
+ import { basename as basename4, join as join23, resolve as resolve7 } from "node:path";
17517
18336
  var BRAND_URL = "halfcycle.ai";
17518
18337
  var TAGLINE = [
17519
18338
  "Fewer cycles.",
@@ -17612,7 +18431,7 @@ function openingBannerContent(projectDir, env = process.env, probes = REAL_PROBE
17612
18431
  }
17613
18432
  function safeBasename(dir) {
17614
18433
  try {
17615
- const name = basename4(resolve6(dir)).trim();
18434
+ const name = basename4(resolve7(dir)).trim();
17616
18435
  return name === "" ? "project" : name;
17617
18436
  } catch {
17618
18437
  return "project";
@@ -17621,7 +18440,9 @@ function safeBasename(dir) {
17621
18440
 
17622
18441
  // dist/pace.js
17623
18442
  import { spawn as spawn3 } from "node:child_process";
17624
- var PACE_USAGE = `${HALFCYCLE_COMMAND} pace [--start <phase> [--agents <n>]] [--repo <root>]`;
18443
+ import { readFileSync as readFileSync22 } from "node:fs";
18444
+ import { join as join24 } from "node:path";
18445
+ var PACE_USAGE = `${HALFCYCLE_COMMAND} pace [--agent claude-code|codex] [--start <phase> [--agents <n>]] [--repo <root>]`;
17625
18446
  function parsePaceArgs(argv) {
17626
18447
  let repoRoot;
17627
18448
  let phase;
@@ -17629,6 +18450,7 @@ function parsePaceArgs(argv) {
17629
18450
  let sawRepo = false;
17630
18451
  let sawStart = false;
17631
18452
  let sawAgents = false;
18453
+ let agent = null;
17632
18454
  for (let i = 0; i < argv.length; i++) {
17633
18455
  const arg = argv[i];
17634
18456
  const value = () => {
@@ -17654,6 +18476,13 @@ function parsePaceArgs(argv) {
17654
18476
  if (v === null)
17655
18477
  return { ok: false };
17656
18478
  phase = v;
18479
+ } else if (arg === "--agent") {
18480
+ if (agent !== null)
18481
+ return { ok: false };
18482
+ const v = value();
18483
+ if (v !== "claude-code" && v !== "codex")
18484
+ return { ok: false };
18485
+ agent = v;
17657
18486
  } else if (arg === "--agents") {
17658
18487
  if (sawAgents)
17659
18488
  return { ok: false };
@@ -17669,21 +18498,21 @@ function parsePaceArgs(argv) {
17669
18498
  if (agents !== void 0 && phase === void 0)
17670
18499
  return { ok: false };
17671
18500
  if (phase === void 0)
17672
- return { ok: true, start: null, repoRoot };
17673
- return { ok: true, start: { phase, agents: agents ?? null }, repoRoot };
18501
+ return { ok: true, start: null, repoRoot, agent };
18502
+ return { ok: true, start: { phase, agents: agents ?? null }, repoRoot, agent };
17674
18503
  }
17675
18504
  var PACE_CHILD_DEADLINE_MS = 11 * 60 * 1e3;
17676
18505
  function runPaceChild(binary, credential, repoRoot, args2, deps = {}) {
17677
18506
  if (binary instanceof Error)
17678
18507
  return Promise.resolve({ code: 1, stdout: "" });
17679
18508
  const err = deps.stderr ?? process.stderr;
17680
- return new Promise((resolve8) => {
18509
+ return new Promise((resolve9) => {
17681
18510
  let settled = false;
17682
18511
  const done = (output) => {
17683
18512
  if (settled)
17684
18513
  return;
17685
18514
  settled = true;
17686
- resolve8(output);
18515
+ resolve9(output);
17687
18516
  };
17688
18517
  try {
17689
18518
  const child = spawn3(process.execPath, [binary, "pace", ...args2], {
@@ -17724,7 +18553,17 @@ function notPacedLine(reason) {
17724
18553
  return reason === "not-configured" ? `${PACE_NOT_CONNECTED_LINE}
17725
18554
  ` : "halfcycle pace: Halfcycle could not read this computer's usage records just now. Run this again in a minute.\n";
17726
18555
  }
17727
- async function runPace(repoRoot, start, deps) {
18556
+ function readPromptScript(repoRoot) {
18557
+ try {
18558
+ return readFileSync22(join24(repoRoot, ".claude", "hooks", "user-prompt-scope.sh"), "utf-8");
18559
+ } catch {
18560
+ return null;
18561
+ }
18562
+ }
18563
+ function promptScriptBehind(script) {
18564
+ return script === null || !script.includes("faster");
18565
+ }
18566
+ async function runPace(repoRoot, start, deps, agent = null) {
17728
18567
  if (start !== null && !isValidPhaseIdentity(start.phase)) {
17729
18568
  return { stdout: "", stderr: notAPhaseNameLine("pace", start.phase), exitCode: 1 };
17730
18569
  }
@@ -17735,7 +18574,10 @@ async function runPace(repoRoot, start, deps) {
17735
18574
  return { stdout: "", stderr: `${PACE_NOT_CONNECTED_LINE}
17736
18575
  `, exitCode: 1 };
17737
18576
  }
17738
- const args2 = start === null ? [] : ["--start", start.phase, ...start.agents !== null ? ["--agents", start.agents] : []];
18577
+ const args2 = [
18578
+ ...agent === null ? [] : ["--agent", agent],
18579
+ ...start === null ? [] : ["--start", start.phase, ...start.agents !== null ? ["--agents", start.agents] : []]
18580
+ ];
17739
18581
  const child = await deps.spawnPace(credential, repoRoot, args2);
17740
18582
  if (child.code !== 0)
17741
18583
  return { stdout: "", stderr: "", exitCode: child.code };
@@ -17747,26 +18589,178 @@ async function runPace(repoRoot, start, deps) {
17747
18589
  if (start !== null) {
17748
18590
  if (result.suggestion === void 0)
17749
18591
  return { stdout: "", stderr: notPacedLine("unreadable"), exitCode: 0 };
17750
- return { stdout: `${paceStartLinesOf(result.suggestion).join("\n")}
18592
+ const behind = promptScriptBehind((deps.readPromptScript ?? readPromptScript)(repoRoot));
18593
+ return { stdout: `${paceStartLinesOf(result.suggestion, behind).join("\n")}
17751
18594
  `, stderr: "", exitCode: 0 };
17752
18595
  }
17753
18596
  const lines = [paceAllowedNowOf(result.allowedNow)];
18597
+ if (result.limit === "reached" && result.limitHeld !== void 0) {
18598
+ const held = result.limitHeld;
18599
+ lines.push(limitReachedLineOf(held.used, held.limit, held.working, held.raiseBy));
18600
+ }
17754
18601
  if (result.windowKind !== void 0 && result.until !== void 0) {
17755
18602
  const until = new Date(result.until);
18603
+ const share = result.keepFreePercent ?? KEEP_FREE_DEFAULT;
17756
18604
  if (result.pace === "slowing")
17757
- lines.push(paceS1Of(result.windowKind, result.allowedNow, until, deps.now()));
18605
+ lines.push(paceS1Of(result.windowKind, result.allowedNow, until, deps.now(), share));
17758
18606
  else if (result.pace === "holding")
17759
- lines.push(paceS2Of(result.windowKind, result.allowedNow, until, deps.now()));
18607
+ lines.push(paceS2Of(result.windowKind, result.allowedNow, until, deps.now(), share));
17760
18608
  }
17761
18609
  return { stdout: `${lines.join("\n")}
17762
18610
  `, stderr: "", exitCode: 0 };
17763
18611
  }
17764
18612
 
18613
+ // dist/route.js
18614
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync23, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "node:fs";
18615
+ import { dirname as dirname7, join as join25 } from "node:path";
18616
+ var ROUTE_USAGE = `${HALFCYCLE_COMMAND} route <phase> <task> [--agent claude-code|codex] [--repo <root>]`;
18617
+ var ROUTING_FILENAME = "routing.json";
18618
+ var ROUTE_WORK_READ_DEADLINE_MS = 5e3;
18619
+ function parseRouteArgs(argv) {
18620
+ const positionals = [];
18621
+ let agent = null;
18622
+ let repoRoot;
18623
+ let sawRepo = false;
18624
+ for (let i = 0; i < argv.length; i++) {
18625
+ const arg = argv[i];
18626
+ const value = () => {
18627
+ const v = argv[i + 1];
18628
+ if (v === void 0 || v === "" || v.startsWith("-"))
18629
+ return null;
18630
+ i += 1;
18631
+ return v;
18632
+ };
18633
+ if (arg === "--agent") {
18634
+ if (agent !== null)
18635
+ return { ok: false };
18636
+ const v = value();
18637
+ if (v !== "claude-code" && v !== "codex")
18638
+ return { ok: false };
18639
+ agent = v;
18640
+ } else if (arg === "--repo") {
18641
+ if (sawRepo)
18642
+ return { ok: false };
18643
+ sawRepo = true;
18644
+ const v = value();
18645
+ if (v === null)
18646
+ return { ok: false };
18647
+ repoRoot = v;
18648
+ } else if (arg.startsWith("-") || arg === "") {
18649
+ return { ok: false };
18650
+ } else {
18651
+ positionals.push(arg);
18652
+ }
18653
+ }
18654
+ if (positionals.length !== 2)
18655
+ return { ok: false };
18656
+ return { ok: true, phase: positionals[0], task: positionals[1], agent: agent ?? "claude-code", repoRoot };
18657
+ }
18658
+ function routingPath(engagementId, home) {
18659
+ return join25(engagementStateDir(engagementId, home), ROUTING_FILENAME);
18660
+ }
18661
+ function readCheapStarted(path, phase) {
18662
+ try {
18663
+ const parsed = routingRecordSchema.safeParse(JSON.parse(readFileSync23(path, "utf-8")));
18664
+ if (!parsed.success || parsed.data.phase !== phase)
18665
+ return [];
18666
+ return [...parsed.data.cheapStarted];
18667
+ } catch {
18668
+ return [];
18669
+ }
18670
+ }
18671
+ function recordCheapStarted(path, phase, task) {
18672
+ const started = readCheapStarted(path, phase);
18673
+ const record2 = { phase, cheapStarted: started.includes(task) ? started : [...started, task] };
18674
+ mkdirSync10(dirname7(path), { recursive: true, mode: 448 });
18675
+ const tmp = `${path}.${String(process.pid)}.tmp`;
18676
+ writeFileSync8(tmp, `${JSON.stringify(record2)}
18677
+ `, { mode: 384 });
18678
+ renameSync4(tmp, path);
18679
+ }
18680
+ function attemptsOf(read, task) {
18681
+ if (read.kind !== "work")
18682
+ return null;
18683
+ return read.work.tasks.find((t) => t.task === task)?.attempts ?? [];
18684
+ }
18685
+ function lastTierOf(attempts) {
18686
+ return [...attempts].sort((a, b) => b.attempt - a.attempt)[0].builderTier;
18687
+ }
18688
+ async function readWorkInTime(deps, credential, phase) {
18689
+ let timer;
18690
+ const late = new Promise((resolve9) => {
18691
+ timer = setTimeout(() => resolve9({ kind: "unreachable" }), ROUTE_WORK_READ_DEADLINE_MS);
18692
+ timer.unref?.();
18693
+ });
18694
+ try {
18695
+ return await Promise.race([deps.readWork(credential, phase).catch(() => ({ kind: "unreachable" })), late]);
18696
+ } finally {
18697
+ if (timer !== void 0)
18698
+ clearTimeout(timer);
18699
+ }
18700
+ }
18701
+ async function runRoute(repoRoot, phase, taskId, agent, deps) {
18702
+ const p = "halfcycle route:";
18703
+ const answer = (line) => ({ stdout: `${line}
18704
+ `, stderr: "", exitCode: 0 });
18705
+ const planRead = readJsonFile(repoRoot, TASK_PLAN_FILE, (raw) => taskPlanSchema.safeParse(raw));
18706
+ if (planRead.kind === "missing")
18707
+ return { stdout: "", stderr: `${p} ${MISSING_PLAN_LINE}
18708
+ `, exitCode: 2 };
18709
+ if (planRead.kind === "bad")
18710
+ return { stdout: "", stderr: `${p} ${planRead.line}
18711
+ `, exitCode: 2 };
18712
+ const plan = planRead.value;
18713
+ if (plan.phase !== phase) {
18714
+ return { stdout: "", stderr: `${p} ${taskPlanLineText.O6(plan.phase, phase)}
18715
+ `, exitCode: 1 };
18716
+ }
18717
+ const task = plan.tasks.find((t) => t.id === taskId);
18718
+ if (task === void 0)
18719
+ return { stdout: "", stderr: `${p} ${routeNoSuchTaskLineOf(taskId)}
18720
+ `, exitCode: 1 };
18721
+ const holder = task.agent;
18722
+ if (holder === void 0)
18723
+ return { stdout: "", stderr: `${p} ${taskPlanLineText.V4(taskId)}
18724
+ `, exitCode: 1 };
18725
+ const member = CREW_ROSTER.find((m) => m.callsign === holder);
18726
+ if (member === void 0)
18727
+ return answer(routeStartUnknownLineOf(holder, taskId));
18728
+ const startsCheaperWhenFirst = task.kind === "build" && task.cheapFirst === true && member.tier === "standard";
18729
+ if (!startsCheaperWhenFirst)
18730
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18731
+ let credential;
18732
+ try {
18733
+ credential = deps.readCredential(repoRoot);
18734
+ } catch {
18735
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18736
+ }
18737
+ const attempts = attemptsOf(await readWorkInTime(deps, credential, phase), taskId);
18738
+ if (attempts !== null && attempts.length > 0) {
18739
+ return answer(routeStartLineOf(holder, taskId, tierAboveOf(lastTierOf(attempts))));
18740
+ }
18741
+ if (agent === "codex" && !hasCheaperRole(member))
18742
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18743
+ let path;
18744
+ try {
18745
+ path = routingPath(credential.engagementId, deps.home);
18746
+ } catch {
18747
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18748
+ }
18749
+ if (readCheapStarted(path, phase).includes(taskId))
18750
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18751
+ try {
18752
+ recordCheapStarted(path, phase, taskId);
18753
+ } catch {
18754
+ return answer(routeStartLineOf(holder, taskId, member.tier));
18755
+ }
18756
+ return answer(agent === "codex" ? routeCheaperCodexLineOf(holder, taskId) : routeCheaperClaudeLineOf(holder, taskId, AGENT_PROFILES["claude-code"].tierModels.fast.model));
18757
+ }
18758
+
17765
18759
  // dist/restore.js
17766
18760
  import { spawnSync as spawnSync3 } from "node:child_process";
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";
18761
+ import { closeSync as closeSync3, fchmodSync, lstatSync as lstatSync4, mkdirSync as mkdirSync11, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync24, readlinkSync, realpathSync as realpathSync6, rmSync as rmSync7, symlinkSync, writeSync as writeSync2 } from "node:fs";
17768
18762
  import { homedir as homedir6 } from "node:os";
17769
- import { join as join24, resolve as resolve7 } from "node:path";
18763
+ import { join as join26, resolve as resolve8 } from "node:path";
17770
18764
  var RESTORE_USAGE = `${HALFCYCLE_COMMAND} restore [<id>]`;
17771
18765
  function parseRestoreArgs(rest) {
17772
18766
  if (rest.length > 1)
@@ -17788,7 +18782,7 @@ function runRestore(cwd, id, options = {}) {
17788
18782
  if (engagementId === null) {
17789
18783
  return refused2(`Halfcycle is not set up in this project, so it has saved nothing here. To set it up, run: ${HALFCYCLE_COMMAND}`);
17790
18784
  }
17791
- const folder = join24(options.home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
18785
+ const folder = join26(options.home ?? homedir6(), HALFCYCLE_DIR_NAME, ENGAGEMENTS_DIR_NAME, engagementId, SAVED_DIR_NAME);
17792
18786
  const refs = savedRefs(root);
17793
18787
  if (refs === null)
17794
18788
  return refused2("Halfcycle could not read the saved copies in this project, so nothing was put back.");
@@ -17864,7 +18858,7 @@ function restoreOne(root, folder, refs, rawId) {
17864
18858
  if (p.keptIn === "commit")
17865
18859
  fromCommit.push(p.path);
17866
18860
  else
17867
- restoreCopy(root, join24(folder, id), p.path, out);
18861
+ restoreCopy(root, join26(folder, id), p.path, out);
17868
18862
  }
17869
18863
  if (fromCommit.length > 0)
17870
18864
  restoreFromCommit(root, ref, fromCommit, out);
@@ -17892,7 +18886,7 @@ function makeFolders(root, rel) {
17892
18886
  const segments = rel.split("/");
17893
18887
  let at = root;
17894
18888
  for (let i = 0; i < segments.length - 1; i++) {
17895
- at = join24(at, segments[i]);
18889
+ at = join26(at, segments[i]);
17896
18890
  const shown2 = segments.slice(0, i + 1).join("/");
17897
18891
  let st = lstatOrNull3(at);
17898
18892
  if (st === null) {
@@ -17912,7 +18906,7 @@ function makeFolders(root, rel) {
17912
18906
  return null;
17913
18907
  }
17914
18908
  function place(root, rel, content, out) {
17915
- const abs = join24(root, rel);
18909
+ const abs = join26(root, rel);
17916
18910
  const there = lstatOrNull3(abs);
17917
18911
  if (there !== null) {
17918
18912
  if (sameAs(abs, there, content))
@@ -17950,7 +18944,7 @@ function sameAs(abs, st, content) {
17950
18944
  try {
17951
18945
  if ("link" in content)
17952
18946
  return st.isSymbolicLink() && readlinkSync(abs) === content.link;
17953
- return st.isFile() && readFileSync22(abs).equals(content.bytes);
18947
+ return st.isFile() && readFileSync24(abs).equals(content.bytes);
17954
18948
  } catch {
17955
18949
  return false;
17956
18950
  }
@@ -17960,20 +18954,20 @@ function restoreCopy(root, saveDir, rel, out) {
17960
18954
  let at = saveDir;
17961
18955
  const chain = [SAVED_COPIES_DIR_NAME, ...segments];
17962
18956
  for (let i = 0; i < chain.length - 1; i++) {
17963
- at = join24(at, chain[i]);
18957
+ at = join26(at, chain[i]);
17964
18958
  const st2 = lstatOrNull3(at);
17965
18959
  if (st2 === null || st2.isSymbolicLink() || !st2.isDirectory()) {
17966
18960
  out.left.push(`${rel}: the saved copy does not hold it`);
17967
18961
  return;
17968
18962
  }
17969
18963
  }
17970
- const src = join24(at, chain[chain.length - 1]);
18964
+ const src = join26(at, chain[chain.length - 1]);
17971
18965
  const st = lstatOrNull3(src);
17972
18966
  try {
17973
18967
  if (st !== null && st.isSymbolicLink())
17974
18968
  return place(root, rel, { link: readlinkSync(src) }, out);
17975
18969
  if (st !== null && st.isFile())
17976
- return place(root, rel, { bytes: readFileSync22(src), mode: st.mode & 511 }, out);
18970
+ return place(root, rel, { bytes: readFileSync24(src), mode: st.mode & 511 }, out);
17977
18971
  } catch {
17978
18972
  }
17979
18973
  out.left.push(`${rel}: the saved copy does not hold it`);
@@ -18071,7 +19065,7 @@ function restoreLines(root, ref, saved, out) {
18071
19065
  }
18072
19066
  function pinnedEngagement(root) {
18073
19067
  try {
18074
- const pin = JSON.parse(readFileSync22(join24(root, BUNDLE_PIN_REL), "utf-8"));
19068
+ const pin = JSON.parse(readFileSync24(join26(root, BUNDLE_PIN_REL), "utf-8"));
18075
19069
  return isEngagementId(pin.engagementId) ? pin.engagementId : null;
18076
19070
  } catch {
18077
19071
  return null;
@@ -18093,16 +19087,16 @@ function savedRefs(root) {
18093
19087
  return refs;
18094
19088
  }
18095
19089
  function readSaved(folder, id) {
18096
- const dir = join24(folder, id);
19090
+ const dir = join26(folder, id);
18097
19091
  const st = lstatOrNull3(dir);
18098
19092
  if (st === null || !st.isDirectory())
18099
19093
  return null;
18100
- const file = join24(dir, SAVED_FILE_NAME);
19094
+ const file = join26(dir, SAVED_FILE_NAME);
18101
19095
  const fst = lstatOrNull3(file);
18102
19096
  if (fst === null || !fst.isFile())
18103
19097
  return null;
18104
19098
  try {
18105
- const parsed = savedSchema.safeParse(JSON.parse(readFileSync22(file, "utf-8")));
19099
+ const parsed = savedSchema.safeParse(JSON.parse(readFileSync24(file, "utf-8")));
18106
19100
  return parsed.success && parsed.data.id === id ? parsed.data : null;
18107
19101
  } catch {
18108
19102
  return null;
@@ -18111,7 +19105,7 @@ function readSaved(folder, id) {
18111
19105
  function pruneAtRestore(root, folder, refs, now) {
18112
19106
  try {
18113
19107
  const common = git2(root, ["rev-parse", "--git-common-dir"]);
18114
- const repository = common.ok && common.text.trim() !== "" ? realOrSelf(resolve7(root, common.text.trim())) : null;
19108
+ const repository = common.ok && common.text.trim() !== "" ? realOrSelf(resolve8(root, common.text.trim())) : null;
18115
19109
  let names = [];
18116
19110
  try {
18117
19111
  names = readdirSync2(folder);
@@ -18123,7 +19117,7 @@ function pruneAtRestore(root, folder, refs, now) {
18123
19117
  if (!savedIdSchema.safeParse(id).success)
18124
19118
  continue;
18125
19119
  here.add(id);
18126
- const dir = join24(folder, id);
19120
+ const dir = join26(folder, id);
18127
19121
  const st = lstatOrNull3(dir);
18128
19122
  const answer = savedPruneAnswer({
18129
19123
  refMadeAt: refs.get(id) ?? null,
@@ -18151,7 +19145,7 @@ function pruneAtRestore(root, folder, refs, now) {
18151
19145
  }
18152
19146
  function markedRepository(dir) {
18153
19147
  try {
18154
- const text = readFileSync22(join24(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
19148
+ const text = readFileSync24(join26(dir, SAVED_REPOSITORY_FILE_NAME), "utf-8").trim();
18155
19149
  return text === "" ? null : text;
18156
19150
  } catch {
18157
19151
  return null;
@@ -18255,6 +19249,8 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
18255
19249
  run a task's test command on this machine, once before the change and once after, and record what it saw
18256
19250
  ${CHECK_TASKS_USAGE}
18257
19251
  check a phase's plan in docs/build-records/ for work that would collide if it ran at once
19252
+ ${CHECK_CRITERIA_USAGE}
19253
+ check that the record of a finished phase's check names every acceptance criterion of its features
18258
19254
  ${CHECK_REPORT_USAGE}
18259
19255
  check a worker's report, saved from its pull request, against the commit it must be about
18260
19256
  ${READY_USAGE}
@@ -18265,6 +19261,8 @@ var USAGE = ` ${HALFCYCLE_COMMAND} [install] [target-repo] [engagement-id] [sel
18265
19261
  ${PACE_USAGE}
18266
19262
  suggest how many agents this build should run at once, and for how long; with no flag, how
18267
19263
  many may start right now
19264
+ ${ROUTE_USAGE}
19265
+ say how to start a task of a phase's plan: which crew member, and on which tier
18268
19266
  ${HALFCYCLE_COMMAND} preview [folder]
18269
19267
  show what setting up this folder would add, change, remove or leave alone; writes nothing
18270
19268
  ${HALFCYCLE_COMMAND} doctor
@@ -18711,7 +19709,6 @@ ${PROJECT_RESET_LINE}
18711
19709
  process.stdout.write(`[halfcycle] Method version: ${methodVersion}
18712
19710
  `);
18713
19711
  }
18714
- await setupClaudePlan({ serviceUrl: credential?.serviceUrl ?? serviceUrl, engagementId, ownsProject, consoleUrl }, { interactive: () => isInteractiveRun(), assumeYes, ask: askLine, write: (text) => process.stdout.write(text) });
18715
19712
  if (checks.gitInstalled) {
18716
19713
  process.stdout.write(`[halfcycle] Git for Windows is installed. Open a new terminal before starting Claude Code.
18717
19714
  `);
@@ -18929,6 +19926,22 @@ halfcycle build-record: until ${obtained.stampFailure.path} can be written, the
18929
19926
  process.exitCode = run.exitCode;
18930
19927
  return;
18931
19928
  }
19929
+ if (cmd === "check-criteria") {
19930
+ const parsed = parseCheckCriteriaArgs(rest);
19931
+ if (!parsed.ok) {
19932
+ process.stderr.write(`halfcycle check-criteria: usage: ${CHECK_CRITERIA_USAGE}
19933
+ `);
19934
+ process.exitCode = 2;
19935
+ return;
19936
+ }
19937
+ const run = runCheckCriteria(parsed.repoRoot ?? process.cwd(), parsed.file);
19938
+ if (run.stdout !== "")
19939
+ process.stdout.write(run.stdout);
19940
+ if (run.stderr !== "")
19941
+ process.stderr.write(run.stderr);
19942
+ process.exitCode = run.exitCode;
19943
+ return;
19944
+ }
18932
19945
  if (cmd === "check-report") {
18933
19946
  const reportShortfalls = missingRequirements("check-report", rest);
18934
19947
  if (reportShortfalls.length > 0) {
@@ -18987,7 +20000,7 @@ halfcycle build-record: until ${obtained.stampFailure.path} can be written, the
18987
20000
  readCredential: (root) => readRepoCredential(root),
18988
20001
  spawnPace: (credential, root, args2) => runPaceChild(runner, credential, root, args2),
18989
20002
  now: () => /* @__PURE__ */ new Date()
18990
- });
20003
+ }, parsed.agent);
18991
20004
  if (run.stdout !== "")
18992
20005
  process.stdout.write(run.stdout);
18993
20006
  if (run.stderr !== "")
@@ -18995,6 +20008,23 @@ halfcycle build-record: until ${obtained.stampFailure.path} can be written, the
18995
20008
  process.exitCode = run.exitCode;
18996
20009
  return;
18997
20010
  }
20011
+ if (cmd === "route") {
20012
+ const parsed = parseRouteArgs(rest);
20013
+ if (!parsed.ok) {
20014
+ process.stderr.write(`halfcycle route: usage: ${ROUTE_USAGE}
20015
+ `);
20016
+ process.exitCode = 2;
20017
+ return;
20018
+ }
20019
+ const run = await runRoute(parsed.repoRoot ?? process.cwd(), parsed.phase, parsed.task, parsed.agent, {
20020
+ readCredential: (root) => readRepoCredential(root),
20021
+ readWork: (credential, phase) => readPhaseWork(credential, phase)
20022
+ });
20023
+ if (run.stderr !== "")
20024
+ process.stderr.write(run.stderr);
20025
+ process.stdout.write(run.stdout, () => process.exit(run.exitCode));
20026
+ return;
20027
+ }
18998
20028
  if (cmd === "change") {
18999
20029
  const parsed = parseChangeArgs(rest);
19000
20030
  if (!parsed.ok) {
@@ -19217,12 +20247,22 @@ halfcycle open-phase: until ${opened.stampFailure.path} can be written, guard ev
19217
20247
  const timeSummary = await readCloseTimeSummary(credential, phaseArg);
19218
20248
  for (const line of timeTotalsLines(timeSummary))
19219
20249
  process.stdout.write(`${line}
20250
+ `);
20251
+ for (const line of phaseLimitLines(credential.engagementId, phaseArg))
20252
+ process.stdout.write(`${line}
20253
+ `);
20254
+ for (const line of relayedChoiceLines(credential.engagementId, phaseArg))
20255
+ process.stdout.write(`${line}
19220
20256
  `);
19221
20257
  const declarations = await readCloseOwnerWaitDeclarations(credential, phaseArg);
20258
+ const work = await readPhaseWork(credential, phaseArg);
20259
+ for (const line of tierFigureLines(work.kind === "work" ? work.work : null, readPhaseExpected(credential.engagementId, phaseArg), timeSummary, readPhaseModels(credential.engagementId, phaseArg, timeSummary?.openedAt ?? null))) {
20260
+ process.stdout.write(`${line}
20261
+ `);
20262
+ }
19222
20263
  for (const line of ownerWaitCloseLines(declarations))
19223
20264
  process.stdout.write(`${line}
19224
20265
  `);
19225
- const work = await readPhaseWork(credential, phaseArg);
19226
20266
  if (work.kind === "work") {
19227
20267
  for (const line of wentAheadCloseLines(work.work.calls))
19228
20268
  process.stdout.write(`${line}