@pmelab/gtd 24.4.0 → 26.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 +15 -18
- package/dist/chunks/{reserved-B9pG93Fc.mjs → reserved-6pAgFGgB.mjs} +69 -125
- package/dist/gtd.bundle.mjs +28 -3
- package/dist/testkit.mjs +30 -5
- package/dist/workflow-kit.mjs +47 -90
- package/dist/workflows/feature.mjs +25 -47
- package/dist/workflows/fix.mjs +1 -1
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +135 -261
- package/package.json +2 -2
- package/src/cli/Cli.test.ts +25 -0
- package/src/cli/Cli.ts +12 -1
- package/src/flags/index.test.ts +67 -0
- package/src/flags/index.ts +39 -0
- package/src/replay/Replay.ts +2 -1
- package/src/workflow/vars.test.ts +16 -0
- package/src/workflow/vars.ts +4 -1
- package/src/workflow-kit/scenarios.test.ts +116 -0
- package/src/workflow-kit/scenarios.ts +53 -113
- package/src/workflows/feature/feature.ts +2 -5
- package/src/workflows/feature/packages.test.ts +17 -34
- package/src/workflows/feature/packages.ts +13 -52
- package/src/workflows/feature/planning.ts +1 -1
- package/src/workflows/feature/review.test.ts +0 -47
- package/src/workflows/feature/review.ts +6 -10
- package/src/workflows/index.ts +0 -2
- package/src/workflows/shared/health.test.ts +1 -21
- package/src/workflows/shared/health.ts +4 -39
- package/src/workflows/shared/options.ts +0 -1
- package/src/workflows/shared/steps.ts +16 -11
- package/src/workflows/shared/tail.test.ts +1 -41
- package/src/workflows/shared/tail.ts +14 -30
- package/src/workflows/shared/text.fixture.ts +1 -0
- package/src/workflows/shared/text.test.ts +23 -41
- package/src/workflows/shared/text.ts +54 -74
- package/src/workflows/shared/vars.ts +0 -1
- package/src/workflows/shared/scenarios.test.ts +0 -256
- package/src/workflows/shared/scenarios.ts +0 -35
package/dist/workflow-kit.mjs
CHANGED
|
@@ -67,102 +67,59 @@ const fixFindings = async (f) => {
|
|
|
67
67
|
};
|
|
68
68
|
//#endregion
|
|
69
69
|
//#region src/workflow-kit/scenarios.ts
|
|
70
|
-
const DOC_STRING = /^(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const scenarioText = (source) => {
|
|
70
|
+
const DOC_STRING = /^("""|```)/;
|
|
71
|
+
const THEN = /^Then\s/;
|
|
72
|
+
const CONTINUATION = /^(And|But|\*)\s/;
|
|
73
|
+
const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
|
|
74
|
+
/** Trimmed lines outside doc strings, whose content is test input, not steps. */
|
|
75
|
+
const stepLines = (source) => {
|
|
77
76
|
const out = [];
|
|
78
|
-
let
|
|
79
|
-
for (const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
fence: delimiter[2]
|
|
85
|
-
};
|
|
86
|
-
out.push(line.trim());
|
|
87
|
-
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
88
|
-
doc = void 0;
|
|
89
|
-
out.push(line.trim());
|
|
90
|
-
} else if (doc !== void 0) {
|
|
91
|
-
const lead = line.length - line.trimStart().length;
|
|
92
|
-
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
93
|
-
} else {
|
|
94
|
-
const trimmed = line.trim();
|
|
95
|
-
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return out.join("\n");
|
|
99
|
-
};
|
|
100
|
-
const textOf = (path) => {
|
|
101
|
-
const source = read(path);
|
|
102
|
-
return source === void 0 ? void 0 : scenarioText(source);
|
|
103
|
-
};
|
|
104
|
-
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
105
|
-
const freezeScenarios = (paths, previous) => {
|
|
106
|
-
if (paths.length === 0) return previous;
|
|
107
|
-
const texts = { ...previous?.texts };
|
|
108
|
-
for (const path of paths) {
|
|
109
|
-
const text = textOf(path);
|
|
110
|
-
if (text !== void 0) texts[path] = text;
|
|
77
|
+
let fence;
|
|
78
|
+
for (const trimmed of source.split("\n").map((l) => l.trim())) {
|
|
79
|
+
const opening = fence === void 0 ? DOC_STRING.exec(trimmed)?.[1] : void 0;
|
|
80
|
+
if (opening !== void 0) fence = opening;
|
|
81
|
+
else if (fence === void 0) out.push(trimmed);
|
|
82
|
+
else if (trimmed.startsWith(fence)) fence = void 0;
|
|
111
83
|
}
|
|
112
|
-
return
|
|
113
|
-
at: head(),
|
|
114
|
-
texts
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
/** Paths whose wording differs from the snapshot (a deleted file counts). */
|
|
118
|
-
const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
|
|
119
|
-
const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
|
|
120
|
-
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
121
|
-
const lcsTable = (a, b) => {
|
|
122
|
-
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
123
|
-
for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
124
|
-
return table;
|
|
84
|
+
return out;
|
|
125
85
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
86
|
+
const neutral = (line) => line === "" || line.startsWith("#") || line.startsWith("|");
|
|
87
|
+
/** Trimmed `Then` step lines, an `And`/`But`/`*` continuing one included; doc strings skipped. */
|
|
88
|
+
const assertionLines = (source) => {
|
|
129
89
|
const out = [];
|
|
130
|
-
let
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
90
|
+
let inThen = false;
|
|
91
|
+
let scenario = "";
|
|
92
|
+
for (const line of stepLines(source)) {
|
|
93
|
+
if (HEADING.test(line)) scenario = line;
|
|
94
|
+
if (THEN.test(line)) inThen = true;
|
|
95
|
+
else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false;
|
|
96
|
+
if (inThen && !neutral(line)) out.push({
|
|
97
|
+
scenario,
|
|
98
|
+
line
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
142
102
|
};
|
|
143
|
-
const
|
|
144
|
-
path,
|
|
145
|
-
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
146
|
-
});
|
|
103
|
+
const key = (a) => `${a.scenario}\n${a.line}`;
|
|
147
104
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
105
|
+
* Assertions a `.feature` file that existed before `changes` lost — the one
|
|
106
|
+
* way rewording a scenario can weaken it. Matched within each scenario and
|
|
107
|
+
* counted, so a line moving to another scenario, or one of two identical
|
|
108
|
+
* `Then`s dropped, still shows. A renamed scenario reports all of its own.
|
|
151
109
|
*/
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
};
|
|
110
|
+
const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".feature") && c.before !== void 0).map((c) => {
|
|
111
|
+
const left = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const a of assertionLines(c.after ?? "")) left.set(key(a), (left.get(key(a)) ?? 0) + 1);
|
|
113
|
+
const lines = assertionLines(c.before ?? "").filter((a) => {
|
|
114
|
+
const n = left.get(key(a)) ?? 0;
|
|
115
|
+
left.set(key(a), n - 1);
|
|
116
|
+
return n <= 0;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
path: c.path,
|
|
120
|
+
lines
|
|
121
|
+
};
|
|
122
|
+
}).filter((r) => r.lines.length > 0);
|
|
166
123
|
//#endregion
|
|
167
124
|
//#region src/workflow-kit/review.ts
|
|
168
125
|
const collect = async (p, capture) => {
|
|
@@ -351,4 +308,4 @@ const reviewToHandOff = async (base, p) => {
|
|
|
351
308
|
}
|
|
352
309
|
};
|
|
353
310
|
//#endregion
|
|
354
|
-
export {
|
|
311
|
+
export { assertionLines, escalation, fixFindings, gate, healthy, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, routeNotes, untilSettled };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { $ as
|
|
2
|
+
import { review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
+
import { $ as unwindFailedMessage, A as architectureDecomposePrompt, B as designSystem, C as fixRisks, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as packagesItemBuildingPrompt, I as buildReviewTriageMessage, J as reviewEditsCapture, K as packagesItemFixSuitePrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as startGateBlockedMessage, R as deferNotesScript, S as fixCheck, T as reviewMissing, V as designTriagePrompt, W as idleMessage, X as reviewNotesCapture, Z as reviewerSystem, _ as FEEDBACK, b as awaitReview, c as refusePendingHandOff, d as defaults, et as unwindFailure, f as envDefaults, g as ARCHITECTURE, h as healthy, i as skills, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as tail, m as gate, n as summary, o as closeReview, q as reviewEditNotesCapture, r as access, t as steering, u as shipped, v as REQUIREMENTS, w as planner, x as coder, y as REVIEW, z as designGateAnswerMessage } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
4
4
|
//#region src/workflows/feature/steps.ts
|
|
5
5
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
6
6
|
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
@@ -78,8 +78,6 @@ const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes,
|
|
|
78
78
|
});
|
|
79
79
|
//#endregion
|
|
80
80
|
//#region src/workflows/feature/packages.ts
|
|
81
|
-
/** Package 0: the e2e scenarios, written red. */
|
|
82
|
-
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
83
81
|
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
84
82
|
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
85
83
|
const declaredTests = (packageText) => {
|
|
@@ -109,47 +107,29 @@ const requireDeclaredTests = (pkg, since) => {
|
|
|
109
107
|
if (missing.length === 0) return;
|
|
110
108
|
refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
|
|
111
109
|
};
|
|
112
|
-
/**
|
|
113
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Build `pkg`, check its declared tests, keep the full suite green, and close
|
|
112
|
+
* it out, which removes it — and `.gtd/ARCHITECTURE.md` with the last one.
|
|
113
|
+
*/
|
|
114
|
+
const packageItem = async (pkg) => {
|
|
114
115
|
const from = head();
|
|
115
116
|
const declared = (turn) => async () => {
|
|
116
117
|
await turn();
|
|
117
118
|
requireDeclaredTests(pkg, from);
|
|
118
119
|
};
|
|
119
|
-
await
|
|
120
|
-
await healthy(
|
|
121
|
-
await run("closing", removeScript([
|
|
122
|
-
return {
|
|
120
|
+
await declared(() => build(pkg))();
|
|
121
|
+
await healthy(declared(fixSuite));
|
|
122
|
+
await run("closing", removeScript([
|
|
123
123
|
pkg,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
};
|
|
124
|
+
".gtd/SATISFIED.md",
|
|
125
|
+
...glob(".gtd/packages/*.md").length === 1 ? [ARCHITECTURE] : []
|
|
126
|
+
]), { label: "Closing out the package" });
|
|
127
127
|
};
|
|
128
128
|
/** The first queued package — the one a package step works on. */
|
|
129
129
|
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
130
|
-
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
131
130
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
132
|
-
const packages = (
|
|
133
|
-
let
|
|
134
|
-
const ranges = [];
|
|
135
|
-
let touched = [];
|
|
136
|
-
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
137
|
-
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
138
|
-
ranges.push(range);
|
|
139
|
-
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
140
|
-
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
141
|
-
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
145
|
-
return {
|
|
146
|
-
ranges,
|
|
147
|
-
scenarios: {
|
|
148
|
-
added: paths("added"),
|
|
149
|
-
changed: paths("modified")
|
|
150
|
-
},
|
|
151
|
-
frozen
|
|
152
|
-
};
|
|
131
|
+
const packages = () => scope("packages", async () => {
|
|
132
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) await scope("item", () => packageItem(pkg));
|
|
153
133
|
});
|
|
154
134
|
//#endregion
|
|
155
135
|
//#region src/workflows/feature/planning.ts
|
|
@@ -182,7 +162,7 @@ const architecture = () => scope("architecture", async () => {
|
|
|
182
162
|
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
183
163
|
await decompose();
|
|
184
164
|
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
185
|
-
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the
|
|
165
|
+
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the packages are built from it; it is removed with the last package");
|
|
186
166
|
});
|
|
187
167
|
//#endregion
|
|
188
168
|
//#region src/workflows/feature/review.ts
|
|
@@ -308,7 +288,7 @@ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
|
308
288
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
309
289
|
* batch, praise dropped.
|
|
310
290
|
*/
|
|
311
|
-
const review = (base, escalations = { rounds: 0 }
|
|
291
|
+
const review = (base, escalations = { rounds: 0 }) => {
|
|
312
292
|
const severities = /* @__PURE__ */ new Map();
|
|
313
293
|
let deferred = [];
|
|
314
294
|
return review$1(base, {
|
|
@@ -319,8 +299,8 @@ const review = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
319
299
|
deferred = [];
|
|
320
300
|
return reviewing(b, carry);
|
|
321
301
|
},
|
|
322
|
-
fixRisks
|
|
323
|
-
keepGreen: () => healthy(
|
|
302
|
+
fixRisks,
|
|
303
|
+
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
324
304
|
awaitReview: (b) => {
|
|
325
305
|
const notes = deferred;
|
|
326
306
|
deferred = [];
|
|
@@ -339,7 +319,7 @@ const review = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
339
319
|
return deferNotes(deferred, head());
|
|
340
320
|
},
|
|
341
321
|
answerQuestions: answerReviewQuestions,
|
|
342
|
-
fixNits
|
|
322
|
+
fixNits,
|
|
343
323
|
close: closeReview,
|
|
344
324
|
captures: {
|
|
345
325
|
edits: reviewEditsCapture,
|
|
@@ -361,8 +341,8 @@ const review = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
361
341
|
}
|
|
362
342
|
});
|
|
363
343
|
};
|
|
364
|
-
/** The build tail: fix (when entered red), keep green
|
|
365
|
-
const buildTail = (fixFirst, base,
|
|
344
|
+
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
345
|
+
const buildTail = (fixFirst, base, green = false) => tail(fixFirst, (escalations) => review(base, escalations), green);
|
|
366
346
|
//#endregion
|
|
367
347
|
//#region src/workflows/feature/feature.ts
|
|
368
348
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
@@ -389,13 +369,11 @@ const reUnwind = async (feedback) => {
|
|
|
389
369
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
390
370
|
const planAndBuild = async (firstBase) => {
|
|
391
371
|
let base = firstBase;
|
|
392
|
-
let carried;
|
|
393
372
|
for (;;) {
|
|
394
373
|
await design(base);
|
|
395
374
|
await architecture();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const outcome = await buildTail(false, base, built);
|
|
375
|
+
await packages();
|
|
376
|
+
const outcome = await buildTail(false, base, true);
|
|
399
377
|
if (outcome.verdict === "signoff") return;
|
|
400
378
|
await reUnwind(outcome);
|
|
401
379
|
base = outcome.base;
|
|
@@ -429,4 +407,4 @@ const feature = shipped(ordinaryStart);
|
|
|
429
407
|
//#region src/workflows/feature/index.ts
|
|
430
408
|
var feature_default = feature;
|
|
431
409
|
//#endregion
|
|
432
|
-
export {
|
|
410
|
+
export { access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, a as REVIEW_ORIG, d as defaults, f as envDefaults, i as skills, n as summary, p as baseline, r as access, s as handOffTail, t as steering, u as shipped, x as coder, y as REVIEW } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
4
4
|
//#region src/workflows/fix/fix.ts
|
|
5
5
|
/**
|
|
6
6
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Y as reviewGateBlockedMessage, c as refusePendingHandOff, d as defaults, f as envDefaults, i as skills, m as gate, n as summary, r as access, s as handOffTail, t as steering, u as shipped } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
3
3
|
//#region src/workflows/review/review.ts
|
|
4
4
|
/**
|
|
5
5
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|