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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +3523 -2168
- package/bin/pre-tool-use-filter.sh +6 -2
- package/dist/bin.js +0 -0
- package/dist/check-criteria.d.ts +57 -0
- package/dist/check-criteria.d.ts.map +1 -0
- package/dist/check-report.d.ts +2 -1
- package/dist/check-report.d.ts.map +1 -1
- package/dist/check-tasks.d.ts +75 -22
- package/dist/check-tasks.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +1407 -367
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts +39 -1
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/index.js +116 -17
- package/dist/index.js.map +3 -3
- package/dist/install.d.ts +7 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/pace.d.ts +19 -4
- package/dist/pace.d.ts.map +1 -1
- package/dist/ready.d.ts +6 -3
- package/dist/ready.d.ts.map +1 -1
- package/dist/route.d.ts +95 -0
- package/dist/route.d.ts.map +1 -0
- package/package.json +11 -11
- package/dist/claude-plan.d.ts +0 -133
- package/dist/claude-plan.d.ts.map +0 -1
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}"
|
|
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}"
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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 =
|
|
1069
|
-
if (shared
|
|
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
|
-
|
|
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
|
|
1420
|
-
ownsSeen
|
|
1421
|
-
|
|
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
|
|
1827
|
-
var agentNameSchema = z4.string().min(1).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
|
-
|
|
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
|
|
5807
|
-
|
|
5808
|
-
|
|
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
|
|
5830
|
-
var
|
|
5831
|
-
var
|
|
5832
|
-
|
|
5833
|
-
|
|
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
|
|
5838
|
-
var
|
|
5839
|
-
var
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
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:
|
|
6337
|
+
claudePlan: z33.string().nullable().optional(),
|
|
5856
6338
|
keepFreePercent: keepFreePercentSchema,
|
|
5857
|
-
settingsUrl: z33.string().
|
|
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
|
-
|
|
5870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
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
|
-
|
|
5917
|
-
|
|
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
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
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(
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
7893
|
-
const body = crewProfileBody(member.role, member.group,
|
|
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(
|
|
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").
|
|
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"
|
|
8789
|
-
#
|
|
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((
|
|
10943
|
+
const bindProblem = await new Promise((resolve9) => {
|
|
10240
10944
|
const onError = (err) => {
|
|
10241
|
-
|
|
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
|
-
|
|
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((
|
|
10966
|
+
waitForCallback: ({ timeoutMs, onStillWaiting, stillWaitingMs = DEFAULT_STILL_WAITING_MS }) => new Promise((resolve9) => {
|
|
10263
10967
|
if (pending !== null) {
|
|
10264
|
-
|
|
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
|
-
|
|
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((
|
|
10993
|
+
return new Promise((resolve9) => {
|
|
10290
10994
|
server.closeAllConnections();
|
|
10291
|
-
server.close(() =>
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10419
|
-
throw new SignInRefused(
|
|
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((
|
|
10521
|
-
const timer = setTimeout(
|
|
11224
|
+
await new Promise((resolve9) => {
|
|
11225
|
+
const timer = setTimeout(resolve9, started.pollIntervalMs);
|
|
10522
11226
|
interrupt = () => {
|
|
10523
11227
|
clearTimeout(timer);
|
|
10524
|
-
|
|
11228
|
+
resolve9();
|
|
10525
11229
|
};
|
|
10526
11230
|
});
|
|
10527
11231
|
} else {
|
|
10528
|
-
await new Promise((
|
|
10529
|
-
interrupt =
|
|
10530
|
-
void sleep(started.pollIntervalMs).then(
|
|
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
|
|
10719
|
-
if (
|
|
10720
|
-
throw new CreateEngagementRefused(res.status,
|
|
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((
|
|
11153
|
-
rl.once("close", () =>
|
|
11154
|
-
rl.question(question2).then(
|
|
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,
|
|
12125
|
+
function refuseSystemFolder(targetRepo, platform5 = process.platform) {
|
|
11422
12126
|
const key = (folder) => {
|
|
11423
12127
|
const real = realOrLiteral(folder);
|
|
11424
|
-
return
|
|
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
|
|
11575
|
-
if (
|
|
11576
|
-
return { refusal:
|
|
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
|
|
12761
|
+
import { delimiter as delimiter2, join as join11 } from "node:path";
|
|
12157
12762
|
function execVersionCommand(command, timeoutMs, deps) {
|
|
12158
|
-
const
|
|
12763
|
+
const platform5 = deps.platform ?? process.platform;
|
|
12159
12764
|
const exec = deps.exec ?? execFileSync3;
|
|
12160
12765
|
try {
|
|
12161
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
12247
|
-
import { basename as basename2, join as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
12935
|
-
const mdPath =
|
|
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
|
-
|
|
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
|
|
12984
|
-
import { join as
|
|
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
|
|
12988
|
-
import { dirname as
|
|
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 =
|
|
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,
|
|
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 =
|
|
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] =
|
|
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
|
-
|
|
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
|
|
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
|
|
13530
|
-
import { join as
|
|
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 =
|
|
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
|
|
13710
|
-
if (
|
|
13711
|
-
return { kind: "stopped", stderr: warnings +
|
|
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
|
|
13759
|
-
import { basename as basename3, isAbsolute as isAbsolute5, join as
|
|
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 =
|
|
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 =
|
|
13925
|
-
const mdPath =
|
|
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
|
-
|
|
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
|
|
14177
|
-
if (
|
|
14178
|
-
return { kind: "refused", line:
|
|
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 ${
|
|
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
|
|
14441
|
-
import { platform as
|
|
14442
|
-
import { join as
|
|
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 =
|
|
15056
|
+
const path = join17(dir, SHOWN_PLANS_FILENAME);
|
|
14448
15057
|
let current;
|
|
14449
15058
|
try {
|
|
14450
|
-
current = parseShownRecord(
|
|
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
|
-
|
|
14457
|
-
temp =
|
|
14458
|
-
|
|
14459
|
-
if (
|
|
14460
|
-
|
|
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 =
|
|
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(
|
|
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((
|
|
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
|
-
|
|
15896
|
+
resolve9(stopped || typeof stdout !== "string" ? fallback : readLine(stdout));
|
|
15288
15897
|
});
|
|
15289
15898
|
} catch {
|
|
15290
|
-
|
|
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
|
|
16091
|
-
if (
|
|
16092
|
-
return
|
|
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(
|
|
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(
|
|
16290
|
-
} catch {
|
|
16291
|
-
return
|
|
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
|
|
16295
|
-
return
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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 =
|
|
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() &&
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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}
|