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