@pmelab/gtd 52.0.0 → 53.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 +96 -131
- package/claude/hooks/drive.ts +0 -3
- package/claude/hooks/register.tsx +3 -68
- package/claude/hooks/ship.ts +85 -1595
- package/dist/chunks/{reserved-D0MKVFFa.mjs → reserved-CQ7amcKG.mjs} +87 -133
- package/dist/chunks/{review-ZzUQH1KZ.mjs → review-CNgN_V8T.mjs} +2 -2
- package/dist/chunks/{tail-I1-vVq0P.mjs → tail-JZ9PAOwp.mjs} +2 -14
- package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
- package/dist/flows.mjs +1 -16
- package/dist/gtd.bundle.mjs +476 -982
- package/dist/testkit.mjs +58 -417
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +159 -363
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +400 -675
- package/package.json +5 -4
- package/src/cli/Cli.test.ts +1 -86
- package/src/cli/Cli.ts +13 -133
- package/src/cli/Metrics.test.ts +21 -31
- package/src/cli/Metrics.ts +25 -36
- package/src/cli/Program.test.ts +1 -35
- package/src/cli/Program.ts +5 -64
- package/src/cli/ToolCommands.ts +0 -3
- package/src/edge/Edge.test.ts +20 -322
- package/src/edge/Edge.ts +10 -181
- package/src/edge/index.ts +0 -1
- package/src/flows/runtime.test.ts +0 -27
- package/src/flows/runtime.ts +1 -49
- package/src/install/Install.ts +15 -24
- package/src/replay/Replay.test.ts +0 -169
- package/src/replay/Replay.ts +4 -93
- package/src/replay/index.ts +0 -1
- package/src/testkit/Layers.ts +0 -1
- package/src/testkit/steps.ts +13 -0
- package/src/wire/BeatDocument.ts +2 -12
- package/src/wire/BeatStatus.ts +0 -3
- package/src/wire/WireSchema.test.ts +0 -18
- package/src/wire/index.ts +0 -2
- package/src/wire/types.ts +0 -7
- package/src/workflow/Workflow.ts +1 -9
- package/src/workflows/feature/acceptance-criteria.feature +159 -3
- package/src/workflows/feature/criteria.test.ts +23 -15
- package/src/workflows/feature/criteria.ts +72 -39
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +6 -5
- package/src/workflows/feature/feature.ts +34 -118
- package/src/workflows/feature/flag-mode.feature +4 -0
- package/src/workflows/feature/hands-on-skip.feature +37 -65
- package/src/workflows/feature/index.ts +0 -1
- package/src/workflows/feature/lens-models.feature +4 -0
- package/src/workflows/feature/lens-review.feature +4 -0
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/one-step-land.feature +4 -4
- package/src/workflows/feature/planning.ts +2 -1
- package/src/workflows/feature/prototype-rounds.feature +5 -1
- package/src/workflows/feature/prototype.feature +11 -3
- package/src/workflows/feature/prototype.ts +18 -29
- package/src/workflows/feature/refactor-absorbs.feature +7 -8
- package/src/workflows/feature/refactor-first.feature +4 -5
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +4 -6
- package/src/workflows/feature/refactor-review.feature +31 -11
- package/src/workflows/feature/refactor.ts +30 -64
- package/src/workflows/feature/review-answers.feature +4 -0
- package/src/workflows/feature/review-gate.feature +13 -1
- package/src/workflows/feature/reviewer-walkthrough.feature +4 -0
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -4
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.test.ts +11 -28
- package/src/workflows/feature/seam.ts +52 -165
- package/src/workflows/feature/severity-budget.feature +4 -0
- package/src/workflows/feature/severity-sorted-review.feature +4 -0
- package/src/workflows/feature/standards.feature +4 -0
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +0 -13
- package/src/workflows/shared/options.test.ts +2 -11
- package/src/workflows/shared/options.ts +4 -21
- package/src/workflows/shared/review.test.ts +3 -3
- package/src/workflows/shared/steps.ts +0 -2
- package/src/workflows/shared/text.test.ts +41 -71
- package/src/workflows/shared/text.ts +82 -212
- package/src/workflows/shared/vars.ts +2 -8
- package/src/workflows/shared/workflows.steps.ts +0 -13
- package/claude/hooks/context.ts +0 -66
- package/src/replay/Slices.test.ts +0 -73
- package/src/replay/Slices.ts +0 -45
- package/src/wire/ContextLimit.fixture.ts +0 -30
- package/src/wire/ContextLimit.test.ts +0 -96
- package/src/wire/ContextLimit.ts +0 -169
- package/src/wire/Slices.ts +0 -25
- package/src/wire/golden/1/slices.json +0 -11
- package/src/workflows/feature/context-limit.feature +0 -37
- package/src/workflows/feature/continuation-cap.feature +0 -65
- package/src/workflows/feature/continuation-rebuild.feature +0 -88
- package/src/workflows/feature/continuation-slices.feature +0 -64
- package/src/workflows/feature/continuation.feature +0 -205
- package/src/workflows/feature/slice-findings.feature +0 -252
- package/src/workflows/feature/slicing-cap.feature +0 -23
- package/src/workflows/feature/slicing.feature +0 -265
- package/src/workflows/feature/slicing.test.ts +0 -49
- package/src/workflows/feature/slicing.ts +0 -105
- package/src/workflows/shared/continuation.ts +0 -92
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope,
|
|
1
|
+
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff,
|
|
5
|
-
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-
|
|
3
|
+
import { $ as observableMessage, B as designGateAnswerMessage, Ct as testerSystem, D as planner, Dt as unwindFailure, Et as unwindFailedMessage, H as designSystem, I as clearCriteriaMessage, J as implementBuildingPrompt, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, Mt as envDefaults, Nt as testFileGlobs, P as builderSystem, Q as normaliseCriteriaScript, S as collecting, St as startGateBlockedMessage, U as designTriagePrompt, V as designImpactPrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, a as flagModeOn, bt as seamByHandMessage, ct as refactorBuildingPrompt, d as healthy, dt as refactorNotePrompt, et as observableQuestion, f as FEEDBACK, ft as refactorRules, g as REFACTOR_NOTE, h as REFACTOR, i as skills, it as prototypeUnguidedMessage, j as agentWithSkills, jt as defaults, l as gate, lt as refactorDroppedRefusal, n as summary, nt as prototypeOpenMessage, p as PROTOTYPE, q as idleMessage, r as access, rt as prototypeTestingPrompt, s as shipped, t as steering, tt as prototypeFixingPrompt, ut as refactorFixPrompt, w as fixNits, x as coder, xt as seamSteerMessage, y as answerReviewQuestions, yt as reviewerSystem, z as criterionLine } from "../chunks/reserved-CQ7amcKG.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, o as tail } from "../chunks/tail-JZ9PAOwp.mjs";
|
|
5
|
+
import { i as reUnwind, n as humanReview, r as isCode$1, t as buildTail } from "../chunks/review-CNgN_V8T.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
|
@@ -26,7 +26,7 @@ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
|
26
26
|
//#region src/workflows/feature/criteria.ts
|
|
27
27
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
28
28
|
const CLEAR_MIN_P = .8;
|
|
29
|
-
const ID
|
|
29
|
+
const ID = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/;
|
|
30
30
|
const criterion = (id, as = id[1]) => ({
|
|
31
31
|
id: as,
|
|
32
32
|
text: id[3],
|
|
@@ -59,24 +59,22 @@ const listUnder = (text, title, level) => {
|
|
|
59
59
|
};
|
|
60
60
|
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
61
61
|
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
62
|
-
const id = ID
|
|
62
|
+
const id = ID.exec(item);
|
|
63
63
|
return id === null ? {
|
|
64
64
|
id: "",
|
|
65
65
|
text: item
|
|
66
66
|
} : criterion(id);
|
|
67
67
|
});
|
|
68
|
-
/** The ID'd criteria of `text
|
|
69
|
-
const criterionIds = (text
|
|
70
|
-
/** The ID'd criteria of `text`
|
|
71
|
-
const handsOnIds = (text
|
|
68
|
+
/** The ID'd criteria of `text`. */
|
|
69
|
+
const criterionIds = (text) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
70
|
+
/** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
|
|
71
|
+
const handsOnIds = (text) => {
|
|
72
72
|
const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id);
|
|
73
|
-
return criterionIds(text
|
|
73
|
+
return criterionIds(text).filter((id) => !tagged.includes(id));
|
|
74
74
|
};
|
|
75
|
-
/** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
|
|
76
|
-
const sliceIds = (slice) => slice === void 0 ? void 0 : criterionIds(read(".gtd/REQUIREMENTS.md") ?? "", slice);
|
|
77
75
|
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
78
76
|
const numbered = (items) => {
|
|
79
|
-
const ids = items.map((item) => ID
|
|
77
|
+
const ids = items.map((item) => ID.exec(item));
|
|
80
78
|
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
81
79
|
return items.map((item, i) => {
|
|
82
80
|
const id = ids[i];
|
|
@@ -121,6 +119,38 @@ const requireCriteria = (file) => {
|
|
|
121
119
|
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
122
120
|
refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
|
|
123
121
|
};
|
|
122
|
+
const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
|
|
123
|
+
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
124
|
+
const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
|
|
125
|
+
/** `text` without its `## Impact` section, whitespace collapsed. */
|
|
126
|
+
const withoutImpact = (text) => {
|
|
127
|
+
let inImpact = false;
|
|
128
|
+
return markFenced(text).filter((l) => {
|
|
129
|
+
const h = l.fenced ? null : HEADING.exec(l.line);
|
|
130
|
+
if (h !== null && h[1].length <= 2) inImpact = isImpactHeading(h);
|
|
131
|
+
return !inImpact;
|
|
132
|
+
}).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Refuse a turn that leaves `file` without a `## Impact` section. Only the
|
|
136
|
+
* pending turn (`head()` is `""`): a triage lap recorded before Impact existed
|
|
137
|
+
* replays without one, and refusing there would fail every in-flight process.
|
|
138
|
+
* A human's edit at a later gate is not checked — the human owns that file then.
|
|
139
|
+
*/
|
|
140
|
+
const requireImpact = (file) => {
|
|
141
|
+
if (head() !== "") return;
|
|
142
|
+
const text = read(file);
|
|
143
|
+
if (text === void 0) return;
|
|
144
|
+
if (hasImpact(text)) return;
|
|
145
|
+
refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
|
|
146
|
+
};
|
|
147
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` section. */
|
|
148
|
+
const requireImpactOnly = (file) => {
|
|
149
|
+
const change = changes().get(file);
|
|
150
|
+
if (change === void 0) return;
|
|
151
|
+
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
|
|
152
|
+
refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
|
|
153
|
+
};
|
|
124
154
|
const idNumber = (id) => Number(id.slice(3));
|
|
125
155
|
/** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
|
|
126
156
|
const unstableIds = (listed, ids) => {
|
|
@@ -154,6 +184,19 @@ const clear = async (stated) => {
|
|
|
154
184
|
});
|
|
155
185
|
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
156
186
|
};
|
|
187
|
+
/** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
|
|
188
|
+
const impact = async () => {
|
|
189
|
+
await agentWithSkills("impact", designImpactPrompt(), {
|
|
190
|
+
label: "Mapping the change's impact",
|
|
191
|
+
file: REQUIREMENTS,
|
|
192
|
+
model: planner(),
|
|
193
|
+
system: designSystem()
|
|
194
|
+
});
|
|
195
|
+
requireProgress(REQUIREMENTS);
|
|
196
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
197
|
+
requireImpact(REQUIREMENTS);
|
|
198
|
+
requireImpactOnly(REQUIREMENTS);
|
|
199
|
+
};
|
|
157
200
|
/**
|
|
158
201
|
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
159
202
|
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
@@ -165,15 +208,9 @@ const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scop
|
|
|
165
208
|
label: "Recording the stated acceptance criteria",
|
|
166
209
|
file: REQUIREMENTS
|
|
167
210
|
});
|
|
211
|
+
await impact();
|
|
168
212
|
return true;
|
|
169
213
|
});
|
|
170
|
-
/** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
|
|
171
|
-
const requireCriteriaIfEdited = () => {
|
|
172
|
-
const change = changes().get(REQUIREMENTS);
|
|
173
|
-
if (change === void 0) return;
|
|
174
|
-
if (change.status === "deleted") refuse(`gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`);
|
|
175
|
-
requireCriteria(REQUIREMENTS);
|
|
176
|
-
};
|
|
177
214
|
//#endregion
|
|
178
215
|
//#region src/workflows/feature/tier.ts
|
|
179
216
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
@@ -261,6 +298,7 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
261
298
|
requireProgress(REQUIREMENTS);
|
|
262
299
|
requireReplies(REQUIREMENTS);
|
|
263
300
|
requireCriteria(REQUIREMENTS);
|
|
301
|
+
requireImpact(REQUIREMENTS);
|
|
264
302
|
guard?.();
|
|
265
303
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
266
304
|
});
|
|
@@ -338,12 +376,12 @@ const requireReport = (ids, retest) => {
|
|
|
338
376
|
};
|
|
339
377
|
};
|
|
340
378
|
/**
|
|
341
|
-
* The
|
|
342
|
-
*
|
|
379
|
+
* The criteria a person can observe by using the change, by the judge: those
|
|
380
|
+
* tagged `(no hands-on)` are never asked about.
|
|
343
381
|
*/
|
|
344
|
-
const observable = async (
|
|
382
|
+
const observable = async () => {
|
|
345
383
|
const requirements = read(".gtd/REQUIREMENTS.md") ?? "";
|
|
346
|
-
const own = handsOnIds(requirements
|
|
384
|
+
const own = handsOnIds(requirements);
|
|
347
385
|
const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id));
|
|
348
386
|
if (criteria.length === 0) return [];
|
|
349
387
|
const { answers } = await judge("observable", {
|
|
@@ -355,10 +393,9 @@ const observable = async (slice) => {
|
|
|
355
393
|
return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P));
|
|
356
394
|
};
|
|
357
395
|
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
358
|
-
const testing = async (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
label: retest ? "Re-testing the slice by hand" : "Testing the slice by hand",
|
|
396
|
+
const testing = async (ids, retest) => {
|
|
397
|
+
await agentWithSkills("testing", prototypeTestingPrompt(criterionIds(read(".gtd/REQUIREMENTS.md") ?? "").filter((id) => !ids.includes(id)), retest), {
|
|
398
|
+
label: retest ? "Re-testing the change by hand" : "Testing the change by hand",
|
|
362
399
|
file: PROTOTYPE,
|
|
363
400
|
model: coder(),
|
|
364
401
|
system: testerSystem(),
|
|
@@ -366,8 +403,8 @@ const testing = async (slice, ids, retest) => {
|
|
|
366
403
|
});
|
|
367
404
|
return requireReport(ids, retest);
|
|
368
405
|
};
|
|
369
|
-
const fixing = async (
|
|
370
|
-
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(
|
|
406
|
+
const fixing = async (guard) => {
|
|
407
|
+
await scope("fix", () => agentWithSkills("fixing", prototypeFixingPrompt(), {
|
|
371
408
|
label: "Fixing what the hands-on test found",
|
|
372
409
|
file: PROTOTYPE,
|
|
373
410
|
model: coder(),
|
|
@@ -391,29 +428,29 @@ const askPerson = async (name, message, label) => {
|
|
|
391
428
|
return true;
|
|
392
429
|
};
|
|
393
430
|
/**
|
|
394
|
-
* Use the
|
|
395
|
-
* can observe, a fix turn takes each failure, and the
|
|
431
|
+
* Use the change by hand: a testing turn reports on every criterion a person
|
|
432
|
+
* can observe, a fix turn takes each failure, and the change is tested again —
|
|
396
433
|
* until the report is clean, or `prototypeRounds` fixes are spent and a person
|
|
397
|
-
* decides. A
|
|
398
|
-
* every fix turn, as it holds the implementation's.
|
|
434
|
+
* decides. A change with no such criterion is not tested by hand. `guard`
|
|
435
|
+
* holds every fix turn, as it holds the implementation's.
|
|
399
436
|
*/
|
|
400
|
-
const prototypeTest = async (
|
|
437
|
+
const prototypeTest = async (guard) => {
|
|
401
438
|
const cap = prototypeRounds();
|
|
402
439
|
if (cap === 0) return;
|
|
403
440
|
await scope("prototype", async () => {
|
|
404
|
-
const ids = await observable(
|
|
441
|
+
const ids = await observable();
|
|
405
442
|
if (ids.length === 0) return;
|
|
406
443
|
let fixes = 0;
|
|
407
444
|
let retest = false;
|
|
408
445
|
for (;;) {
|
|
409
|
-
const { unguided, open } = await testing(
|
|
446
|
+
const { unguided, open } = await testing(ids, retest);
|
|
410
447
|
retest = !unguided;
|
|
411
448
|
if (unguided) {
|
|
412
449
|
if (!await askPerson("unguided", prototypeUnguidedMessage(), "No way to test by hand")) return;
|
|
413
450
|
} else if (open.length === 0) return;
|
|
414
451
|
else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
|
|
415
452
|
fixes++;
|
|
416
|
-
await fixing(
|
|
453
|
+
await fixing(guard);
|
|
417
454
|
} else {
|
|
418
455
|
if (!await askPerson("open", prototypeOpenMessage(open, fixes), "Hands-on test still failing")) return;
|
|
419
456
|
fixes = 0;
|
|
@@ -423,53 +460,6 @@ const prototypeTest = async (slice, guard) => {
|
|
|
423
460
|
});
|
|
424
461
|
};
|
|
425
462
|
//#endregion
|
|
426
|
-
//#region src/workflows/shared/continuation.ts
|
|
427
|
-
const phase = (name, steered) => ({
|
|
428
|
-
name,
|
|
429
|
-
spent: false,
|
|
430
|
-
count: 0,
|
|
431
|
-
stalled: 0,
|
|
432
|
-
steered
|
|
433
|
-
});
|
|
434
|
-
const own = (fn) => fn();
|
|
435
|
-
const continued = (fn) => scope("continue", fn);
|
|
436
|
-
const afresh = (fn) => scope("fresh", fn);
|
|
437
|
-
const setting = (name, least) => {
|
|
438
|
-
const raw = vars[name] ?? defaults[name];
|
|
439
|
-
const n = Number(raw);
|
|
440
|
-
if (raw.trim() === "" || !Number.isInteger(n) || n < least) refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`);
|
|
441
|
-
return n;
|
|
442
|
-
};
|
|
443
|
-
/**
|
|
444
|
-
* Run `turn` until one ends without a handoff note: each stopped turn is picked
|
|
445
|
-
* up by a continuation in a fresh conversation. Past `continuations` of them in
|
|
446
|
-
* the phase, or after `continuationStall` in a row that close nothing still
|
|
447
|
-
* open, the phase rests at `oversized` for a person; landing there grants a new round.
|
|
448
|
-
*/
|
|
449
|
-
const continuing = async (phase, turn) => {
|
|
450
|
-
const cap = setting("continuations", 0);
|
|
451
|
-
const stall = setting("continuationStall", 1);
|
|
452
|
-
let last = await turn(void 0, phase.spent ? afresh : own);
|
|
453
|
-
while (last.handoff !== void 0) {
|
|
454
|
-
phase.spent = true;
|
|
455
|
-
const from = last.handoff;
|
|
456
|
-
if (phase.count >= cap || phase.stalled >= stall) {
|
|
457
|
-
await human("oversized", {
|
|
458
|
-
message: oversizedMessage(phase.name, from, phase.count >= cap ? { cap } : { stall }, phase.steered !== void 0),
|
|
459
|
-
label: "Too big for its context",
|
|
460
|
-
acceptClean: true
|
|
461
|
-
});
|
|
462
|
-
phase.steered?.();
|
|
463
|
-
phase.count = 0;
|
|
464
|
-
phase.stalled = 0;
|
|
465
|
-
}
|
|
466
|
-
last = await turn(from, continued);
|
|
467
|
-
phase.count++;
|
|
468
|
-
phase.stalled = from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item)) ? 0 : phase.stalled + 1;
|
|
469
|
-
}
|
|
470
|
-
return last.result;
|
|
471
|
-
};
|
|
472
|
-
//#endregion
|
|
473
463
|
//#region src/workflows/feature/refactor.ts
|
|
474
464
|
const REFACTOR_PACKAGE = REFACTOR;
|
|
475
465
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
@@ -615,7 +605,7 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
615
605
|
requireNoNewFlags(pattern, entry);
|
|
616
606
|
};
|
|
617
607
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
618
|
-
const refactorLap = async (p, guard,
|
|
608
|
+
const refactorLap = async (p, guard, feedback) => {
|
|
619
609
|
const { pattern, strays, reference, note } = p;
|
|
620
610
|
await run("deriving", deriveScript([
|
|
621
611
|
...packageLines(strays, pattern, reference),
|
|
@@ -635,48 +625,32 @@ const refactorLap = async (p, guard, refactor, feedback) => {
|
|
|
635
625
|
label: "Deriving the refactor package",
|
|
636
626
|
file: REFACTOR_PACKAGE
|
|
637
627
|
});
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
628
|
+
await agentWithSkills("building", refactorBuildingPrompt({
|
|
629
|
+
pkg: REFACTOR_PACKAGE,
|
|
630
|
+
strays,
|
|
631
|
+
pattern,
|
|
632
|
+
reference,
|
|
633
|
+
feedback: feedback !== void 0,
|
|
634
|
+
note: note !== void 0
|
|
635
|
+
}), {
|
|
636
|
+
label: "Refactoring",
|
|
637
|
+
model: coder(),
|
|
638
|
+
system: builderSystem()
|
|
639
|
+
});
|
|
640
|
+
guard();
|
|
641
|
+
await healthy(async () => {
|
|
642
|
+
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
643
|
+
label: "Fixing the check",
|
|
644
|
+
file: FEEDBACK,
|
|
650
645
|
model: coder(),
|
|
651
646
|
system: builderSystem()
|
|
652
|
-
}));
|
|
653
|
-
const left = read(HANDOFF);
|
|
654
|
-
guard();
|
|
655
|
-
const snapshot = head();
|
|
656
|
-
for (const c of changes()) reached.add(c.path);
|
|
657
|
-
await healthy(async () => {
|
|
658
|
-
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
659
|
-
label: "Fixing the check",
|
|
660
|
-
file: FEEDBACK,
|
|
661
|
-
model: coder(),
|
|
662
|
-
system: builderSystem()
|
|
663
|
-
});
|
|
664
|
-
guard();
|
|
665
|
-
}, {
|
|
666
|
-
describeGuard: guard,
|
|
667
|
-
rules: refactorRules(strays, pattern)
|
|
668
647
|
});
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
snapshot,
|
|
674
|
-
note: left.trim(),
|
|
675
|
-
open: strays.filter((path) => !reached.has(path))
|
|
676
|
-
}
|
|
677
|
-
};
|
|
648
|
+
guard();
|
|
649
|
+
}, {
|
|
650
|
+
describeGuard: guard,
|
|
651
|
+
rules: refactorRules(strays, pattern)
|
|
678
652
|
});
|
|
679
|
-
await run("closing", removeScript([REFACTOR_PACKAGE
|
|
653
|
+
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
680
654
|
};
|
|
681
655
|
const strayAccess = (strays) => ({ write: [
|
|
682
656
|
...strays,
|
|
@@ -690,7 +664,7 @@ const refactorFirst = async (shape, options = {}) => {
|
|
|
690
664
|
name: "refactor",
|
|
691
665
|
access: strayAccess(p.strays)
|
|
692
666
|
}, async () => {
|
|
693
|
-
await refactorLap(p, guardOf(p, head())
|
|
667
|
+
await refactorLap(p, guardOf(p, head()));
|
|
694
668
|
return true;
|
|
695
669
|
});
|
|
696
670
|
};
|
|
@@ -715,10 +689,9 @@ const reviewedRefactor = async (shape, options = {}) => {
|
|
|
715
689
|
unmatched: "gtd land: refactor-feedback: the review feedback changed files but wrote no .gtd/REFACTOR.md — a refactor cannot act on it; revert the change, or `gtd abandon` and restart",
|
|
716
690
|
dropped: refactorDroppedRefusal
|
|
717
691
|
};
|
|
718
|
-
const refactor = phase("refactor");
|
|
719
692
|
let collected;
|
|
720
693
|
for (;;) {
|
|
721
|
-
await refactorLap(p, guard,
|
|
694
|
+
await refactorLap(p, guard, collected);
|
|
722
695
|
const outcome = await buildTail(entry, {
|
|
723
696
|
green: true,
|
|
724
697
|
guard,
|
|
@@ -748,32 +721,12 @@ const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
|
748
721
|
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
749
722
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
750
723
|
});
|
|
751
|
-
const naivePass = (retry
|
|
752
|
-
label:
|
|
724
|
+
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
725
|
+
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
753
726
|
keep: true,
|
|
754
727
|
model: coder(),
|
|
755
728
|
system: builderSystem()
|
|
756
729
|
});
|
|
757
|
-
const handoffOf = (tip, slice) => {
|
|
758
|
-
const note = tip.changes.find((c) => c.path === ".gtd/HANDOFF.md" && c.after !== void 0);
|
|
759
|
-
if (note === void 0) return void 0;
|
|
760
|
-
return {
|
|
761
|
-
snapshot: tip.hash,
|
|
762
|
-
note: note.after.trim(),
|
|
763
|
-
open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes))
|
|
764
|
-
};
|
|
765
|
-
};
|
|
766
|
-
/** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
|
|
767
|
-
const naiveContinued = (retry, slice, naive) => continuing(naive, async (from, fresh) => {
|
|
768
|
-
const tip = await fresh(async () => {
|
|
769
|
-
await naivePass(retry, slice, from);
|
|
770
|
-
return kept("naive");
|
|
771
|
-
});
|
|
772
|
-
return {
|
|
773
|
-
result: tip,
|
|
774
|
-
handoff: handoffOf(tip, slice)
|
|
775
|
-
};
|
|
776
|
-
});
|
|
777
730
|
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
778
731
|
const EXTENSION = /\.[\w-]+$/;
|
|
779
732
|
const ALIAS = /^(?:[@~]\/|#)/;
|
|
@@ -809,7 +762,7 @@ const addedLines = (change) => {
|
|
|
809
762
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
810
763
|
};
|
|
811
764
|
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
812
|
-
const idsOf = (file
|
|
765
|
+
const idsOf = (file) => criterionIds(read(file) ?? "");
|
|
813
766
|
const unnamed = (ids, tests) => {
|
|
814
767
|
const lines = tests.map(addedLines);
|
|
815
768
|
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
@@ -819,9 +772,9 @@ const unnamed = (ids, tests) => {
|
|
|
819
772
|
* test line of the pass names, or a test importing a file the pass added —
|
|
820
773
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
821
774
|
*/
|
|
822
|
-
const acceptanceFault = (file, tip
|
|
775
|
+
const acceptanceFault = (file, tip) => {
|
|
823
776
|
const tests = acceptanceTests(tip.changes);
|
|
824
|
-
const untested = unnamed(idsOf(file
|
|
777
|
+
const untested = unnamed(idsOf(file), tests);
|
|
825
778
|
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]}] …"`;
|
|
826
779
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
827
780
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -831,8 +784,8 @@ const acceptanceFault = (file, tip, slice) => {
|
|
|
831
784
|
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")}`;
|
|
832
785
|
};
|
|
833
786
|
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
834
|
-
const naiveAcceptance = (tip
|
|
835
|
-
const ids = idsOf(REQUIREMENTS
|
|
787
|
+
const naiveAcceptance = (tip) => {
|
|
788
|
+
const ids = idsOf(REQUIREMENTS);
|
|
836
789
|
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
837
790
|
const lines = addedLines(c);
|
|
838
791
|
return {
|
|
@@ -902,31 +855,19 @@ const applied = async (tip) => {
|
|
|
902
855
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
903
856
|
};
|
|
904
857
|
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
905
|
-
const settled = (carried
|
|
906
|
-
const listed = idsOf(REQUIREMENTS
|
|
907
|
-
if (carried === void 0) return listed;
|
|
858
|
+
const settled = (carried) => {
|
|
859
|
+
const listed = idsOf(REQUIREMENTS);
|
|
908
860
|
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id));
|
|
909
861
|
};
|
|
910
|
-
const build =
|
|
911
|
-
await
|
|
912
|
-
label:
|
|
862
|
+
const build = async (options) => {
|
|
863
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
|
|
864
|
+
label: "Implementing",
|
|
913
865
|
model: coder(),
|
|
914
866
|
system: builderSystem(),
|
|
915
867
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
916
|
-
})
|
|
868
|
+
});
|
|
917
869
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
918
|
-
|
|
919
|
-
if (note === void 0) return { result: void 0 };
|
|
920
|
-
const ids = settled(options.rebuild?.acceptance?.ids, options.slice);
|
|
921
|
-
return {
|
|
922
|
-
result: void 0,
|
|
923
|
-
handoff: {
|
|
924
|
-
snapshot: head(),
|
|
925
|
-
note: note.trim(),
|
|
926
|
-
open: unnamed(ids, acceptanceTests(changesSince(start())))
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
});
|
|
870
|
+
};
|
|
930
871
|
async function implement(first, tier, amend) {
|
|
931
872
|
await implementing(typeof first === "object" && first !== null ? first : {
|
|
932
873
|
reference: first,
|
|
@@ -943,30 +884,19 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
943
884
|
else requireCriteriaFrozen(REQUIREMENTS);
|
|
944
885
|
if (built && acceptance !== void 0) requireAcceptance({
|
|
945
886
|
...acceptance,
|
|
946
|
-
ids: settled(acceptance.ids
|
|
887
|
+
ids: settled(acceptance.ids)
|
|
947
888
|
}, since);
|
|
948
889
|
const keepsTests = built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0;
|
|
949
890
|
await green(keepsTests);
|
|
950
|
-
await prototypeTest(
|
|
891
|
+
await prototypeTest(keepsTests);
|
|
951
892
|
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
952
|
-
await closing(
|
|
893
|
+
await run("closing", removeScript([
|
|
894
|
+
REQUIREMENTS,
|
|
895
|
+
REFACTOR_NOTE,
|
|
896
|
+
PROTOTYPE
|
|
897
|
+
]), { label: "Closing out the implementation" });
|
|
953
898
|
return ids;
|
|
954
899
|
});
|
|
955
|
-
const moreToBuild = (more) => typeof more === "function" ? more() : more === true;
|
|
956
|
-
/** The last slice's closing, for a last slice a steer left with no criterion to build. */
|
|
957
|
-
const closeOut = () => scope("implement", () => closing(false));
|
|
958
|
-
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
959
|
-
const closing = (more) => run("closing", removeScript(more ? [
|
|
960
|
-
REFACTOR_NOTE,
|
|
961
|
-
HANDOFF,
|
|
962
|
-
PROTOTYPE
|
|
963
|
-
] : [
|
|
964
|
-
REQUIREMENTS,
|
|
965
|
-
REFACTOR_NOTE,
|
|
966
|
-
SLICES,
|
|
967
|
-
HANDOFF,
|
|
968
|
-
PROTOTYPE
|
|
969
|
-
]), { label: "Closing out the implementation" });
|
|
970
900
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
971
901
|
const steer = async (retry) => {
|
|
972
902
|
await human("steer", {
|
|
@@ -997,8 +927,8 @@ const budget = () => {
|
|
|
997
927
|
};
|
|
998
928
|
};
|
|
999
929
|
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
1000
|
-
const vetted = async (tip, vetting
|
|
1001
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip
|
|
930
|
+
const vetted = async (tip, vetting) => {
|
|
931
|
+
const fault = vetting.waived ? void 0 : acceptanceFault(REQUIREMENTS, tip);
|
|
1002
932
|
if (fault !== void 0) {
|
|
1003
933
|
const verdict = await vetting.again({
|
|
1004
934
|
fault,
|
|
@@ -1032,16 +962,16 @@ const ruled = async (tip, again) => {
|
|
|
1032
962
|
tip
|
|
1033
963
|
} : next;
|
|
1034
964
|
};
|
|
1035
|
-
const decide = (
|
|
965
|
+
const decide = () => scope("seam", async () => {
|
|
1036
966
|
const vetting = {
|
|
1037
967
|
again: budget(),
|
|
1038
968
|
waived: false
|
|
1039
969
|
};
|
|
1040
|
-
const naive = phase("naive pass", requireCriteriaIfEdited);
|
|
1041
970
|
let retry;
|
|
1042
971
|
for (;;) {
|
|
1043
|
-
|
|
1044
|
-
const
|
|
972
|
+
await naivePass(retry);
|
|
973
|
+
const tip = kept("naive");
|
|
974
|
+
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
1045
975
|
reference: tip.hash,
|
|
1046
976
|
tip
|
|
1047
977
|
});
|
|
@@ -1062,8 +992,8 @@ const refactorNote = async (reference, strays) => {
|
|
|
1062
992
|
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")}` : ""}`);
|
|
1063
993
|
return note;
|
|
1064
994
|
};
|
|
1065
|
-
const seamBuild = async (base
|
|
1066
|
-
const { reference, tip, shape } = await decide(
|
|
995
|
+
const seamBuild = async (base) => {
|
|
996
|
+
const { reference, tip, shape } = await decide();
|
|
1067
997
|
if (shape === void 0) return {
|
|
1068
998
|
reference,
|
|
1069
999
|
base,
|
|
@@ -1078,107 +1008,31 @@ const seamBuild = async (base, slice$1 = {}) => {
|
|
|
1078
1008
|
return {
|
|
1079
1009
|
reference,
|
|
1080
1010
|
base,
|
|
1081
|
-
rebuild: { acceptance: naiveAcceptance(tip
|
|
1011
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
1082
1012
|
};
|
|
1083
1013
|
}
|
|
1084
1014
|
const strays = refactorStrays(shape);
|
|
1085
1015
|
if (strays.length === 0) return {
|
|
1086
1016
|
reference,
|
|
1087
1017
|
base,
|
|
1088
|
-
rebuild: { acceptance: naiveAcceptance(tip
|
|
1018
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
1089
1019
|
};
|
|
1090
1020
|
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
1091
|
-
const refactored = await
|
|
1021
|
+
const refactored = await reviewedRefactor(shape, {
|
|
1092
1022
|
reference,
|
|
1093
1023
|
note
|
|
1094
|
-
})
|
|
1024
|
+
});
|
|
1095
1025
|
return {
|
|
1096
1026
|
reference,
|
|
1097
1027
|
base: head(),
|
|
1098
1028
|
refactored,
|
|
1099
1029
|
rebuild: {
|
|
1100
1030
|
note,
|
|
1101
|
-
acceptance: naiveAcceptance(tip
|
|
1031
|
+
acceptance: naiveAcceptance(tip)
|
|
1102
1032
|
}
|
|
1103
1033
|
};
|
|
1104
1034
|
};
|
|
1105
1035
|
//#endregion
|
|
1106
|
-
//#region src/workflows/feature/slicing.ts
|
|
1107
|
-
const ID = /^(AC-\d+)(?![\w-])/;
|
|
1108
|
-
/** The `sliceMaxCriteria` setting. Anything but a positive integer fails rather than guess a cap. */
|
|
1109
|
-
const sliceCap = () => {
|
|
1110
|
-
const value = vars.sliceMaxCriteria ?? defaults.sliceMaxCriteria;
|
|
1111
|
-
if (!/^[1-9]\d*$/.test(value)) refuse(`gtd: sliceMaxCriteria must be a positive integer, got ${JSON.stringify(value)}`);
|
|
1112
|
-
return Number(value);
|
|
1113
|
-
};
|
|
1114
|
-
/** Each `## ` heading of `text` with the list items under it; an item not led by an ID keeps an empty one. */
|
|
1115
|
-
const slicesOf = (text) => {
|
|
1116
|
-
const slices = [];
|
|
1117
|
-
let open = false;
|
|
1118
|
-
for (const { line, fenced } of markFenced(text)) {
|
|
1119
|
-
if (fenced) continue;
|
|
1120
|
-
const heading = HEADING.exec(line);
|
|
1121
|
-
if (heading !== null) {
|
|
1122
|
-
open = heading[1] === "##";
|
|
1123
|
-
if (open) slices.push({
|
|
1124
|
-
title: heading[2],
|
|
1125
|
-
ids: []
|
|
1126
|
-
});
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
const item = ITEM.exec(line);
|
|
1130
|
-
if (item !== null && open) slices.at(-1).ids.push(ID.exec(item[1])?.[1] ?? "");
|
|
1131
|
-
}
|
|
1132
|
-
return slices;
|
|
1133
|
-
};
|
|
1134
|
-
const twice = (list) => [...new Set(list.filter((id, i) => list.indexOf(id) !== i))];
|
|
1135
|
-
/** Why `slices` fail to put each of `ids` in exactly one slice of at most `cap`, or `undefined`. */
|
|
1136
|
-
const sliceFault = (slices, ids, cap) => {
|
|
1137
|
-
const listed = slices.flatMap((s) => s.ids);
|
|
1138
|
-
if (slices.length === 0) return `no "## " slice`;
|
|
1139
|
-
const empty = slices.filter((s) => s.ids.length === 0).map((s) => s.title);
|
|
1140
|
-
if (empty.length > 0) return `a slice with no criterion: ${empty.join(", ")}`;
|
|
1141
|
-
if (listed.includes("")) return "a list item not led by a criterion ID";
|
|
1142
|
-
const over = slices.filter((s) => s.ids.length > cap).map((s) => s.title);
|
|
1143
|
-
if (over.length > 0) return `more than ${cap} criteria in: ${over.join(", ")}`;
|
|
1144
|
-
const repeated = twice(listed);
|
|
1145
|
-
if (repeated.length > 0) return `an ID in more than one slice: ${repeated.join(", ")}`;
|
|
1146
|
-
const unknown = listed.filter((id) => !ids.includes(id));
|
|
1147
|
-
if (unknown.length > 0) return `an ID ${REQUIREMENTS} does not list: ${unknown.join(", ")}`;
|
|
1148
|
-
const missing = ids.filter((id) => !listed.includes(id));
|
|
1149
|
-
if (missing.length > 0) return `no slice for: ${missing.join(", ")}`;
|
|
1150
|
-
};
|
|
1151
|
-
/** Refuse a turn whose `.gtd/SLICES.md` does not put each of `ids` in exactly one slice of at most `cap`. */
|
|
1152
|
-
const requireSlices = (ids, cap) => {
|
|
1153
|
-
const slices = slicesOf(read(".gtd/SLICES.md") ?? "");
|
|
1154
|
-
const why = sliceFault(slices, ids, cap);
|
|
1155
|
-
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`);
|
|
1156
|
-
return slices;
|
|
1157
|
-
};
|
|
1158
|
-
/**
|
|
1159
|
-
* The slices to build, in order. Criteria within `sliceMaxCriteria` are one
|
|
1160
|
-
* slice and no turn; above it `design.slicing` groups them.
|
|
1161
|
-
*/
|
|
1162
|
-
const slicing = async () => {
|
|
1163
|
-
const all = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
1164
|
-
const cap = sliceCap();
|
|
1165
|
-
if (all.length <= cap) return [{
|
|
1166
|
-
title: "",
|
|
1167
|
-
ids: all
|
|
1168
|
-
}];
|
|
1169
|
-
return scope("design", async () => {
|
|
1170
|
-
await agentWithSkills("slicing", slicingPrompt(cap), {
|
|
1171
|
-
label: "Slicing the change",
|
|
1172
|
-
file: SLICES,
|
|
1173
|
-
model: planner(),
|
|
1174
|
-
system: designSystem()
|
|
1175
|
-
});
|
|
1176
|
-
return requireSlices(all, cap);
|
|
1177
|
-
});
|
|
1178
|
-
};
|
|
1179
|
-
/** Slice `i`'s criteria; `undefined` when `slices` is the one slice of an unsliced change. */
|
|
1180
|
-
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) };
|
|
1181
|
-
//#endregion
|
|
1182
1036
|
//#region src/workflows/feature/feature.ts
|
|
1183
1037
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
1184
1038
|
const unwind = () => {
|
|
@@ -1186,94 +1040,37 @@ const unwind = () => {
|
|
|
1186
1040
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
1187
1041
|
};
|
|
1188
1042
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1043
|
+
* A naive pass (judged only with `flagMode` on), then implement and the
|
|
1044
|
+
* quality lap, then the review; design must have run since `firstBase`.
|
|
1191
1045
|
*/
|
|
1192
1046
|
const buildFeature = async (firstBase) => {
|
|
1193
1047
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
1194
|
-
const slices = await slicing();
|
|
1195
1048
|
const handedOut = new Set(listedIds());
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1049
|
+
const seam = await seamBuild(firstBase);
|
|
1050
|
+
const used = gather(handedOut);
|
|
1051
|
+
const closed = await implementing({
|
|
1052
|
+
reference: seam.reference,
|
|
1053
|
+
tier,
|
|
1054
|
+
afterRefactor: seam.refactored,
|
|
1055
|
+
naive: seam.land === true ? seam.reference : void 0,
|
|
1056
|
+
rebuild: seam.rebuild
|
|
1057
|
+
});
|
|
1058
|
+
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
1059
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
|
|
1060
|
+
const outcome = await reviewedTail(base, base);
|
|
1061
|
+
await featureFeedback(outcome, base, {
|
|
1062
|
+
closed,
|
|
1063
|
+
used
|
|
1064
|
+
});
|
|
1214
1065
|
};
|
|
1215
1066
|
const listedIds = () => criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
1216
1067
|
const gather = (handedOut) => {
|
|
1217
1068
|
for (const id of listedIds()) handedOut.add(id);
|
|
1218
1069
|
return [...handedOut];
|
|
1219
1070
|
};
|
|
1220
|
-
/**
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
criteria: build.criteria,
|
|
1224
|
-
refactor: build.refactor
|
|
1225
|
-
});
|
|
1226
|
-
const used = gather(build.handedOut);
|
|
1227
|
-
await slice(build.feature, async () => {
|
|
1228
|
-
const closed = await implementing({
|
|
1229
|
-
reference: seam.reference,
|
|
1230
|
-
tier: build.tier,
|
|
1231
|
-
afterRefactor: seam.refactored,
|
|
1232
|
-
naive: seam.land === true ? seam.reference : void 0,
|
|
1233
|
-
rebuild: seam.rebuild,
|
|
1234
|
-
slice: build.criteria,
|
|
1235
|
-
more: build.more
|
|
1236
|
-
});
|
|
1237
|
-
const more = build.more();
|
|
1238
|
-
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
1239
|
-
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? sliceBase : seam.base;
|
|
1240
|
-
if (more) {
|
|
1241
|
-
const guard = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
1242
|
-
const report = await tail(async (_, report) => report, {
|
|
1243
|
-
green: true,
|
|
1244
|
-
lensBase: base,
|
|
1245
|
-
guard,
|
|
1246
|
-
guardRests: false
|
|
1247
|
-
});
|
|
1248
|
-
build.laps.push({
|
|
1249
|
-
slice: build.number,
|
|
1250
|
-
report
|
|
1251
|
-
});
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
const reviewBase = build.reviewBase ?? base;
|
|
1255
|
-
const outcome = await reviewedTail(reviewBase, base, {
|
|
1256
|
-
slice: build.number,
|
|
1257
|
-
laps: build.laps
|
|
1258
|
-
});
|
|
1259
|
-
await featureFeedback(outcome, base, {
|
|
1260
|
-
closed,
|
|
1261
|
-
used
|
|
1262
|
-
});
|
|
1263
|
-
});
|
|
1264
|
-
};
|
|
1265
|
-
/**
|
|
1266
|
-
* The quality lap of slice `last.slice`, then a reviewer's walkthrough since
|
|
1267
|
-
* `base` for the human, carrying what earlier slices' `laps` left too.
|
|
1268
|
-
*/
|
|
1269
|
-
const reviewedTail = (base, lensBase, last = {
|
|
1270
|
-
slice: "",
|
|
1271
|
-
laps: []
|
|
1272
|
-
}, lap = "first") => tail((escalations, report) => humanReview(base, escalations, {
|
|
1273
|
-
report: last.laps.length === 0 ? report : joinReports([...last.laps, {
|
|
1274
|
-
slice: last.slice,
|
|
1275
|
-
report
|
|
1276
|
-
}]),
|
|
1071
|
+
/** The quality lap, then a reviewer's walkthrough since `base` for the human. */
|
|
1072
|
+
const reviewedTail = (base, lensBase, lap = "first") => tail((escalations, report) => humanReview(base, escalations, {
|
|
1073
|
+
report,
|
|
1277
1074
|
walkthrough: true,
|
|
1278
1075
|
lap
|
|
1279
1076
|
}), {
|
|
@@ -1298,7 +1095,7 @@ const featureFeedback = async (first, lensBase, ids) => {
|
|
|
1298
1095
|
tier: riskTierOn() ? recordRisk() : void 0,
|
|
1299
1096
|
amend: true
|
|
1300
1097
|
});
|
|
1301
|
-
outcome = await reviewedTail(outcome.base, lensBase,
|
|
1098
|
+
outcome = await reviewedTail(outcome.base, lensBase, "feedback");
|
|
1302
1099
|
}
|
|
1303
1100
|
};
|
|
1304
1101
|
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
@@ -1306,7 +1103,7 @@ const planAndBuild = async (firstBase, stated) => {
|
|
|
1306
1103
|
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
1307
1104
|
await buildFeature(firstBase);
|
|
1308
1105
|
};
|
|
1309
|
-
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback.
|
|
1106
|
+
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
|
|
1310
1107
|
const afterTail = async (first) => {
|
|
1311
1108
|
let outcome = first;
|
|
1312
1109
|
while (outcome.verdict === "feedback") {
|
|
@@ -1328,7 +1125,6 @@ const ordinaryStart = async () => {
|
|
|
1328
1125
|
});
|
|
1329
1126
|
refusePendingHandOff();
|
|
1330
1127
|
flagModeOn();
|
|
1331
|
-
sliceCap();
|
|
1332
1128
|
prototypeRounds();
|
|
1333
1129
|
const stated = statedCriteria(changes(), head());
|
|
1334
1130
|
await unwind();
|
|
@@ -1345,4 +1141,4 @@ const feature = shipped(ordinaryStart);
|
|
|
1345
1141
|
//#region src/workflows/feature/index.ts
|
|
1346
1142
|
var feature_default = feature;
|
|
1347
1143
|
//#endregion
|
|
1348
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail,
|
|
1144
|
+
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, handsOnIds, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unstableIds, unwind };
|