@pmelab/gtd 58.1.0 → 59.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 +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +30 -2
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/gherkin/git-control.feature +78 -0
- package/src/gherkin/index.ts +35 -1
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +118 -2
- package/src/testkit/TestRepo.ts +57 -6
- package/src/testkit/index.ts +1 -0
- package/src/twins/EmittedScriptRecognizer.ts +12 -14
- package/src/twins/FlowScripts.ts +10 -9
- package/src/twins/InMemRepo.ts +26 -1
- package/src/twins/Layers.ts +12 -1
- package/src/twins/index.ts +6 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -1,1018 +0,0 @@
|
|
|
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, restoreScript, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
|
|
2
|
-
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import { B as designSystem, Ct as testFileGlobs, D as planner, F as clearCriteriaMessage, I as clearCriteriaQuestion, J as implementBuildingPrompt, L as criterionLine, M as buildReviewCollectingPrompt, P as builderSystem, Q as naiveTryMessage, R as designGateAnswerMessage, S as collecting, St as envDefaults, V as designTriagePrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, at as refactorFixPrompt, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as startGateBlockedMessage, h as REFACTOR, ht as seamSteerMessage, i as skills, it as refactorDroppedRefusal, j as agentWithSkills, l as gate, mt as seamByHandMessage, n as summary, o as naiveTestOn, ot as refactorNotePrompt, p as NAIVE_TEST, pt as reviewerSystem, q as idleMessage, r as access, rt as refactorBuildingPrompt, s as shipped, st as refactorRules, t as steering, vt as unwindFailure, w as fixNits, x as coder, xt as defaults, y as answerReviewQuestions, z as designImpactPrompt } from "../chunks/reserved-lMYCaapY.mjs";
|
|
4
|
-
import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
|
|
5
|
-
import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
|
|
6
|
-
import { posix } from "node:path";
|
|
7
|
-
//#region src/workflows/shared/fences.ts
|
|
8
|
-
const markFenced = (text) => {
|
|
9
|
-
let fence;
|
|
10
|
-
return text.split(/\r?\n/).map((line) => {
|
|
11
|
-
const run = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
|
|
12
|
-
const fenced = fence !== void 0 || run !== void 0;
|
|
13
|
-
if (fence === void 0) fence = run;
|
|
14
|
-
else if (run !== void 0 && run[0] === fence[0] && run.length >= fence.length && /^ {0,3}(`+|~+)[ \t]*$/.test(line)) fence = void 0;
|
|
15
|
-
return {
|
|
16
|
-
line,
|
|
17
|
-
fenced
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
|
-
/** A markdown heading: its `#` run, then its text. */
|
|
22
|
-
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
23
|
-
/** A list item, its checkbox dropped: its text. */
|
|
24
|
-
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
25
|
-
//#endregion
|
|
26
|
-
//#region src/workflows/feature/release-flag.ts
|
|
27
|
-
const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i;
|
|
28
|
-
const isFlagHeading = (h) => h !== null && h[1].length === 2 && /^release flag$/i.test(h[2].trim());
|
|
29
|
-
/** The first line under `## Release Flag`, `undefined` without the section. */
|
|
30
|
-
const requestLine = (text) => {
|
|
31
|
-
const lines = markFenced(text);
|
|
32
|
-
const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)));
|
|
33
|
-
if (at === -1) return void 0;
|
|
34
|
-
const body = lines.slice(at + 1);
|
|
35
|
-
const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line));
|
|
36
|
-
return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim();
|
|
37
|
-
};
|
|
38
|
-
/** What `text` asks for, or why it cannot be read. No section asks for no flag. */
|
|
39
|
-
const releaseFlagOf = (text) => {
|
|
40
|
-
const line = requestLine(text);
|
|
41
|
-
if (line === void 0 || /^none$/i.test(line)) return { kind: "none" };
|
|
42
|
-
if (/^new$/i.test(line)) return { kind: "new" };
|
|
43
|
-
const name = NAME.exec(line)?.[1];
|
|
44
|
-
return name === void 0 ? { unreadable: line } : {
|
|
45
|
-
kind: "existing",
|
|
46
|
-
name
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
/** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
|
|
50
|
-
const requireReleaseFlag = (file) => {
|
|
51
|
-
const request = releaseFlagOf(read(file) ?? "");
|
|
52
|
-
if (!("unreadable" in request)) return;
|
|
53
|
-
refuse(`gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`);
|
|
54
|
-
};
|
|
55
|
-
/** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
|
|
56
|
-
const requestedFlag = () => {
|
|
57
|
-
requireReleaseFlag(REQUIREMENTS);
|
|
58
|
-
return releaseFlagOf(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
59
|
-
};
|
|
60
|
-
/** Whether the change goes behind a release flag: only then does the seam judge rule. */
|
|
61
|
-
const flagged = () => requestedFlag().kind !== "none";
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/workflows/feature/criteria.ts
|
|
64
|
-
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
65
|
-
const CLEAR_MIN_P = .8;
|
|
66
|
-
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
67
|
-
const criterion = (id, as = id[1]) => ({
|
|
68
|
-
id: as,
|
|
69
|
-
text: id[2]
|
|
70
|
-
});
|
|
71
|
-
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
72
|
-
const listUnder = (text, title, level) => {
|
|
73
|
-
const lines = markFenced(text);
|
|
74
|
-
const at = lines.findIndex((l) => {
|
|
75
|
-
const heading = l.fenced ? null : HEADING.exec(l.line);
|
|
76
|
-
return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
|
|
77
|
-
});
|
|
78
|
-
if (at === -1) return void 0;
|
|
79
|
-
const items = [];
|
|
80
|
-
let open = false;
|
|
81
|
-
for (const { line, fenced } of lines.slice(at + 1)) {
|
|
82
|
-
if (fenced) {
|
|
83
|
-
open = false;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (HEADING.test(line)) break;
|
|
87
|
-
const item = ITEM.exec(line);
|
|
88
|
-
if (item !== null) {
|
|
89
|
-
items.push(item[1].trim());
|
|
90
|
-
open = true;
|
|
91
|
-
} else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
|
|
92
|
-
else open = false;
|
|
93
|
-
}
|
|
94
|
-
return items;
|
|
95
|
-
};
|
|
96
|
-
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
97
|
-
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
98
|
-
const id = ID.exec(item);
|
|
99
|
-
return id === null ? {
|
|
100
|
-
id: "",
|
|
101
|
-
text: item
|
|
102
|
-
} : criterion(id);
|
|
103
|
-
});
|
|
104
|
-
/** The ID'd criteria of `text`. */
|
|
105
|
-
const criterionIds = (text) => (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
106
|
-
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
107
|
-
const numbered = (items) => {
|
|
108
|
-
const ids = items.map((item) => ID.exec(item));
|
|
109
|
-
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
110
|
-
return items.map((item, i) => {
|
|
111
|
-
const id = ids[i];
|
|
112
|
-
if (own) return criterion(id);
|
|
113
|
-
return id === null ? {
|
|
114
|
-
id: `AC-${i + 1}`,
|
|
115
|
-
text: item
|
|
116
|
-
} : criterion(id, `AC-${i + 1}`);
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
const STATED = /^acceptance criteria:?$/i;
|
|
120
|
-
const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
|
|
121
|
-
/**
|
|
122
|
-
* The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
|
|
123
|
-
* added or changed under an `Acceptance criteria` heading. More than one such
|
|
124
|
-
* file is ambiguous, and any other change in the sketch is input only triage
|
|
125
|
-
* folds in, so none.
|
|
126
|
-
*/
|
|
127
|
-
const statedCriteria = (sketch, commit) => {
|
|
128
|
-
const found = sketch.flatMap((change) => {
|
|
129
|
-
if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
|
|
130
|
-
const items = statedItems(change.after);
|
|
131
|
-
if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
|
|
132
|
-
return [{
|
|
133
|
-
path: change.path,
|
|
134
|
-
commit,
|
|
135
|
-
text: change.after,
|
|
136
|
-
criteria: numbered(items)
|
|
137
|
-
}];
|
|
138
|
-
});
|
|
139
|
-
return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
|
|
140
|
-
};
|
|
141
|
-
const listing = (criteria) => criteria.map(criterionLine).join("\n");
|
|
142
|
-
/** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
|
|
143
|
-
const requireCriteria = (file) => {
|
|
144
|
-
const text = read(file);
|
|
145
|
-
if (text === void 0) return;
|
|
146
|
-
const criteria = criteriaOf(text) ?? [];
|
|
147
|
-
const unnamed = criteria.filter((c) => c.id === "");
|
|
148
|
-
const ids = criteria.map((c) => c.id).filter((id) => id !== "");
|
|
149
|
-
const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
150
|
-
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
151
|
-
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`);
|
|
152
|
-
};
|
|
153
|
-
const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
|
|
154
|
-
const isImpactTurnHeading = (h) => h !== null && h[1].length === 2 && /^(impact|release flag)$/i.test(h[2].trim());
|
|
155
|
-
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
156
|
-
const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
|
|
157
|
-
/** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
|
|
158
|
-
const withoutImpact = (text) => {
|
|
159
|
-
let inImpact = false;
|
|
160
|
-
return markFenced(text).filter((l) => {
|
|
161
|
-
const h = l.fenced ? null : HEADING.exec(l.line);
|
|
162
|
-
if (h !== null && h[1].length <= 2) inImpact = isImpactTurnHeading(h);
|
|
163
|
-
return !inImpact;
|
|
164
|
-
}).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
|
|
165
|
-
};
|
|
166
|
-
/**
|
|
167
|
-
* Refuse a turn that leaves `file` without a `## Impact` section. Only the
|
|
168
|
-
* pending turn (`head()` is `""`): a triage lap recorded before Impact existed
|
|
169
|
-
* replays without one, and refusing there would fail every in-flight process.
|
|
170
|
-
* A human's edit at a later gate is not checked — the human owns that file then.
|
|
171
|
-
*/
|
|
172
|
-
const requireImpact = (file) => {
|
|
173
|
-
if (head() !== "") return;
|
|
174
|
-
const text = read(file);
|
|
175
|
-
if (text === void 0) return;
|
|
176
|
-
if (hasImpact(text)) return;
|
|
177
|
-
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`);
|
|
178
|
-
};
|
|
179
|
-
/** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
|
|
180
|
-
const requireImpactOnly = (file) => {
|
|
181
|
-
const change = changes().get(file);
|
|
182
|
-
if (change === void 0) return;
|
|
183
|
-
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
|
|
184
|
-
refuse(`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`);
|
|
185
|
-
};
|
|
186
|
-
const idNumber = (id) => Number(id.slice(3));
|
|
187
|
-
/** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
|
|
188
|
-
const unstableIds = (listed, ids) => {
|
|
189
|
-
const top = Math.max(0, ...ids.used.map(idNumber));
|
|
190
|
-
const reused = listed.filter((id) => !ids.closed.includes(id) && idNumber(id) <= top);
|
|
191
|
-
if (reused.length === 0) return void 0;
|
|
192
|
-
return `numbers ${reused.join(", ")} at or below AC-${top}, an ID already handed out — keep a built criterion's ID (reword it in place when the review changes it), and number new ones from AC-${top + 1}`;
|
|
193
|
-
};
|
|
194
|
-
/** Refuse criteria in `file` that renumber or reuse an earlier ID. */
|
|
195
|
-
const requireStableIds = (file, ids) => {
|
|
196
|
-
const why = unstableIds(criterionIds(read(file) ?? ""), ids);
|
|
197
|
-
if (why !== void 0) refuse(`gtd land: criteria-ids: ${file} ${why}`);
|
|
198
|
-
};
|
|
199
|
-
/** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
|
|
200
|
-
const requireCriteriaFrozen = (file) => {
|
|
201
|
-
const change = changes().get(file);
|
|
202
|
-
if (change === void 0) return;
|
|
203
|
-
if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
|
|
204
|
-
refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
|
|
205
|
-
};
|
|
206
|
-
/** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
|
|
207
|
-
const clear = async (stated) => {
|
|
208
|
-
const { answers, truncated } = await judge("clear", {
|
|
209
|
-
questions: [clearCriteriaQuestion],
|
|
210
|
-
evidence: {
|
|
211
|
-
criteria: listing(stated.criteria),
|
|
212
|
-
sketch: stated.text
|
|
213
|
-
},
|
|
214
|
-
message: clearCriteriaMessage(),
|
|
215
|
-
label: "Judging the stated acceptance criteria"
|
|
216
|
-
});
|
|
217
|
-
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
218
|
-
};
|
|
219
|
-
/** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
|
|
220
|
-
const impact = async () => {
|
|
221
|
-
await agentWithSkills("impact", designImpactPrompt(), {
|
|
222
|
-
label: "Mapping the change's impact",
|
|
223
|
-
file: REQUIREMENTS,
|
|
224
|
-
model: planner(),
|
|
225
|
-
system: designSystem()
|
|
226
|
-
});
|
|
227
|
-
requireProgress(REQUIREMENTS);
|
|
228
|
-
requireCriteriaFrozen(REQUIREMENTS);
|
|
229
|
-
requireImpact(REQUIREMENTS);
|
|
230
|
-
requireImpactOnly(REQUIREMENTS);
|
|
231
|
-
requireReleaseFlag(REQUIREMENTS);
|
|
232
|
-
};
|
|
233
|
-
/**
|
|
234
|
-
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
235
|
-
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
236
|
-
* design gate. `false` when design must run.
|
|
237
|
-
*/
|
|
238
|
-
const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
|
|
239
|
-
if (!await clear(stated)) return false;
|
|
240
|
-
await run("normalise", normaliseCriteriaScript(stated), {
|
|
241
|
-
label: "Recording the stated acceptance criteria",
|
|
242
|
-
file: REQUIREMENTS
|
|
243
|
-
});
|
|
244
|
-
await impact();
|
|
245
|
-
return true;
|
|
246
|
-
});
|
|
247
|
-
//#endregion
|
|
248
|
-
//#region src/workflows/feature/tier.ts
|
|
249
|
-
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
250
|
-
const riskTierOn = () => vars.flag_risk_tier === "1";
|
|
251
|
-
const sectionLines = (text, title) => {
|
|
252
|
-
if (text === void 0) return [];
|
|
253
|
-
const heading = new RegExp(`^##[ \\t]+${title}[ \\t]*$`, "i");
|
|
254
|
-
const lines = markFenced(text);
|
|
255
|
-
const start = lines.findIndex((l) => !l.fenced && heading.test(l.line));
|
|
256
|
-
if (start < 0) return [];
|
|
257
|
-
const rest = lines.slice(start + 1);
|
|
258
|
-
const end = rest.findIndex((l) => !l.fenced && /^#{1,2}[ \t]/.test(l.line));
|
|
259
|
-
return end < 0 ? rest : rest.slice(0, end);
|
|
260
|
-
};
|
|
261
|
-
/** Anything but a legible `low` is `high`: under-tiering is the risky direction. */
|
|
262
|
-
const readRiskTier = (requirements) => {
|
|
263
|
-
return (sectionLines(requirements, "Risk Tier").find((l) => !l.fenced && l.line.trim() !== "")?.line)?.trim().split(/\s+/)[0]?.replace(/[`*_]/g, "").toLowerCase() === "low" ? "low" : "high";
|
|
264
|
-
};
|
|
265
|
-
const CONTROL = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/g;
|
|
266
|
-
const tidy = (text) => text.replace(/\s+/g, " ").replace(CONTROL, "").trim();
|
|
267
|
-
const productionSignals = (requirements) => {
|
|
268
|
-
const items = [];
|
|
269
|
-
for (const { line, fenced } of sectionLines(requirements, "Production Signals")) {
|
|
270
|
-
if (fenced) continue;
|
|
271
|
-
const top = /^[-*+][ \t]+(.*)$/.exec(line);
|
|
272
|
-
if (top !== null) items.push(top[1]);
|
|
273
|
-
}
|
|
274
|
-
return items.map(tidy).filter((item) => item !== "");
|
|
275
|
-
};
|
|
276
|
-
/** The tier is always marked, first, so the last `risk-tier` mark opens the current lap's marks. */
|
|
277
|
-
const recordRisk = () => {
|
|
278
|
-
const requirements = read(REQUIREMENTS);
|
|
279
|
-
const tier = readRiskTier(requirements);
|
|
280
|
-
mark("risk-tier", tier);
|
|
281
|
-
for (const signal of productionSignals(requirements)) mark("production-signal", signal);
|
|
282
|
-
return tier;
|
|
283
|
-
};
|
|
284
|
-
//#endregion
|
|
285
|
-
//#region src/workflows/feature/steps.ts
|
|
286
|
-
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
287
|
-
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base, riskTierOn()), {
|
|
288
|
-
label: "Triaging the change",
|
|
289
|
-
file: REQUIREMENTS,
|
|
290
|
-
mode: "qa",
|
|
291
|
-
model: planner(),
|
|
292
|
-
system: designSystem()
|
|
293
|
-
});
|
|
294
|
-
const answerProductQuestions = () => human("gate.answer", {
|
|
295
|
-
message: designGateAnswerMessage(riskTierOn()),
|
|
296
|
-
label: "Awaiting your product answers",
|
|
297
|
-
file: REQUIREMENTS,
|
|
298
|
-
mode: "qa",
|
|
299
|
-
acceptClean: true
|
|
300
|
-
});
|
|
301
|
-
/** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
|
|
302
|
-
const refactorCollecting = (capture, round, strays) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, {
|
|
303
|
-
strays,
|
|
304
|
-
round
|
|
305
|
-
}), {
|
|
306
|
-
label: "Collecting your refactor feedback",
|
|
307
|
-
file: REFACTOR,
|
|
308
|
-
model: planner(),
|
|
309
|
-
system: reviewerSystem(),
|
|
310
|
-
allowEmpty: true
|
|
311
|
-
});
|
|
312
|
-
//#endregion
|
|
313
|
-
//#region src/workflows/feature/planning.ts
|
|
314
|
-
/**
|
|
315
|
-
* Always stop for the human. Resolves `true` when the round changed anything
|
|
316
|
-
* (the author revises), `false` on a clean re-run. Open threads refuse every
|
|
317
|
-
* landing; unticked questions only refuse a round with no thread for the
|
|
318
|
-
* agent to answer.
|
|
319
|
-
*/
|
|
320
|
-
const noteGate = async (file, answer) => {
|
|
321
|
-
await answer();
|
|
322
|
-
requireThreadsClosed(file);
|
|
323
|
-
if (changes().length === 0) return false;
|
|
324
|
-
if (!hasThreadFor("agent", file)) requireAnswers(file);
|
|
325
|
-
return true;
|
|
326
|
-
};
|
|
327
|
-
/** Triage the sketch since `base` into requirements, until no product question is open; `guard` holds every round's criteria. */
|
|
328
|
-
const design = (base, guard) => scope("design", async () => {
|
|
329
|
-
do {
|
|
330
|
-
await triage(base);
|
|
331
|
-
requireProgress(REQUIREMENTS);
|
|
332
|
-
requireReplies(REQUIREMENTS);
|
|
333
|
-
requireCriteria(REQUIREMENTS);
|
|
334
|
-
requireImpact(REQUIREMENTS);
|
|
335
|
-
requireReleaseFlag(REQUIREMENTS);
|
|
336
|
-
guard?.();
|
|
337
|
-
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
338
|
-
});
|
|
339
|
-
//#endregion
|
|
340
|
-
//#region src/workflows/feature/green.ts
|
|
341
|
-
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
342
|
-
label: "Fixing the check",
|
|
343
|
-
file: FEEDBACK,
|
|
344
|
-
model: coder(),
|
|
345
|
-
system: builderSystem()
|
|
346
|
-
}), {
|
|
347
|
-
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
348
|
-
guardRests: false
|
|
349
|
-
});
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/workflows/feature/refactor.ts
|
|
352
|
-
const REFACTOR_PACKAGE = REFACTOR;
|
|
353
|
-
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
354
|
-
const requireWithin = (allowed) => {
|
|
355
|
-
const outside = changes().map((c) => c.path).filter((path) => isCode(path) && !allowed.includes(path));
|
|
356
|
-
if (outside.length === 0) return;
|
|
357
|
-
refuse(`gtd land: refactor-scope: the refactor may change only its stray paths — revert these:\n${outside.map((path) => ` - ${path}`).join("\n")}`);
|
|
358
|
-
};
|
|
359
|
-
const isTestFile = (path) => testFileGlobs.some((glob) => matches(path, glob));
|
|
360
|
-
const requireNoTestFiles = () => {
|
|
361
|
-
const tests = changes().map((c) => c.path).filter((path) => isCode(path) && isTestFile(path));
|
|
362
|
-
if (tests.length === 0) return;
|
|
363
|
-
refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
|
|
364
|
-
};
|
|
365
|
-
const holdsCallSites = (path) => !path.endsWith(".feature") && !path.endsWith(".snap");
|
|
366
|
-
const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm;
|
|
367
|
-
const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g;
|
|
368
|
-
const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g;
|
|
369
|
-
const MODIFIER = /(?<![.\w])(?:it|test|describe)\.(skip|only|todo|fails|skipIf|runIf)\b|(?<![.\w])(x)(?:it|test|describe)\b/g;
|
|
370
|
-
const counts = (text) => {
|
|
371
|
-
const live = text.replace(COMMENT, "");
|
|
372
|
-
const modifiers = [...live.matchAll(MODIFIER)].map((m) => m[1] ?? m[2]).sort();
|
|
373
|
-
return JSON.stringify([
|
|
374
|
-
[...live.matchAll(ASSERTION)].length,
|
|
375
|
-
[...live.matchAll(TEST_CASE)].length,
|
|
376
|
-
modifiers
|
|
377
|
-
]);
|
|
378
|
-
};
|
|
379
|
-
const tested = (text) => {
|
|
380
|
-
const live = text.replace(COMMENT, "");
|
|
381
|
-
return live.search(ASSERTION) >= 0 || live.search(TEST_CASE) >= 0;
|
|
382
|
-
};
|
|
383
|
-
const callSitesOnly = ({ path, status, before, after }) => status === "modified" && holdsCallSites(path) && before !== void 0 && after !== void 0 && tested(before) && counts(before) === counts(after);
|
|
384
|
-
/**
|
|
385
|
-
* The test-file half of the refactor guard: an existing test may follow a changed call shape,
|
|
386
|
-
* nothing more. Returns the test files it admitted, which the scope check then allows.
|
|
387
|
-
*/
|
|
388
|
-
const requireTestCallSitesOnly = () => {
|
|
389
|
-
const tests = changes().filter((c) => isCode(c.path) && isTestFile(c.path));
|
|
390
|
-
const refused = tests.filter((c) => !callSitesOnly(c)).map((c) => c.path);
|
|
391
|
-
if (refused.length > 0) refuse(`gtd land: refactor-tests: the refactor may update an existing test file's call sites only — never add, delete, skip or comment out a test or an assertion, or touch a snapshot, a .feature file or test data; revert these:\n${refused.map((path) => ` - ${path}`).join("\n")}`);
|
|
392
|
-
return tests.map((c) => c.path);
|
|
393
|
-
};
|
|
394
|
-
const flagExpression = (pattern) => {
|
|
395
|
-
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
396
|
-
let expression;
|
|
397
|
-
try {
|
|
398
|
-
expression = new RegExp(pattern, "g");
|
|
399
|
-
} catch (error) {
|
|
400
|
-
throw new Error(`gtd: refactorFlagPattern is not a valid regular expression: ${String(error)}`);
|
|
401
|
-
}
|
|
402
|
-
if (expression.test("")) throw new Error("gtd: refactorFlagPattern must not match the empty string");
|
|
403
|
-
return expression;
|
|
404
|
-
};
|
|
405
|
-
const countFlags = (text, expression, into) => {
|
|
406
|
-
let zeroLength = false;
|
|
407
|
-
for (const [name] of text.matchAll(expression)) if (name === "") zeroLength = true;
|
|
408
|
-
else into.set(name, (into.get(name) ?? 0) + 1);
|
|
409
|
-
return zeroLength;
|
|
410
|
-
};
|
|
411
|
-
const listPaths = (paths) => paths.map((path) => ` - ${path}`).join("\n");
|
|
412
|
-
const requireNoNewFlags = (pattern, since) => {
|
|
413
|
-
const expression = flagExpression(pattern);
|
|
414
|
-
const before = /* @__PURE__ */ new Map();
|
|
415
|
-
const after = /* @__PURE__ */ new Map();
|
|
416
|
-
const unreadable = [];
|
|
417
|
-
const zeroLength = [];
|
|
418
|
-
for (const { path, status, before: was, after: now } of changesSince(since)) {
|
|
419
|
-
if (!isCode(path)) continue;
|
|
420
|
-
const oldSide = status === "added" ? "" : was;
|
|
421
|
-
const newSide = status === "deleted" ? "" : now;
|
|
422
|
-
if (oldSide === void 0 || newSide === void 0) {
|
|
423
|
-
unreadable.push(path);
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
countFlags(oldSide, expression, before);
|
|
427
|
-
if (countFlags(newSide, expression, after)) zeroLength.push(path);
|
|
428
|
-
}
|
|
429
|
-
if (unreadable.length > 0) refuse(`gtd land: refactor-flags: cannot compare flag reads in:\n${listPaths(unreadable)}`);
|
|
430
|
-
if (zeroLength.length > 0) refuse(`gtd land: refactor-flags: refactorFlagPattern (${pattern}) produced a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
431
|
-
const raised = [...after].filter(([name, count]) => count > (before.get(name) ?? 0));
|
|
432
|
-
if (raised.length === 0) return;
|
|
433
|
-
refuse(`gtd land: refactor-flags: the refactor may not introduce or add reads of a flag (${pattern}):\n${raised.map(([name, count]) => ` - ${name}: ${before.get(name) ?? 0} → ${count}`).join("\n")}`);
|
|
434
|
-
};
|
|
435
|
-
/** A test-file stray is never refactored: the implement turn still reads it in the naive tip. */
|
|
436
|
-
const refactorStrays = (shape) => shape.strays.filter((path) => !isTestFile(path));
|
|
437
|
-
const packageLines = (strays, pattern, reference) => [
|
|
438
|
-
"# Behaviour-neutral refactor",
|
|
439
|
-
"",
|
|
440
|
-
"## Scope (read-only)",
|
|
441
|
-
"",
|
|
442
|
-
...strays.map((path) => `- \`${path}\``),
|
|
443
|
-
"",
|
|
444
|
-
"This list is fixed when the package is derived: editing it here widens or narrows nothing.",
|
|
445
|
-
"",
|
|
446
|
-
"## Rules",
|
|
447
|
-
"",
|
|
448
|
-
"- The suite's outcome must not change",
|
|
449
|
-
"- Change only the paths above, plus an existing test file where a changed call shape reaches it: never add or delete a test file, and never add, remove or alter a test or an assertion",
|
|
450
|
-
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
451
|
-
"",
|
|
452
|
-
"## Reference",
|
|
453
|
-
"",
|
|
454
|
-
...reference === void 0 ? ["None."] : [
|
|
455
|
-
`Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
456
|
-
"",
|
|
457
|
-
"Its changes to the existing code above are the seam this refactor exists to land, minus the feature's own behaviour."
|
|
458
|
-
],
|
|
459
|
-
""
|
|
460
|
-
];
|
|
461
|
-
const demoteHeadings = (text) => {
|
|
462
|
-
const marked = markFenced(text.trimEnd());
|
|
463
|
-
const lines = marked.map((m) => m.line);
|
|
464
|
-
const headings = marked.map((m) => m.fenced ? void 0 : /^(#{1,6}) /.exec(m.line)?.[1]?.length);
|
|
465
|
-
const shift = 3 - Math.min(...headings.filter((n) => n !== void 0));
|
|
466
|
-
return lines.map((line, i) => {
|
|
467
|
-
const depth = headings[i];
|
|
468
|
-
if (depth === void 0) return line;
|
|
469
|
-
return "#".repeat(Math.min(6, depth + shift)) + line.slice(depth);
|
|
470
|
-
});
|
|
471
|
-
};
|
|
472
|
-
const deriveScript = (lines) => `mkdir -p .gtd
|
|
473
|
-
printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(REFACTOR_PACKAGE)}
|
|
474
|
-
`;
|
|
475
|
-
/** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
|
|
476
|
-
const plan = (shape, options) => {
|
|
477
|
-
if (shape.choice !== "refactor-first" || shape.unreadable.length > 0) return void 0;
|
|
478
|
-
const pattern = vars.refactorFlagPattern ?? defaults.refactorFlagPattern;
|
|
479
|
-
const expression = flagExpression(pattern);
|
|
480
|
-
const strays = refactorStrays(shape);
|
|
481
|
-
if (strays.length === 0) return void 0;
|
|
482
|
-
const zeroLength = strays.filter((path) => countFlags(read(path) ?? "", expression, /* @__PURE__ */ new Map()));
|
|
483
|
-
if (zeroLength.length > 0) throw new Error(`gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`);
|
|
484
|
-
return {
|
|
485
|
-
pattern,
|
|
486
|
-
strays,
|
|
487
|
-
reference: options.reference?.trim() ? options.reference : void 0,
|
|
488
|
-
note: options.note?.trim() ? options.note : void 0
|
|
489
|
-
};
|
|
490
|
-
};
|
|
491
|
-
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
492
|
-
requireWithin([...strays, ...requireTestCallSitesOnly()]);
|
|
493
|
-
requireNoNewFlags(pattern, entry);
|
|
494
|
-
};
|
|
495
|
-
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
496
|
-
const refactorLap = async (p, guard, feedback) => {
|
|
497
|
-
const { pattern, strays, reference, note } = p;
|
|
498
|
-
await run("deriving", deriveScript([
|
|
499
|
-
...packageLines(strays, pattern, reference),
|
|
500
|
-
...note === void 0 ? [] : [
|
|
501
|
-
"## Refactor note",
|
|
502
|
-
"",
|
|
503
|
-
...demoteHeadings(note),
|
|
504
|
-
""
|
|
505
|
-
],
|
|
506
|
-
...feedback === void 0 ? [] : [
|
|
507
|
-
"## Review feedback",
|
|
508
|
-
"",
|
|
509
|
-
...demoteHeadings(feedback),
|
|
510
|
-
""
|
|
511
|
-
]
|
|
512
|
-
]), {
|
|
513
|
-
label: "Deriving the refactor package",
|
|
514
|
-
file: REFACTOR_PACKAGE
|
|
515
|
-
});
|
|
516
|
-
await agentWithSkills("building", refactorBuildingPrompt({
|
|
517
|
-
pkg: REFACTOR_PACKAGE,
|
|
518
|
-
strays,
|
|
519
|
-
pattern,
|
|
520
|
-
reference,
|
|
521
|
-
feedback: feedback !== void 0,
|
|
522
|
-
note: note !== void 0
|
|
523
|
-
}), {
|
|
524
|
-
label: "Refactoring",
|
|
525
|
-
model: coder(),
|
|
526
|
-
system: builderSystem()
|
|
527
|
-
});
|
|
528
|
-
guard();
|
|
529
|
-
await healthy(async () => {
|
|
530
|
-
await agentWithSkills("fix.suite.fixing", refactorFixPrompt(strays, pattern), {
|
|
531
|
-
label: "Fixing the check",
|
|
532
|
-
file: FEEDBACK,
|
|
533
|
-
model: coder(),
|
|
534
|
-
system: builderSystem()
|
|
535
|
-
});
|
|
536
|
-
guard();
|
|
537
|
-
}, {
|
|
538
|
-
describeGuard: guard,
|
|
539
|
-
rules: refactorRules(strays, pattern)
|
|
540
|
-
});
|
|
541
|
-
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
542
|
-
};
|
|
543
|
-
const strayAccess = (strays) => ({ write: [
|
|
544
|
-
...strays,
|
|
545
|
-
...testFileGlobs,
|
|
546
|
-
".gtd/**"
|
|
547
|
-
] });
|
|
548
|
-
const refactorFirst = async (shape, options = {}) => {
|
|
549
|
-
const p = plan(shape, options);
|
|
550
|
-
if (p === void 0) return false;
|
|
551
|
-
return scope({
|
|
552
|
-
name: "refactor",
|
|
553
|
-
access: strayAccess(p.strays)
|
|
554
|
-
}, async () => {
|
|
555
|
-
await refactorLap(p, guardOf(p, head()));
|
|
556
|
-
return true;
|
|
557
|
-
});
|
|
558
|
-
};
|
|
559
|
-
/**
|
|
560
|
-
* `refactorFirst` plus its review, in the one `refactor` scope so the tail's
|
|
561
|
-
* steps are `refactor.build.*`. A review edit re-runs the refactor, never a
|
|
562
|
-
* re-plan: the collected notes ride the re-derived package. Every round
|
|
563
|
-
* reviews the whole refactor since `entry`, since each lap rebuilds over it.
|
|
564
|
-
*/
|
|
565
|
-
const reviewedRefactor = async (shape, options = {}) => {
|
|
566
|
-
const p = plan(shape, options);
|
|
567
|
-
if (p === void 0) return false;
|
|
568
|
-
return scope({
|
|
569
|
-
name: "refactor",
|
|
570
|
-
access: strayAccess(p.strays)
|
|
571
|
-
}, async () => {
|
|
572
|
-
const entry = head();
|
|
573
|
-
const guard = guardOf(p, entry);
|
|
574
|
-
const feedback = {
|
|
575
|
-
file: REFACTOR_PACKAGE,
|
|
576
|
-
collecting: (capture, round) => refactorCollecting(capture, round, p.strays),
|
|
577
|
-
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",
|
|
578
|
-
dropped: refactorDroppedRefusal
|
|
579
|
-
};
|
|
580
|
-
let collected;
|
|
581
|
-
for (;;) {
|
|
582
|
-
await refactorLap(p, guard, collected);
|
|
583
|
-
const outcome = await buildTail(entry, {
|
|
584
|
-
green: true,
|
|
585
|
-
guard,
|
|
586
|
-
guardRests: false,
|
|
587
|
-
lenses: lensEntriesOf(vars.refactorQualityReviews ?? ""),
|
|
588
|
-
feedback,
|
|
589
|
-
fixAccess: strayAccess(p.strays),
|
|
590
|
-
lensBase: entry,
|
|
591
|
-
rules: refactorRules(p.strays, p.pattern),
|
|
592
|
-
lap: "refactor"
|
|
593
|
-
});
|
|
594
|
-
if (outcome.verdict === "signoff") return true;
|
|
595
|
-
await reUnwind(outcome);
|
|
596
|
-
collected = read(REFACTOR_PACKAGE);
|
|
597
|
-
if (collected === void 0) throw new Error(`gtd: review feedback left no ${REFACTOR_PACKAGE}`);
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
};
|
|
601
|
-
//#endregion
|
|
602
|
-
//#region src/workflows/feature/seam.ts
|
|
603
|
-
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
604
|
-
const NAIVE_RETRIES = 2;
|
|
605
|
-
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
606
|
-
const judgeSeam = (tip) => judgeDiffShape("judge", {
|
|
607
|
-
changes: tip.changes,
|
|
608
|
-
allow: (vars.seamAllow ?? defaults.seamAllow).split(",").map((glob) => glob.trim()).filter((glob) => glob !== ""),
|
|
609
|
-
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path)
|
|
610
|
-
});
|
|
611
|
-
const naivePass = (retry) => agentWithSkills("naive", naivePassPrompt(retry), {
|
|
612
|
-
label: retry === void 0 ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
613
|
-
keep: true,
|
|
614
|
-
model: coder(),
|
|
615
|
-
system: builderSystem()
|
|
616
|
-
});
|
|
617
|
-
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g;
|
|
618
|
-
const EXTENSION = /\.[\w-]+$/;
|
|
619
|
-
const ALIAS = /^(?:[@~]\/|#)/;
|
|
620
|
-
/** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
|
|
621
|
-
const namesOf = (path) => {
|
|
622
|
-
const bare = path.replace(EXTENSION, "");
|
|
623
|
-
return posix.basename(bare) === "index" ? [
|
|
624
|
-
path,
|
|
625
|
-
bare,
|
|
626
|
-
posix.dirname(bare)
|
|
627
|
-
] : [path, bare];
|
|
628
|
-
};
|
|
629
|
-
/**
|
|
630
|
-
* The repository paths `test` imports, each with and without an extension: a
|
|
631
|
-
* relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
|
|
632
|
-
* `#`) as a path tail, anything else as written. Comments are skipped; a
|
|
633
|
-
* computed import goes unseen.
|
|
634
|
-
*/
|
|
635
|
-
const importsOf = (test) => [...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
|
|
636
|
-
const relative = /^\.\.?(\/|$)/.test(spec);
|
|
637
|
-
const tail = ALIAS.test(spec);
|
|
638
|
-
const path = relative ? posix.join(posix.dirname(test.path), spec) : spec.replace(ALIAS, "");
|
|
639
|
-
return [path, path.replace(EXTENSION, "")].map((p) => ({
|
|
640
|
-
path: p,
|
|
641
|
-
tail
|
|
642
|
-
}));
|
|
643
|
-
});
|
|
644
|
-
const reaches = (imports, added) => namesOf(added).some((name) => imports.some(({ path, tail }) => name === path || tail && name.endsWith(`/${path}`)));
|
|
645
|
-
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
646
|
-
/**
|
|
647
|
-
* Why `tip`'s tests fall short, or `undefined`: a test importing a file the
|
|
648
|
-
* pass added — naive structure a reset and refactor replaces, which the tests
|
|
649
|
-
* must survive.
|
|
650
|
-
*/
|
|
651
|
-
const acceptanceFault = (tip) => {
|
|
652
|
-
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
653
|
-
const reaching = acceptanceTests(tip.changes).filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
654
|
-
const imports = importsOf(test);
|
|
655
|
-
return added.filter((path) => reaches(imports, path)).map((path) => ` - ${test.path} → ${path}`);
|
|
656
|
-
});
|
|
657
|
-
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")}`;
|
|
658
|
-
};
|
|
659
|
-
/** What a rebuild carries over from the naive pass `tip`: every test file it added or changed. */
|
|
660
|
-
const naiveAcceptance = (tip) => ({
|
|
661
|
-
hash: tip.hash,
|
|
662
|
-
tests: acceptanceTests(tip.changes).map((c) => c.path)
|
|
663
|
-
});
|
|
664
|
-
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`;
|
|
665
|
-
/**
|
|
666
|
-
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
667
|
-
* `true` when green. The pass's paths go back to HEAD before (an interrupted
|
|
668
|
-
* run left them applied) and after; afterwards so does anything else the run
|
|
669
|
-
* changed, judged against what was already dirty, which is left alone.
|
|
670
|
-
*/
|
|
671
|
-
const naiveGreen = (tip) => {
|
|
672
|
-
const paths = tip.changes.map((c) => restore(c.path));
|
|
673
|
-
return check("check", [
|
|
674
|
-
...paths,
|
|
675
|
-
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
676
|
-
`if ! git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply --whitespace=nowarn; then`,
|
|
677
|
-
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
678
|
-
" exit 1",
|
|
679
|
-
"fi",
|
|
680
|
-
"(",
|
|
681
|
-
env.testCommand ?? "",
|
|
682
|
-
")",
|
|
683
|
-
"code=$?",
|
|
684
|
-
...paths,
|
|
685
|
-
"git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
|
|
686
|
-
" printf '%s\\n' \"$dirty\" | grep -qxF -- \"$path\" && continue",
|
|
687
|
-
" if git cat-file -e \"HEAD:$path\" 2>/dev/null; then git checkout -q HEAD -- \"$path\"; else rm -f -- \"$path\"; fi",
|
|
688
|
-
"done",
|
|
689
|
-
"exit \"$code\""
|
|
690
|
-
].join("\n"), {
|
|
691
|
-
report: FEEDBACK,
|
|
692
|
-
label: "Running the naive pass's tests"
|
|
693
|
-
});
|
|
694
|
-
};
|
|
695
|
-
/** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
|
|
696
|
-
const applied = async (tip) => {
|
|
697
|
-
await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
|
|
698
|
-
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
699
|
-
};
|
|
700
|
-
const build = async (options) => {
|
|
701
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.amend, options.afterRefactor, requestedFlag(), options.rebuild), {
|
|
702
|
-
label: "Implementing",
|
|
703
|
-
model: coder(),
|
|
704
|
-
system: builderSystem(),
|
|
705
|
-
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
706
|
-
});
|
|
707
|
-
requireCriteriaFrozen(REQUIREMENTS);
|
|
708
|
-
};
|
|
709
|
-
async function implement(first, tier, amend) {
|
|
710
|
-
await implementing(typeof first === "object" && first !== null ? first : {
|
|
711
|
-
reference: first,
|
|
712
|
-
tier,
|
|
713
|
-
amend
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
/** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
|
|
717
|
-
const implementing = (options) => scope("implement", async () => {
|
|
718
|
-
if (options.naive === void 0 || !await applied(options.naive)) await build(options);
|
|
719
|
-
else requireCriteriaFrozen(REQUIREMENTS);
|
|
720
|
-
await green();
|
|
721
|
-
const ids = criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
722
|
-
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
|
|
723
|
-
return ids;
|
|
724
|
-
});
|
|
725
|
-
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
726
|
-
const steer = async (retry) => {
|
|
727
|
-
await human("steer", {
|
|
728
|
-
message: seamSteerMessage(retry, flagged()),
|
|
729
|
-
label: "Steer or overrule the naive pass",
|
|
730
|
-
file: REQUIREMENTS,
|
|
731
|
-
acceptClean: true
|
|
732
|
-
});
|
|
733
|
-
if (changes().length === 0) return false;
|
|
734
|
-
requireCriteria(REQUIREMENTS);
|
|
735
|
-
return true;
|
|
736
|
-
};
|
|
737
|
-
/** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
|
|
738
|
-
const budget = () => {
|
|
739
|
-
let retries = 0;
|
|
740
|
-
return async (next) => {
|
|
741
|
-
if (retries < NAIVE_RETRIES) {
|
|
742
|
-
retries++;
|
|
743
|
-
return {
|
|
744
|
-
kind: "retry",
|
|
745
|
-
retry: next
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
const steered = await steer(next);
|
|
749
|
-
retries = 0;
|
|
750
|
-
if (read(".gtd/FEEDBACK.md") !== void 0) await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" });
|
|
751
|
-
return { kind: steered ? "steered" : "overruled" };
|
|
752
|
-
};
|
|
753
|
-
};
|
|
754
|
-
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
755
|
-
const vetted = async (tip, vetting) => {
|
|
756
|
-
const fault = vetting.waived ? void 0 : acceptanceFault(tip);
|
|
757
|
-
if (fault !== void 0) {
|
|
758
|
-
const verdict = await vetting.again({
|
|
759
|
-
fault,
|
|
760
|
-
hash: tip.hash
|
|
761
|
-
});
|
|
762
|
-
if (verdict.kind !== "overruled") return verdict;
|
|
763
|
-
vetting.waived = true;
|
|
764
|
-
}
|
|
765
|
-
if (await naiveGreen(tip)) return void 0;
|
|
766
|
-
const verdict = await vetting.again({ red: tip.hash });
|
|
767
|
-
return verdict.kind === "overruled" ? void 0 : verdict;
|
|
768
|
-
};
|
|
769
|
-
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
770
|
-
const ruled = async (tip, again) => {
|
|
771
|
-
const shape = await judgeSeam(tip);
|
|
772
|
-
if (shape.choice === "land") return {
|
|
773
|
-
reference: tip.hash,
|
|
774
|
-
tip
|
|
775
|
-
};
|
|
776
|
-
if (shape.choice === "refactor-first") return {
|
|
777
|
-
reference: tip.hash,
|
|
778
|
-
tip,
|
|
779
|
-
shape
|
|
780
|
-
};
|
|
781
|
-
const next = await again({
|
|
782
|
-
strays: shape.strays,
|
|
783
|
-
p: shape.judgment?.answers.shape?.p ?? 0
|
|
784
|
-
});
|
|
785
|
-
return next.kind === "overruled" ? {
|
|
786
|
-
reference: tip.hash,
|
|
787
|
-
tip
|
|
788
|
-
} : next;
|
|
789
|
-
};
|
|
790
|
-
/** Back to `before`: every code path, and the notes file. `.gtd/` edits (the requirements) stay. */
|
|
791
|
-
const untryScript = (before) => {
|
|
792
|
-
const code = changesSince(before).filter((c) => isCode$1(c.path));
|
|
793
|
-
return restoreScript(head(), {
|
|
794
|
-
restore: code.filter((c) => c.status !== "added").map((c) => c.path),
|
|
795
|
-
remove: [...code.filter((c) => c.status === "added").map((c) => c.path), NAIVE_TEST]
|
|
796
|
-
}, before);
|
|
797
|
-
};
|
|
798
|
-
/**
|
|
799
|
-
* A person tries the green pass `tip` in the working tree. Resolves
|
|
800
|
-
* `undefined` on a clean landing (signed off), else what they sent back;
|
|
801
|
-
* either way the pass, their code edits and the notes file leave the tree.
|
|
802
|
-
*/
|
|
803
|
-
const tried = async (tip) => {
|
|
804
|
-
const before = head();
|
|
805
|
-
await run("applying", applyScript(tip.hash, FEEDBACK, naiveApplyFailure(tip.hash), [".gtd"]), { label: "Applying the naive pass to try it" });
|
|
806
|
-
await human("try", {
|
|
807
|
-
message: naiveTryMessage(flagged()),
|
|
808
|
-
label: "Try the naive pass",
|
|
809
|
-
file: NAIVE_TEST,
|
|
810
|
-
acceptClean: true
|
|
811
|
-
});
|
|
812
|
-
const landed = changes();
|
|
813
|
-
const edits = landed.some((c) => isCode$1(c.path)) ? head() : void 0;
|
|
814
|
-
const notes = read(".gtd/NAIVE-TEST.md")?.trim() || void 0;
|
|
815
|
-
if (landed.some((c) => c.path === ".gtd/REQUIREMENTS.md")) requireCriteria(REQUIREMENTS);
|
|
816
|
-
await run("restoring", untryScript(before), { label: "Taking the naive pass out again" });
|
|
817
|
-
return landed.length === 0 ? void 0 : {
|
|
818
|
-
tested: tip.hash,
|
|
819
|
-
notes,
|
|
820
|
-
edits
|
|
821
|
-
};
|
|
822
|
-
};
|
|
823
|
-
const decide = () => scope("seam", async () => {
|
|
824
|
-
const vetting = {
|
|
825
|
-
again: budget(),
|
|
826
|
-
waived: false
|
|
827
|
-
};
|
|
828
|
-
let retry;
|
|
829
|
-
for (;;) {
|
|
830
|
-
await naivePass(retry);
|
|
831
|
-
const tip = kept("naive");
|
|
832
|
-
const verdict = await vetted(tip, vetting);
|
|
833
|
-
if (verdict === void 0 && naiveTestOn()) {
|
|
834
|
-
const sentBack = await tried(tip);
|
|
835
|
-
if (sentBack !== void 0) {
|
|
836
|
-
vetting.again = budget();
|
|
837
|
-
retry = sentBack;
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
const outcome = verdict ?? (flagged() ? await ruled(tip, vetting.again) : {
|
|
842
|
-
reference: tip.hash,
|
|
843
|
-
tip
|
|
844
|
-
});
|
|
845
|
-
if (!("kind" in outcome)) return outcome;
|
|
846
|
-
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
850
|
-
const refactorNote = async (reference, strays) => {
|
|
851
|
-
await agent("note", refactorNotePrompt(reference, strays), {
|
|
852
|
-
label: "Writing the refactor note",
|
|
853
|
-
file: REFACTOR_NOTE,
|
|
854
|
-
model: coder(),
|
|
855
|
-
system: builderSystem()
|
|
856
|
-
});
|
|
857
|
-
const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
|
|
858
|
-
const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
|
|
859
|
-
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")}` : ""}`);
|
|
860
|
-
return note;
|
|
861
|
-
};
|
|
862
|
-
const seamBuild = async (base) => {
|
|
863
|
-
const { reference, tip, shape } = await decide();
|
|
864
|
-
if (shape === void 0) return {
|
|
865
|
-
reference,
|
|
866
|
-
base,
|
|
867
|
-
land: true
|
|
868
|
-
};
|
|
869
|
-
if (shape.unreadable.length > 0) {
|
|
870
|
-
await scope("seam", () => human("by-hand", {
|
|
871
|
-
message: seamByHandMessage(shape.strays, shape.unreadable, reference),
|
|
872
|
-
label: "Reviewing unreadable files by hand",
|
|
873
|
-
acceptClean: true
|
|
874
|
-
}));
|
|
875
|
-
return {
|
|
876
|
-
reference,
|
|
877
|
-
base,
|
|
878
|
-
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
const strays = refactorStrays(shape);
|
|
882
|
-
if (strays.length === 0) return {
|
|
883
|
-
reference,
|
|
884
|
-
base,
|
|
885
|
-
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
886
|
-
};
|
|
887
|
-
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
888
|
-
const refactored = await reviewedRefactor(shape, {
|
|
889
|
-
reference,
|
|
890
|
-
note
|
|
891
|
-
});
|
|
892
|
-
return {
|
|
893
|
-
reference,
|
|
894
|
-
base: head(),
|
|
895
|
-
refactored,
|
|
896
|
-
rebuild: {
|
|
897
|
-
note,
|
|
898
|
-
acceptance: naiveAcceptance(tip)
|
|
899
|
-
}
|
|
900
|
-
};
|
|
901
|
-
};
|
|
902
|
-
//#endregion
|
|
903
|
-
//#region src/workflows/feature/feature.ts
|
|
904
|
-
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
905
|
-
const unwind = () => {
|
|
906
|
-
const commit = head();
|
|
907
|
-
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
908
|
-
};
|
|
909
|
-
/**
|
|
910
|
-
* A naive pass (judged only when a release flag is asked for), then implement and the
|
|
911
|
-
* quality lap, then the review; design must have run since `firstBase`.
|
|
912
|
-
*/
|
|
913
|
-
const buildFeature = async (firstBase) => {
|
|
914
|
-
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
915
|
-
const handedOut = new Set(listedIds());
|
|
916
|
-
const seam = await seamBuild(firstBase);
|
|
917
|
-
const used = gather(handedOut);
|
|
918
|
-
const requirements = read(REQUIREMENTS);
|
|
919
|
-
const closed = await implementing({
|
|
920
|
-
reference: seam.reference,
|
|
921
|
-
tier,
|
|
922
|
-
afterRefactor: seam.refactored,
|
|
923
|
-
naive: seam.land === true ? seam.reference : void 0,
|
|
924
|
-
rebuild: seam.rebuild
|
|
925
|
-
});
|
|
926
|
-
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
927
|
-
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
|
|
928
|
-
const outcome = await buildTail(base, {
|
|
929
|
-
green: true,
|
|
930
|
-
lensBase: base,
|
|
931
|
-
requirements
|
|
932
|
-
});
|
|
933
|
-
await featureFeedback(outcome, base, {
|
|
934
|
-
closed,
|
|
935
|
-
used
|
|
936
|
-
});
|
|
937
|
-
};
|
|
938
|
-
const listedIds = () => criterionIds(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
939
|
-
const gather = (handedOut) => {
|
|
940
|
-
for (const id of listedIds()) handedOut.add(id);
|
|
941
|
-
return [...handedOut];
|
|
942
|
-
};
|
|
943
|
-
const featureFeedback = async (first, lensBase, ids) => {
|
|
944
|
-
let outcome = first;
|
|
945
|
-
let closed = ids.closed;
|
|
946
|
-
const handedOut = /* @__PURE__ */ new Set([...ids.used, ...ids.closed]);
|
|
947
|
-
while (outcome.verdict === "feedback") {
|
|
948
|
-
await reUnwind(outcome);
|
|
949
|
-
const lap = {
|
|
950
|
-
closed,
|
|
951
|
-
used: [...handedOut]
|
|
952
|
-
};
|
|
953
|
-
await design(outcome.base, () => {
|
|
954
|
-
requireStableIds(REQUIREMENTS, lap);
|
|
955
|
-
gather(handedOut);
|
|
956
|
-
});
|
|
957
|
-
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
958
|
-
const requirements = read(REQUIREMENTS);
|
|
959
|
-
closed = await implementing({
|
|
960
|
-
tier,
|
|
961
|
-
amend: true
|
|
962
|
-
});
|
|
963
|
-
outcome = await buildTail(outcome.base, {
|
|
964
|
-
green: true,
|
|
965
|
-
lensBase,
|
|
966
|
-
requirements,
|
|
967
|
-
lap: "feedback"
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
};
|
|
971
|
-
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
972
|
-
const planAndBuild = async (firstBase, stated) => {
|
|
973
|
-
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
974
|
-
await buildFeature(firstBase);
|
|
975
|
-
};
|
|
976
|
-
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
|
|
977
|
-
const afterTail = async (first) => {
|
|
978
|
-
let outcome = first;
|
|
979
|
-
while (outcome.verdict === "feedback") {
|
|
980
|
-
await reUnwind(outcome);
|
|
981
|
-
await design(outcome.base);
|
|
982
|
-
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
983
|
-
const requirements = read(REQUIREMENTS);
|
|
984
|
-
await implement({
|
|
985
|
-
tier,
|
|
986
|
-
amend: true
|
|
987
|
-
});
|
|
988
|
-
outcome = await buildTail(outcome.base, {
|
|
989
|
-
green: true,
|
|
990
|
-
requirements
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
};
|
|
994
|
-
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
995
|
-
const ordinaryStart = async () => {
|
|
996
|
-
await human("idle", {
|
|
997
|
-
message: idleMessage(),
|
|
998
|
-
label: "Idle",
|
|
999
|
-
file: ".gtd/TODO.md"
|
|
1000
|
-
});
|
|
1001
|
-
refusePendingHandOff();
|
|
1002
|
-
naiveTestOn();
|
|
1003
|
-
const stated = statedCriteria(changes(), head());
|
|
1004
|
-
await unwind();
|
|
1005
|
-
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
1006
|
-
message: unwindFailedMessage(),
|
|
1007
|
-
label: "Could not unwind your input",
|
|
1008
|
-
file: FEEDBACK
|
|
1009
|
-
});
|
|
1010
|
-
await gate("start-gate", startGateBlockedMessage());
|
|
1011
|
-
await planAndBuild(start(), stated);
|
|
1012
|
-
};
|
|
1013
|
-
const feature = shipped(ordinaryStart);
|
|
1014
|
-
//#endregion
|
|
1015
|
-
//#region src/workflows/feature/index.ts
|
|
1016
|
-
var feature_default = feature;
|
|
1017
|
-
//#endregion
|
|
1018
|
-
export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, hasImpact, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireCriteria, requireCriteriaFrozen, requireImpact, requireImpactOnly, requireNoNewFlags, requireNoTestFiles, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unstableIds, unwind };
|