@pmelab/gtd 36.0.0 → 38.0.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/README.md +30 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +277 -58
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +355 -75
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/testkit/steps.ts +35 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +22 -8
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +26 -3
- package/src/workflows/feature/feature.ts +69 -16
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +169 -31
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +19 -4
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.ts +129 -28
- package/src/workflows/shared/vars.ts +1 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
1
|
+
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-
|
|
3
|
+
import { $ as naivePassPrompt, A as buildReviewAnswerQuestionsPrompt, B as coveringPrompt, E as reviewMissing, F as builderSystem, G as designTriagePrompt, H as deferNotesScript, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, N as buildReviewFixNitsPrompt, O as reviewing, P as buildReviewTriageMessage, Q as naiveApplyFailure, R as coverageMessage, S as fixCheck, T as planner, U as designGateAnswerMessage, V as criterionLine, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, a as flagModeOn, at as refactorNotePrompt, b as awaitReview, bt as envDefaults, ct as reviewEditsCapture, d as guarded, dt as reviewerSystem, et as normaliseCriteriaScript, f as healthy, ft as seamByHandMessage, g as REFACTOR_NOTE, gt as uncoveredMessage, h as REFACTOR, ht as startGateBlockedMessage, i as skills, it as refactorFixPrompt, j as buildReviewAwaitAfterDeferMessage, k as agentWithSkills, m as QUALITY, mt as slicingPrompt, n as summary, nt as refactorBuildingPrompt, ot as refactorRules, p as FEEDBACK, pt as seamSteerMessage, r as access, rt as refactorDroppedRefusal, s as shipped, st as reviewEditNotesCapture, t as steering, tt as qualityUnresolvedMessage, u as gate, ut as reviewNotesCapture, v as REVIEW, vt as unwindFailure, w as fixRisks, x as coder, xt as testFileGlobs, y as SLICES, yt as defaults, z as coverageQuestion } from "../chunks/reserved-sfMdy7v7.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-Cgca5Gp-.mjs";
|
|
5
5
|
import { posix } from "node:path";
|
|
6
6
|
//#region src/workflows/shared/fences.ts
|
|
7
7
|
const markFenced = (text) => {
|
|
@@ -17,13 +17,15 @@ const markFenced = (text) => {
|
|
|
17
17
|
};
|
|
18
18
|
});
|
|
19
19
|
};
|
|
20
|
+
/** A markdown heading: its `#` run, then its text. */
|
|
21
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
22
|
+
/** A list item, its checkbox dropped: its text. */
|
|
23
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
20
24
|
//#endregion
|
|
21
25
|
//#region src/workflows/feature/criteria.ts
|
|
22
26
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
23
27
|
const CLEAR_MIN_P = .8;
|
|
24
|
-
const
|
|
25
|
-
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
26
|
-
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
28
|
+
const ID$1 = /^(AC-\d+):\s*(.*)$/;
|
|
27
29
|
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
28
30
|
const listUnder = (text, title, level) => {
|
|
29
31
|
const lines = markFenced(text);
|
|
@@ -51,7 +53,7 @@ const listUnder = (text, title, level) => {
|
|
|
51
53
|
};
|
|
52
54
|
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
53
55
|
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
54
|
-
const id = ID.exec(item);
|
|
56
|
+
const id = ID$1.exec(item);
|
|
55
57
|
return id === null ? {
|
|
56
58
|
id: "",
|
|
57
59
|
text: item
|
|
@@ -60,16 +62,20 @@ const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((
|
|
|
60
62
|
text: id[2]
|
|
61
63
|
};
|
|
62
64
|
});
|
|
65
|
+
/** The ID'd criteria of `text`, only `slice`'s when given. */
|
|
66
|
+
const criterionIds = (text, slice) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "" && (slice?.only === void 0 || slice.only.includes(id)) && !(slice?.except ?? []).includes(id));
|
|
67
|
+
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
68
|
+
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
63
69
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
64
70
|
const numbered = (items) => {
|
|
65
|
-
const ids = items.map((item) => ID.exec(item));
|
|
71
|
+
const ids = items.map((item) => ID$1.exec(item));
|
|
66
72
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
67
73
|
return items.map((item, i) => own ? {
|
|
68
74
|
id: ids[i][1],
|
|
69
75
|
text: ids[i][2]
|
|
70
76
|
} : {
|
|
71
77
|
id: `AC-${i + 1}`,
|
|
72
|
-
text: item.replace(ID, "$2")
|
|
78
|
+
text: item.replace(ID$1, "$2")
|
|
73
79
|
});
|
|
74
80
|
};
|
|
75
81
|
const STATED = /^acceptance criteria:?$/i;
|
|
@@ -623,7 +629,8 @@ const plan = (shape, options) => {
|
|
|
623
629
|
return {
|
|
624
630
|
pattern,
|
|
625
631
|
strays,
|
|
626
|
-
reference: options.reference?.trim() ? options.reference : void 0
|
|
632
|
+
reference: options.reference?.trim() ? options.reference : void 0,
|
|
633
|
+
note: options.note?.trim() ? options.note : void 0
|
|
627
634
|
};
|
|
628
635
|
};
|
|
629
636
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
@@ -632,13 +639,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
632
639
|
};
|
|
633
640
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
634
641
|
const refactorLap = async (p, guard, feedback) => {
|
|
635
|
-
const { pattern, strays, reference } = p;
|
|
636
|
-
await run("deriving", deriveScript([
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
+
const { pattern, strays, reference, note } = p;
|
|
643
|
+
await run("deriving", deriveScript([
|
|
644
|
+
...packageLines(strays, pattern, reference),
|
|
645
|
+
...note === void 0 ? [] : [
|
|
646
|
+
"## Refactor note",
|
|
647
|
+
"",
|
|
648
|
+
...demoteHeadings(note),
|
|
649
|
+
""
|
|
650
|
+
],
|
|
651
|
+
...feedback === void 0 ? [] : [
|
|
652
|
+
"## Review feedback",
|
|
653
|
+
"",
|
|
654
|
+
...demoteHeadings(feedback),
|
|
655
|
+
""
|
|
656
|
+
]
|
|
657
|
+
]), {
|
|
642
658
|
label: "Deriving the refactor package",
|
|
643
659
|
file: REFACTOR_PACKAGE
|
|
644
660
|
});
|
|
@@ -647,7 +663,8 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
647
663
|
strays,
|
|
648
664
|
pattern,
|
|
649
665
|
reference,
|
|
650
|
-
feedback: feedback !== void 0
|
|
666
|
+
feedback: feedback !== void 0,
|
|
667
|
+
note: note !== void 0
|
|
651
668
|
}), {
|
|
652
669
|
label: "Refactoring",
|
|
653
670
|
model: coder(),
|
|
@@ -733,22 +750,27 @@ const COVERED_MIN_P = .7;
|
|
|
733
750
|
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
734
751
|
const COVERING_RETRIES = 2;
|
|
735
752
|
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
736
|
-
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
753
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
737
754
|
label: "Fixing the check",
|
|
738
755
|
file: FEEDBACK,
|
|
739
756
|
model: coder(),
|
|
740
757
|
system: builderSystem()
|
|
741
758
|
}), {
|
|
742
|
-
guard:
|
|
759
|
+
guard: () => {
|
|
760
|
+
frozen();
|
|
761
|
+
guard?.();
|
|
762
|
+
},
|
|
743
763
|
guardRests: false
|
|
744
764
|
});
|
|
745
765
|
const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
|
|
746
766
|
/**
|
|
747
767
|
* The criterion IDs of `.gtd/REQUIREMENTS.md` — read afresh, never carried
|
|
748
|
-
* over — no passing test since `base` covers, by the judge.
|
|
768
|
+
* over, only `slice`'s when given — no passing test since `base` covers, by the judge.
|
|
749
769
|
*/
|
|
750
|
-
const uncoveredSince = async (base) => {
|
|
751
|
-
const
|
|
770
|
+
const uncoveredSince = async (base, slice) => {
|
|
771
|
+
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
772
|
+
const own = criterionIds(requirements, slice);
|
|
773
|
+
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
752
774
|
if (criteria.length === 0) return {
|
|
753
775
|
covered: [],
|
|
754
776
|
uncovered: []
|
|
@@ -775,14 +797,14 @@ const record = (covered, uncovered = []) => {
|
|
|
775
797
|
* sending uncovered IDs back to a covering turn; marks the IDs covered on the
|
|
776
798
|
* landing that ends it. A human may overrule once the retries are spent.
|
|
777
799
|
*/
|
|
778
|
-
const requireCoverage = async (base = start()) => {
|
|
800
|
+
const requireCoverage = async (base = start(), slice) => {
|
|
779
801
|
let retries = 0;
|
|
780
802
|
for (;;) {
|
|
781
|
-
const { covered, uncovered } = await uncoveredSince(base);
|
|
803
|
+
const { covered, uncovered } = await uncoveredSince(base, slice);
|
|
782
804
|
if (uncovered.length === 0) return record(covered);
|
|
783
805
|
if (retries < COVERING_RETRIES) {
|
|
784
806
|
retries++;
|
|
785
|
-
await agentWithSkills("covering", coveringPrompt(uncovered), {
|
|
807
|
+
await agentWithSkills("covering", coveringPrompt(uncovered, sliceIds(slice)), {
|
|
786
808
|
label: "Covering the acceptance criteria",
|
|
787
809
|
model: coder(),
|
|
788
810
|
system: builderSystem(),
|
|
@@ -813,7 +835,7 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
813
835
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
814
836
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
815
837
|
});
|
|
816
|
-
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
838
|
+
const naivePass = (retry, slice) => agentWithSkills("naive", naivePassPrompt(retry, sliceIds(slice)), {
|
|
817
839
|
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
818
840
|
keep: true,
|
|
819
841
|
model: coder(),
|
|
@@ -853,16 +875,20 @@ const addedLines = (change) => {
|
|
|
853
875
|
const before = new Set((change.before ?? "").split("\n"));
|
|
854
876
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
855
877
|
};
|
|
878
|
+
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
879
|
+
const idsOf = (file, slice) => criterionIds(read(file) ?? "", slice);
|
|
880
|
+
const unnamed = (ids, tests) => {
|
|
881
|
+
const lines = tests.map(addedLines);
|
|
882
|
+
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
883
|
+
};
|
|
856
884
|
/**
|
|
857
885
|
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
858
886
|
* test line of the pass names, or a test importing a file the pass added —
|
|
859
887
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
860
888
|
*/
|
|
861
|
-
const acceptanceFault = (file, tip) => {
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const lines = tests.map(addedLines);
|
|
865
|
-
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
889
|
+
const acceptanceFault = (file, tip, slice) => {
|
|
890
|
+
const tests = acceptanceTests(tip.changes);
|
|
891
|
+
const untested = unnamed(idsOf(file, slice), tests);
|
|
866
892
|
if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
|
|
867
893
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
868
894
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -871,6 +897,41 @@ const acceptanceFault = (file, tip) => {
|
|
|
871
897
|
});
|
|
872
898
|
if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
|
|
873
899
|
};
|
|
900
|
+
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
901
|
+
const naiveAcceptance = (tip, slice) => {
|
|
902
|
+
const ids = idsOf(REQUIREMENTS, slice);
|
|
903
|
+
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
904
|
+
const lines = addedLines(c);
|
|
905
|
+
return {
|
|
906
|
+
path: c.path,
|
|
907
|
+
ids: ids.filter((id) => named(id, lines))
|
|
908
|
+
};
|
|
909
|
+
});
|
|
910
|
+
return {
|
|
911
|
+
hash: tip.hash,
|
|
912
|
+
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
913
|
+
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
|
|
914
|
+
};
|
|
915
|
+
};
|
|
916
|
+
/**
|
|
917
|
+
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
918
|
+
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
919
|
+
*/
|
|
920
|
+
const requireAcceptance = (acceptance, since) => {
|
|
921
|
+
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
|
|
922
|
+
if (missing.length === 0) return;
|
|
923
|
+
refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
927
|
+
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
928
|
+
* wedges later fix turns.
|
|
929
|
+
*/
|
|
930
|
+
const requireAcceptanceKept = (acceptance) => {
|
|
931
|
+
const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
|
|
932
|
+
if (dropped.length === 0) return;
|
|
933
|
+
refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
|
|
934
|
+
};
|
|
874
935
|
const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
|
|
875
936
|
/**
|
|
876
937
|
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
@@ -914,18 +975,30 @@ function implement(first, tier, amend) {
|
|
|
914
975
|
amend
|
|
915
976
|
};
|
|
916
977
|
return scope("implement", async () => {
|
|
917
|
-
|
|
978
|
+
const since = head();
|
|
979
|
+
const acceptance = options.rebuild?.acceptance;
|
|
980
|
+
const built = options.naive === void 0 || !await applied(options.naive);
|
|
981
|
+
if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild, sliceIds(options.slice)), {
|
|
918
982
|
label: "Implementing",
|
|
919
983
|
model: coder(),
|
|
920
984
|
system: builderSystem(),
|
|
921
985
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
922
986
|
});
|
|
923
987
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
924
|
-
|
|
925
|
-
await
|
|
926
|
-
await
|
|
988
|
+
if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
|
|
989
|
+
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
990
|
+
await requireCoverage(options.base, options.slice);
|
|
991
|
+
await closing(options.more === true);
|
|
927
992
|
});
|
|
928
993
|
}
|
|
994
|
+
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
995
|
+
const closeOut = () => scope("implement", () => closing(false));
|
|
996
|
+
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
997
|
+
const closing = (more) => run("closing", removeScript(more ? [REFACTOR_NOTE] : [
|
|
998
|
+
REQUIREMENTS,
|
|
999
|
+
REFACTOR_NOTE,
|
|
1000
|
+
SLICES
|
|
1001
|
+
]), { label: "Closing out the implementation" });
|
|
929
1002
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
930
1003
|
const steer = async (retry) => {
|
|
931
1004
|
await human("steer", {
|
|
@@ -956,8 +1029,8 @@ const budget = () => {
|
|
|
956
1029
|
};
|
|
957
1030
|
};
|
|
958
1031
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
959
|
-
const vetted = async (tip, vetting) => {
|
|
960
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
1032
|
+
const vetted = async (tip, vetting, slice) => {
|
|
1033
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip, slice);
|
|
961
1034
|
if (fault !== void 0) {
|
|
962
1035
|
const verdict = await vetting.again({
|
|
963
1036
|
fault,
|
|
@@ -973,33 +1046,56 @@ const vetted = async (tip, vetting) => {
|
|
|
973
1046
|
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
974
1047
|
const ruled = async (tip, again) => {
|
|
975
1048
|
const shape = await judgeSeam(tip);
|
|
976
|
-
if (shape.choice === "land") return {
|
|
1049
|
+
if (shape.choice === "land") return {
|
|
1050
|
+
reference: tip.hash,
|
|
1051
|
+
tip
|
|
1052
|
+
};
|
|
977
1053
|
if (shape.choice === "refactor-first") return {
|
|
978
1054
|
reference: tip.hash,
|
|
1055
|
+
tip,
|
|
979
1056
|
shape
|
|
980
1057
|
};
|
|
981
1058
|
const next = await again({
|
|
982
1059
|
strays: shape.strays,
|
|
983
1060
|
p: shape.judgment?.answers.shape?.p ?? 0
|
|
984
1061
|
});
|
|
985
|
-
return next.kind === "overruled" ? {
|
|
1062
|
+
return next.kind === "overruled" ? {
|
|
1063
|
+
reference: tip.hash,
|
|
1064
|
+
tip
|
|
1065
|
+
} : next;
|
|
986
1066
|
};
|
|
987
|
-
const decide = () => scope("seam", async () => {
|
|
1067
|
+
const decide = (slice) => scope("seam", async () => {
|
|
988
1068
|
const vetting = {
|
|
989
1069
|
again: budget(),
|
|
990
1070
|
waived: false
|
|
991
1071
|
};
|
|
992
1072
|
let retry;
|
|
993
1073
|
for (;;) {
|
|
994
|
-
await naivePass(retry);
|
|
1074
|
+
await naivePass(retry, slice);
|
|
995
1075
|
const tip = kept("naive");
|
|
996
|
-
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
1076
|
+
const outcome = await vetted(tip, vetting, slice) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
1077
|
+
reference: tip.hash,
|
|
1078
|
+
tip
|
|
1079
|
+
});
|
|
997
1080
|
if (!("kind" in outcome)) return outcome;
|
|
998
1081
|
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
999
1082
|
}
|
|
1000
1083
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1084
|
+
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
1085
|
+
const refactorNote = async (reference, strays) => {
|
|
1086
|
+
await agent("note", refactorNotePrompt(reference, strays), {
|
|
1087
|
+
label: "Writing the refactor note",
|
|
1088
|
+
file: REFACTOR_NOTE,
|
|
1089
|
+
model: coder(),
|
|
1090
|
+
system: builderSystem()
|
|
1091
|
+
});
|
|
1092
|
+
const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
|
|
1093
|
+
const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
|
|
1094
|
+
if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
|
|
1095
|
+
return note;
|
|
1096
|
+
};
|
|
1097
|
+
const seamBuild = async (base, slice$1 = {}) => {
|
|
1098
|
+
const { reference, tip, shape } = await decide(slice$1.criteria);
|
|
1003
1099
|
if (shape === void 0) return {
|
|
1004
1100
|
reference,
|
|
1005
1101
|
base,
|
|
@@ -1013,42 +1109,164 @@ const seamBuild = async (base) => {
|
|
|
1013
1109
|
}));
|
|
1014
1110
|
return {
|
|
1015
1111
|
reference,
|
|
1016
|
-
base
|
|
1112
|
+
base,
|
|
1113
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
1017
1114
|
};
|
|
1018
1115
|
}
|
|
1019
|
-
|
|
1116
|
+
const strays = refactorStrays(shape);
|
|
1117
|
+
if (strays.length === 0) return {
|
|
1020
1118
|
reference,
|
|
1021
|
-
base
|
|
1119
|
+
base,
|
|
1120
|
+
rebuild: { acceptance: naiveAcceptance(tip, slice$1.criteria) }
|
|
1022
1121
|
};
|
|
1023
|
-
const
|
|
1122
|
+
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
1123
|
+
const refactored = await slice(slice$1.refactor ?? "refactor", () => reviewedRefactor(shape, {
|
|
1124
|
+
reference,
|
|
1125
|
+
note
|
|
1126
|
+
}));
|
|
1024
1127
|
return {
|
|
1025
1128
|
reference,
|
|
1026
1129
|
base: head(),
|
|
1027
|
-
refactored
|
|
1130
|
+
refactored,
|
|
1131
|
+
rebuild: {
|
|
1132
|
+
note,
|
|
1133
|
+
acceptance: naiveAcceptance(tip, slice$1.criteria)
|
|
1134
|
+
}
|
|
1028
1135
|
};
|
|
1029
1136
|
};
|
|
1030
1137
|
//#endregion
|
|
1138
|
+
//#region src/workflows/feature/slicing.ts
|
|
1139
|
+
const ID = /^(AC-\d+)(?![\w-])/;
|
|
1140
|
+
/** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
|
|
1141
|
+
const sliceCap = () => {
|
|
1142
|
+
const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
|
|
1143
|
+
if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
|
|
1144
|
+
return Number(value);
|
|
1145
|
+
};
|
|
1146
|
+
/** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
|
|
1147
|
+
const slicesOf = (text) => {
|
|
1148
|
+
const slices = [];
|
|
1149
|
+
let open = false;
|
|
1150
|
+
for (const { line, fenced } of markFenced(text)) {
|
|
1151
|
+
if (fenced) continue;
|
|
1152
|
+
const heading = HEADING.exec(line);
|
|
1153
|
+
if (heading !== null) {
|
|
1154
|
+
open = heading[1] === "##";
|
|
1155
|
+
if (open) slices.push({
|
|
1156
|
+
title: heading[2],
|
|
1157
|
+
ids: []
|
|
1158
|
+
});
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const item = ITEM.exec(line);
|
|
1162
|
+
if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
|
|
1163
|
+
}
|
|
1164
|
+
return slices;
|
|
1165
|
+
};
|
|
1166
|
+
const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
|
|
1167
|
+
/** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
|
|
1168
|
+
const sliceFault = (slices, ids, cap) => {
|
|
1169
|
+
const listed = slices.flatMap((s) => s.ids);
|
|
1170
|
+
if (slices.length === 0) return `no "## " slice`;
|
|
1171
|
+
const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
|
|
1172
|
+
if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
|
|
1173
|
+
if (listed.includes("")) return "a list item not led by a criterion ID";
|
|
1174
|
+
const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
|
|
1175
|
+
if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
|
|
1176
|
+
const repeated = twice(listed);
|
|
1177
|
+
if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
|
|
1178
|
+
const unknown = listed.filter((id) => !ids.includes(id));
|
|
1179
|
+
if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
|
|
1180
|
+
const missing = ids.filter((id) => !listed.includes(id));
|
|
1181
|
+
if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
|
|
1182
|
+
};
|
|
1183
|
+
/** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
|
|
1184
|
+
const requireSlices = (ids, cap) => {
|
|
1185
|
+
const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
|
|
1186
|
+
const why = sliceFault(slices, ids, cap);
|
|
1187
|
+
if (why !== void 0) refuse(`gtd land: slices: ${SLICES} has ${why} — list each slice as a "## <title>" heading over "- AC-<n>" items, every criterion of ${REQUIREMENTS} in exactly one slice, at most ${cap} per slice`);
|
|
1188
|
+
return slices;
|
|
1189
|
+
};
|
|
1190
|
+
/**
|
|
1191
|
+
* The slices to build, in order. Criteria within `sliceMaxCriteria` are one
|
|
1192
|
+
* slice and no turn; above it `design.slicing` groups them.
|
|
1193
|
+
*/
|
|
1194
|
+
const slicing = async () => {
|
|
1195
|
+
const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
1196
|
+
const cap = sliceCap();
|
|
1197
|
+
if (all.length <= cap) return [{
|
|
1198
|
+
title: "",
|
|
1199
|
+
ids: all
|
|
1200
|
+
}];
|
|
1201
|
+
return scope("design", async () => {
|
|
1202
|
+
await agentWithSkills("slicing", slicingPrompt(cap), {
|
|
1203
|
+
label: "Slicing the change",
|
|
1204
|
+
file: SLICES,
|
|
1205
|
+
model: planner(),
|
|
1206
|
+
system: designSystem()
|
|
1207
|
+
});
|
|
1208
|
+
return requireSlices(all, cap);
|
|
1209
|
+
});
|
|
1210
|
+
};
|
|
1211
|
+
/** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
|
|
1212
|
+
const sliceCriteria = (slices, i) => slices.length === 1 ? void 0 : i < slices.length - 1 ? { only: slices[i].ids } : { except: slices.slice(0, -1).flatMap((s) => s.ids) };
|
|
1213
|
+
//#endregion
|
|
1031
1214
|
//#region src/workflows/feature/feature.ts
|
|
1032
1215
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
1033
1216
|
const unwind = () => {
|
|
1034
1217
|
const commit = head();
|
|
1035
1218
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
1036
1219
|
};
|
|
1037
|
-
/**
|
|
1220
|
+
/**
|
|
1221
|
+
* The slices `design.slicing` grouped the criteria into, each built in turn
|
|
1222
|
+
* from where the previous one landed; design must have run since `firstBase`.
|
|
1223
|
+
*/
|
|
1038
1224
|
const buildFeature = async (firstBase) => {
|
|
1039
1225
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1226
|
+
const slices = await slicing();
|
|
1227
|
+
const one = slices.length === 1;
|
|
1228
|
+
for (const i of slices.keys()) {
|
|
1229
|
+
const more = i < slices.length - 1;
|
|
1230
|
+
const criteria = sliceCriteria(slices, i);
|
|
1231
|
+
if (sliceIds(criteria)?.length === 0) {
|
|
1232
|
+
if (!more) await closeOut();
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const n = one ? "" : `-${i + 1}`;
|
|
1236
|
+
await buildSlice(i === 0 ? firstBase : head(), {
|
|
1237
|
+
tier,
|
|
1238
|
+
criteria,
|
|
1239
|
+
refactor: `refactor${n}`,
|
|
1240
|
+
feature: `feature${n}`,
|
|
1241
|
+
more
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap. */
|
|
1246
|
+
const buildSlice = async (sliceBase, build) => {
|
|
1247
|
+
const seam = await seamBuild(sliceBase, {
|
|
1248
|
+
criteria: build.criteria,
|
|
1249
|
+
refactor: build.refactor
|
|
1250
|
+
});
|
|
1251
|
+
await slice(build.feature, async () => {
|
|
1042
1252
|
await implement({
|
|
1043
1253
|
reference: seam.reference,
|
|
1044
|
-
tier,
|
|
1254
|
+
tier: build.tier,
|
|
1045
1255
|
afterRefactor: seam.refactored,
|
|
1046
|
-
naive: seam.land === true ? seam.reference : void 0
|
|
1256
|
+
naive: seam.land === true ? seam.reference : void 0,
|
|
1257
|
+
rebuild: seam.rebuild,
|
|
1258
|
+
slice: build.criteria,
|
|
1259
|
+
base: build.criteria === void 0 ? void 0 : sliceBase,
|
|
1260
|
+
more: build.more
|
|
1047
1261
|
});
|
|
1048
|
-
const
|
|
1262
|
+
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
1263
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
|
|
1264
|
+
const guard = build.more ? () => requireCriteriaFrozen(REQUIREMENTS) : void 0;
|
|
1049
1265
|
await tail(settleQuality, {
|
|
1050
1266
|
green: true,
|
|
1051
|
-
lensBase: base
|
|
1267
|
+
lensBase: base,
|
|
1268
|
+
guard,
|
|
1269
|
+
guardRests: false
|
|
1052
1270
|
});
|
|
1053
1271
|
});
|
|
1054
1272
|
};
|
|
@@ -1094,6 +1312,7 @@ const ordinaryStart = async () => {
|
|
|
1094
1312
|
});
|
|
1095
1313
|
refusePendingHandOff();
|
|
1096
1314
|
flagModeOn();
|
|
1315
|
+
sliceCap();
|
|
1097
1316
|
const stated = statedCriteria(changes(), head());
|
|
1098
1317
|
await unwind();
|
|
1099
1318
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
@@ -1109,4 +1328,4 @@ const feature = shipped(ordinaryStart);
|
|
|
1109
1328
|
//#region src/workflows/feature/index.ts
|
|
1110
1329
|
var feature_default = feature;
|
|
1111
1330
|
//#endregion
|
|
1112
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, isTestFile, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
|
|
1331
|
+
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireSlices, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { K as finisherSystem, bt as envDefaults, c as baseline, i as skills, k as agentWithSkills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, v as REVIEW, x as coder, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Cgca5Gp-.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { bt as envDefaults, i as skills, lt as reviewGateBlockedMessage, n as summary, r as access, s as shipped, t as steering, u as gate, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Cgca5Gp-.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Et as requireFlag, St as flagMentions, bt as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flipProblems, x as coder, yt as defaults } from "../chunks/reserved-sfMdy7v7.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|