@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/workflows.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { dirname } from "node:path";
|
|
2
1
|
//#region \0rolldown/runtime.js
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -330,13 +329,6 @@ const escalation$1 = async (count, steps) => {
|
|
|
330
329
|
await steps.describe();
|
|
331
330
|
await steps.stop();
|
|
332
331
|
};
|
|
333
|
-
/** Run `check` again while `pending()` holds — a run then does not count — and resolve the first run that does. */
|
|
334
|
-
const untilSettled = async (check, pending) => {
|
|
335
|
-
for (;;) {
|
|
336
|
-
const green = await check();
|
|
337
|
-
if (!pending()) return green;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
332
|
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
341
333
|
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
342
334
|
/**
|
|
@@ -389,102 +381,59 @@ const fixFindings = async (f) => {
|
|
|
389
381
|
};
|
|
390
382
|
//#endregion
|
|
391
383
|
//#region src/workflow-kit/scenarios.ts
|
|
392
|
-
const DOC_STRING = /^(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const scenarioText = (source) => {
|
|
384
|
+
const DOC_STRING = /^("""|```)/;
|
|
385
|
+
const THEN = /^Then\s/;
|
|
386
|
+
const CONTINUATION = /^(And|But|\*)\s/;
|
|
387
|
+
const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
|
|
388
|
+
/** Trimmed lines outside doc strings, whose content is test input, not steps. */
|
|
389
|
+
const stepLines = (source) => {
|
|
399
390
|
const out = [];
|
|
400
|
-
let
|
|
401
|
-
for (const
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
fence: delimiter[2]
|
|
407
|
-
};
|
|
408
|
-
out.push(line.trim());
|
|
409
|
-
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
410
|
-
doc = void 0;
|
|
411
|
-
out.push(line.trim());
|
|
412
|
-
} else if (doc !== void 0) {
|
|
413
|
-
const lead = line.length - line.trimStart().length;
|
|
414
|
-
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
415
|
-
} else {
|
|
416
|
-
const trimmed = line.trim();
|
|
417
|
-
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
418
|
-
}
|
|
391
|
+
let fence;
|
|
392
|
+
for (const trimmed of source.split("\n").map((l) => l.trim())) {
|
|
393
|
+
const opening = fence === void 0 ? DOC_STRING.exec(trimmed)?.[1] : void 0;
|
|
394
|
+
if (opening !== void 0) fence = opening;
|
|
395
|
+
else if (fence === void 0) out.push(trimmed);
|
|
396
|
+
else if (trimmed.startsWith(fence)) fence = void 0;
|
|
419
397
|
}
|
|
420
|
-
return out
|
|
421
|
-
};
|
|
422
|
-
const textOf = (path) => {
|
|
423
|
-
const source = read(path);
|
|
424
|
-
return source === void 0 ? void 0 : scenarioText(source);
|
|
425
|
-
};
|
|
426
|
-
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
427
|
-
const freezeScenarios = (paths, previous) => {
|
|
428
|
-
if (paths.length === 0) return previous;
|
|
429
|
-
const texts = { ...previous?.texts };
|
|
430
|
-
for (const path of paths) {
|
|
431
|
-
const text = textOf(path);
|
|
432
|
-
if (text !== void 0) texts[path] = text;
|
|
433
|
-
}
|
|
434
|
-
return Object.keys(texts).length === 0 ? previous : {
|
|
435
|
-
at: head$1(),
|
|
436
|
-
texts
|
|
437
|
-
};
|
|
398
|
+
return out;
|
|
438
399
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
443
|
-
const lcsTable = (a, b) => {
|
|
444
|
-
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
445
|
-
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]);
|
|
446
|
-
return table;
|
|
447
|
-
};
|
|
448
|
-
/** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
|
|
449
|
-
const lineDiff = (a, b) => {
|
|
450
|
-
const table = lcsTable(a, b);
|
|
400
|
+
const neutral = (line) => line === "" || line.startsWith("#") || line.startsWith("|");
|
|
401
|
+
/** Trimmed `Then` step lines, an `And`/`But`/`*` continuing one included; doc strings skipped. */
|
|
402
|
+
const assertionLines = (source) => {
|
|
451
403
|
const out = [];
|
|
452
|
-
let
|
|
453
|
-
let
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
404
|
+
let inThen = false;
|
|
405
|
+
let scenario = "";
|
|
406
|
+
for (const line of stepLines(source)) {
|
|
407
|
+
if (HEADING.test(line)) scenario = line;
|
|
408
|
+
if (THEN.test(line)) inThen = true;
|
|
409
|
+
else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false;
|
|
410
|
+
if (inThen && !neutral(line)) out.push({
|
|
411
|
+
scenario,
|
|
412
|
+
line
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
464
416
|
};
|
|
465
|
-
const
|
|
466
|
-
path,
|
|
467
|
-
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
468
|
-
});
|
|
417
|
+
const key = (a) => `${a.scenario}\n${a.line}`;
|
|
469
418
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
419
|
+
* Assertions a `.feature` file that existed before `changes` lost — the one
|
|
420
|
+
* way rewording a scenario can weaken it. Matched within each scenario and
|
|
421
|
+
* counted, so a line moving to another scenario, or one of two identical
|
|
422
|
+
* `Then`s dropped, still shows. A renamed scenario reports all of its own.
|
|
473
423
|
*/
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
};
|
|
424
|
+
const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".feature") && c.before !== void 0).map((c) => {
|
|
425
|
+
const left = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const a of assertionLines(c.after ?? "")) left.set(key(a), (left.get(key(a)) ?? 0) + 1);
|
|
427
|
+
const lines = assertionLines(c.before ?? "").filter((a) => {
|
|
428
|
+
const n = left.get(key(a)) ?? 0;
|
|
429
|
+
left.set(key(a), n - 1);
|
|
430
|
+
return n <= 0;
|
|
431
|
+
});
|
|
432
|
+
return {
|
|
433
|
+
path: c.path,
|
|
434
|
+
lines
|
|
435
|
+
};
|
|
436
|
+
}).filter((r) => r.lines.length > 0);
|
|
488
437
|
//#endregion
|
|
489
438
|
//#region node_modules/mdast-util-to-string/lib/index.js
|
|
490
439
|
/**
|
|
@@ -17146,7 +17095,8 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
17146
17095
|
- Every open point here is TECHNICAL — triage already resolved
|
|
17147
17096
|
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
17148
17097
|
yourself unless you genuinely cannot defend a default; a wrong
|
|
17149
|
-
technical call is still caught by
|
|
17098
|
+
technical call is still caught by each package's suite run and the
|
|
17099
|
+
quality lap
|
|
17150
17100
|
- The narrow exception: a simplification that drops something
|
|
17151
17101
|
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
17152
17102
|
silent default
|
|
@@ -17158,11 +17108,13 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
17158
17108
|
only, as fenced TypeScript (or the repository's language)
|
|
17159
17109
|
3. \`## Call Stacks\` — seam level, entry point → module
|
|
17160
17110
|
boundary; one line per hop, never per line of code
|
|
17161
|
-
4. \`## E2E Scenarios\` —
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17165
|
-
|
|
17111
|
+
4. \`## E2E Scenarios\` — one line per user-visible behaviour,
|
|
17112
|
+
\`- e2e: <path> — <behaviour> (covers <concern>)\`, in plain
|
|
17113
|
+
words: the builder writes the scenario in the repository's
|
|
17114
|
+
own e2e framework, so never spell out Gherkin here. With no
|
|
17115
|
+
user-visible behaviour change, the literal line
|
|
17116
|
+
\`No e2e change.\` plus a one-line reason — never empty or
|
|
17117
|
+
missing
|
|
17166
17118
|
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
17167
17119
|
\`## Interfaces\`, each test a line
|
|
17168
17120
|
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
@@ -17188,35 +17140,31 @@ const architectureDecomposePrompt = () => `${styleBlock}
|
|
|
17188
17140
|
${stateFileRules}
|
|
17189
17141
|
- The only state files this turn touches are the package files
|
|
17190
17142
|
under \`.gtd/packages/\` — no other files for notes or output.
|
|
17191
|
-
Never delete \`.gtd/ARCHITECTURE.md\`:
|
|
17192
|
-
|
|
17143
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: package files may
|
|
17144
|
+
reference it, and it is removed with the last package
|
|
17193
17145
|
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
17194
17146
|
conversation, otherwise read it (the converged technical
|
|
17195
17147
|
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
17196
17148
|
and unit tests — this turn groups them into packages. A
|
|
17197
17149
|
\`## Merged Concerns\` heading there records merges, never a
|
|
17198
17150
|
concern of its own: write no package file for it
|
|
17199
|
-
- A package is
|
|
17200
|
-
green
|
|
17201
|
-
|
|
17202
|
-
same files stay in one package unless
|
|
17203
|
-
an interface the earlier one creates.
|
|
17204
|
-
|
|
17205
|
-
interface-introducing packages first
|
|
17151
|
+
- A package is one concern's slice, and each must leave the full
|
|
17152
|
+
suite green — every package boundary costs a full run, so write
|
|
17153
|
+
as few as the concerns allow. Keep the merge rule: concerns whose
|
|
17154
|
+
footprints centre on the same files stay in one package unless
|
|
17155
|
+
the later one only consumes an interface the earlier one creates.
|
|
17156
|
+
Order them interface-introducing packages first
|
|
17206
17157
|
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
17207
17158
|
requirement(s) — both, independently, if merged — its tasks, and
|
|
17208
17159
|
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
17209
17160
|
relevant paths:
|
|
17210
|
-
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
- the last numbered package is the wiring package: CLI entry,
|
|
17218
|
-
workflow composition, config. A red e2e after it is a bug, not
|
|
17219
|
-
missing work
|
|
17161
|
+
- \`01-…\` onward — one vertical slice each: the unit tests,
|
|
17162
|
+
the code, the wiring (CLI entry, workflow composition, config)
|
|
17163
|
+
and every \`## E2E Scenarios\` entry its concern covers, so the
|
|
17164
|
+
slice ends with its scenarios passing. A requirement no test
|
|
17165
|
+
covers (docs, README, deletions, config) is a task of the slice
|
|
17166
|
+
it belongs to; only one no slice owns becomes a chore package,
|
|
17167
|
+
which declares no tests
|
|
17220
17168
|
- Every package with tests has a \`## Tests\` section, one line per
|
|
17221
17169
|
declared test, copied from the architecture entries:
|
|
17222
17170
|
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
@@ -17262,10 +17210,10 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
|
17262
17210
|
- Write the tests the package declares under \`## Tests\` first,
|
|
17263
17211
|
then the code that makes them pass; the turn is refused unless
|
|
17264
17212
|
every declared test is in your diff
|
|
17265
|
-
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17213
|
+
- A declared e2e test is a scenario in the repository's own e2e
|
|
17214
|
+
framework for the behaviour its \`## E2E Scenarios\` entry names.
|
|
17215
|
+
Word it to fit the existing steps — reuse and generalise them
|
|
17216
|
+
rather than adding one-off ones
|
|
17269
17217
|
- Implement every task the package describes, no more, no less,
|
|
17270
17218
|
fanning independent ones out to parallel subagents where your
|
|
17271
17219
|
harness supports it; leave other package files untouched
|
|
@@ -17319,22 +17267,9 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
|
17319
17267
|
turn, or land untouched to give it one more attempt at the same
|
|
17320
17268
|
analysis.
|
|
17321
17269
|
`;
|
|
17322
|
-
const
|
|
17323
|
-
const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
|
|
17324
|
-
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
17325
|
-
- A red scenario that is NEW in this plan likely means missing wiring
|
|
17326
|
-
between packages; new scenarios:
|
|
17327
|
-
${list(built.scenarios.added)}
|
|
17328
|
-
- A red CHANGED or existing scenario or test is a regression a package
|
|
17329
|
-
introduced; scenarios this plan changed:
|
|
17330
|
-
${list(built.scenarios.changed)}
|
|
17331
|
-
- Each package's commits, to bisect a break:
|
|
17332
|
-
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
17333
|
-
`;
|
|
17334
|
-
};
|
|
17335
|
-
const buildFixPrompt = (built) => `${stateFileRules}
|
|
17270
|
+
const buildFixPrompt = () => `${stateFileRules}
|
|
17336
17271
|
${fixFeedbackPrompt}
|
|
17337
|
-
|
|
17272
|
+
- Leave everything uncommitted — do not commit
|
|
17338
17273
|
`;
|
|
17339
17274
|
const finisherSystem = () => `${finisherPersona}
|
|
17340
17275
|
|
|
@@ -17655,7 +17590,18 @@ actionability, and never dismiss a real note or edit as approval.
|
|
|
17655
17590
|
- If not: finish without writing anything — changing nothing is
|
|
17656
17591
|
the sign-off
|
|
17657
17592
|
`;
|
|
17658
|
-
const
|
|
17593
|
+
const scenariosPrompt = (removed) => `- When a scenario in a \`.feature\` file was added or changed since the
|
|
17594
|
+
base, the first chunk is \`## Scenarios\`: one pointer per such
|
|
17595
|
+
scenario, its note saying in plain words what behaviour it now pins
|
|
17596
|
+
down — the human signs off the scenarios here, nowhere else
|
|
17597
|
+
${removed.length === 0 ? "" : `- These assertions of scenarios that existed at the base are gone:
|
|
17598
|
+
${removed.map(({ path, lines }) => lines.map((a) => ` ${path} — ${a.scenario} — ${a.line}\n`).join("")).join("")} Open the note of each scenario that lost one with \`Weakened:\`,
|
|
17599
|
+
naming the lost line and whether \`.gtd/REQUIREMENTS.md\` asked
|
|
17600
|
+
for it. A scenario that no longer exists has no pointer of its own:
|
|
17601
|
+
put its \`Weakened:\` note on a pointer of the chunk covering its
|
|
17602
|
+
removal
|
|
17603
|
+
`}`;
|
|
17604
|
+
const buildReviewReviewingPrompt = (base, carry, removed = []) => `${styleBlock}
|
|
17659
17605
|
|
|
17660
17606
|
${styleFormatContract}
|
|
17661
17607
|
|
|
@@ -17697,7 +17643,7 @@ review the changes:
|
|
|
17697
17643
|
A note sitting entirely on the line(s) beneath the pointer is
|
|
17698
17644
|
also valid. Either way, the note must never start with a bare \`./path\` token
|
|
17699
17645
|
— that parses as a second pointer, not a note
|
|
17700
|
-
No diff is given — read the changes yourself. The range runs
|
|
17646
|
+
${scenariosPrompt(removed)}No diff is given — read the changes yourself. The range runs
|
|
17701
17647
|
from \`${base}\` to the working tree (committed turns
|
|
17702
17648
|
plus anything pending); on a feedback round that's the previous
|
|
17703
17649
|
review's boundary, so it covers only what's new.
|
|
@@ -17744,20 +17690,6 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
|
17744
17690
|
Print the closing message and stop — this writes nothing itself.
|
|
17745
17691
|
`;
|
|
17746
17692
|
};
|
|
17747
|
-
const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
|
|
17748
|
-
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
17749
|
-
|
|
17750
|
-
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
17751
|
-
|
|
17752
|
-
To accept the change, land untouched.
|
|
17753
|
-
|
|
17754
|
-
To reject it, run
|
|
17755
|
-
|
|
17756
|
-
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
17757
|
-
|
|
17758
|
-
and land. Restoring only some of the files is a partial accept: every file
|
|
17759
|
-
still differing from the frozen wording is accepted.
|
|
17760
|
-
`;
|
|
17761
17693
|
//#endregion
|
|
17762
17694
|
//#region src/workflows/shared/steps.ts
|
|
17763
17695
|
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
@@ -17768,7 +17700,7 @@ const REVIEW = ".gtd/REVIEW.md";
|
|
|
17768
17700
|
const QUALITY = ".gtd/QUALITY.md";
|
|
17769
17701
|
const planner = () => env.plannerModel ?? "";
|
|
17770
17702
|
const coder = () => env.coderModel ?? "";
|
|
17771
|
-
const fixCheck = (
|
|
17703
|
+
const fixCheck = () => agentWithSkills("fix", buildFixPrompt(), {
|
|
17772
17704
|
label: "Fixing the check",
|
|
17773
17705
|
file: FEEDBACK,
|
|
17774
17706
|
model: coder(),
|
|
@@ -17824,7 +17756,7 @@ const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPr
|
|
|
17824
17756
|
allowEmpty: true
|
|
17825
17757
|
});
|
|
17826
17758
|
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
17827
|
-
const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
|
|
17759
|
+
const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))), {
|
|
17828
17760
|
label: "Reviewing",
|
|
17829
17761
|
file: REVIEW,
|
|
17830
17762
|
mode: "review",
|
|
@@ -17855,25 +17787,13 @@ const reviewMissing = (round) => human("review.review-missing", {
|
|
|
17855
17787
|
});
|
|
17856
17788
|
//#endregion
|
|
17857
17789
|
//#region src/workflows/shared/health.ts
|
|
17858
|
-
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
17859
|
-
const SETUP = ".gtd/SETUP.md";
|
|
17860
|
-
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND.";
|
|
17861
|
-
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
17862
|
-
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
17863
|
-
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
17864
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
17865
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
17866
|
-
"exit 0"
|
|
17867
|
-
] : [`rm -f ${quote(SETUP)}`];
|
|
17868
17790
|
/** Fix turns a run of red checks gets before it escalates. */
|
|
17869
17791
|
const FIX_CAP = 3;
|
|
17870
|
-
const unconfigured = () => read(SETUP) !== void 0;
|
|
17871
17792
|
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
17872
|
-
const baseline = (name, label = "Checking the baseline") =>
|
|
17793
|
+
const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
|
|
17873
17794
|
report: FEEDBACK,
|
|
17874
|
-
label
|
|
17875
|
-
|
|
17876
|
-
}), unconfigured);
|
|
17795
|
+
label
|
|
17796
|
+
});
|
|
17877
17797
|
const escalationSteps = {
|
|
17878
17798
|
limit: 2,
|
|
17879
17799
|
describe: describeEscalation,
|
|
@@ -17911,22 +17831,18 @@ const sameFailure = async (previous, current) => {
|
|
|
17911
17831
|
});
|
|
17912
17832
|
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
17913
17833
|
};
|
|
17914
|
-
const runSuite = (options) => {
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
|
|
17920
|
-
...fast ? { preamble: fastSuiteGuard() } : {}
|
|
17921
|
-
});
|
|
17922
|
-
};
|
|
17834
|
+
const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
17835
|
+
report: FEEDBACK,
|
|
17836
|
+
label: "Running checks",
|
|
17837
|
+
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []]
|
|
17838
|
+
});
|
|
17923
17839
|
/**
|
|
17924
17840
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
17925
17841
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
17926
17842
|
* identical to the previous round's.
|
|
17927
17843
|
*/
|
|
17928
17844
|
const healthy = (fix, options = {}) => healthy$1(fix, {
|
|
17929
|
-
check:
|
|
17845
|
+
check: () => runSuite(options),
|
|
17930
17846
|
report: FEEDBACK,
|
|
17931
17847
|
fixCap: 3,
|
|
17932
17848
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
@@ -37268,7 +37184,6 @@ const defaults = {
|
|
|
37268
37184
|
/** Environment settings. */
|
|
37269
37185
|
const envDefaults = {
|
|
37270
37186
|
testCommand: "npm test",
|
|
37271
|
-
fastTestCommand: "",
|
|
37272
37187
|
plannerModel: "smart",
|
|
37273
37188
|
coderModel: "base"
|
|
37274
37189
|
};
|
|
@@ -37283,7 +37198,6 @@ const ShippedOptions = Struct({
|
|
|
37283
37198
|
reviewSeverityMinP: setting,
|
|
37284
37199
|
qualityReviews: setting,
|
|
37285
37200
|
testCommand: setting,
|
|
37286
|
-
fastTestCommand: setting,
|
|
37287
37201
|
plannerModel: setting,
|
|
37288
37202
|
coderModel: setting
|
|
37289
37203
|
});
|
|
@@ -37303,21 +37217,6 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
|
|
|
37303
37217
|
base
|
|
37304
37218
|
}));
|
|
37305
37219
|
//#endregion
|
|
37306
|
-
//#region src/workflows/shared/scenarios.ts
|
|
37307
|
-
/** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
|
|
37308
|
-
const holdWording = (frozen) => holdWording$1(frozen, (at, drifted) => human("scenario-wording", {
|
|
37309
|
-
message: scenarioWordingMessage(at, drifted),
|
|
37310
|
-
label: "Scenario wording changed",
|
|
37311
|
-
acceptClean: true,
|
|
37312
|
-
base: at
|
|
37313
|
-
}));
|
|
37314
|
-
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
37315
|
-
const guarded = (frozen, turn, within) => async () => {
|
|
37316
|
-
await turn();
|
|
37317
|
-
if (within === void 0) await holdWording(frozen);
|
|
37318
|
-
else await scope(within, () => holdWording(frozen));
|
|
37319
|
-
};
|
|
37320
|
-
//#endregion
|
|
37321
37220
|
//#region src/workflows/shared/tail.ts
|
|
37322
37221
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
37323
37222
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
@@ -37328,8 +37227,8 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
|
37328
37227
|
*/
|
|
37329
37228
|
const qualityLap = () => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY);
|
|
37330
37229
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
37331
|
-
const fixQualityFindings = (escalations
|
|
37332
|
-
fix:
|
|
37230
|
+
const fixQualityFindings = (escalations) => fixFindings({
|
|
37231
|
+
fix: fixQuality,
|
|
37333
37232
|
findings: QUALITY,
|
|
37334
37233
|
cap: 3,
|
|
37335
37234
|
escalate: () => escalation(escalations)
|
|
@@ -37345,27 +37244,24 @@ const refusePendingHandOff = () => {
|
|
|
37345
37244
|
};
|
|
37346
37245
|
/**
|
|
37347
37246
|
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
37348
|
-
* `finish` — the human review, as the calling workflow runs it.
|
|
37247
|
+
* `finish` — the human review, as the calling workflow runs it. `green`: the
|
|
37248
|
+
* caller just left the full suite green (the package loop), so no run opens it.
|
|
37349
37249
|
*/
|
|
37350
|
-
const tail = (fixFirst, finish,
|
|
37351
|
-
const frozen = built?.frozen;
|
|
37250
|
+
const tail = (fixFirst, finish, green = false) => scope("build", async () => {
|
|
37352
37251
|
const escalations = { rounds: 0 };
|
|
37353
|
-
const
|
|
37252
|
+
const fix = fixCheck;
|
|
37354
37253
|
const repair = async () => {
|
|
37355
|
-
await
|
|
37356
|
-
await healthy(
|
|
37254
|
+
await fix();
|
|
37255
|
+
await healthy(fix, {
|
|
37357
37256
|
fixesSoFar: 1,
|
|
37358
37257
|
escalations
|
|
37359
37258
|
});
|
|
37360
37259
|
};
|
|
37361
37260
|
if (fixFirst) await repair();
|
|
37362
|
-
else await healthy(
|
|
37363
|
-
|
|
37364
|
-
sweepOnGreen: [ARCHITECTURE]
|
|
37365
|
-
});
|
|
37366
|
-
if (await qualityLap() === "findings") if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
|
|
37261
|
+
else if (!green) await healthy(fix, { escalations });
|
|
37262
|
+
if (await qualityLap() === "findings") if (await fixQualityFindings(escalations)) await healthy(fix, { escalations });
|
|
37367
37263
|
else await repair();
|
|
37368
|
-
return finish(escalations
|
|
37264
|
+
return finish(escalations);
|
|
37369
37265
|
});
|
|
37370
37266
|
/**
|
|
37371
37267
|
* The review and fix workflows' human review since `base`: a sign-off closes
|
|
@@ -37376,7 +37272,7 @@ const handOffReview = (base, escalations) => reviewToHandOff(base, {
|
|
|
37376
37272
|
file: REVIEW,
|
|
37377
37273
|
reviewing,
|
|
37378
37274
|
fixRisks,
|
|
37379
|
-
keepGreen: () => healthy(
|
|
37275
|
+
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
37380
37276
|
awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
|
|
37381
37277
|
reviewMissing,
|
|
37382
37278
|
close: closeReview,
|
|
@@ -37465,8 +37361,6 @@ const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes,
|
|
|
37465
37361
|
});
|
|
37466
37362
|
//#endregion
|
|
37467
37363
|
//#region src/workflows/feature/packages.ts
|
|
37468
|
-
/** Package 0: the e2e scenarios, written red. */
|
|
37469
|
-
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
37470
37364
|
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
37471
37365
|
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
37472
37366
|
const declaredTests = (packageText) => {
|
|
@@ -37496,47 +37390,29 @@ const requireDeclaredTests = (pkg, since) => {
|
|
|
37496
37390
|
if (missing.length === 0) return;
|
|
37497
37391
|
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")}`);
|
|
37498
37392
|
};
|
|
37499
|
-
/**
|
|
37500
|
-
|
|
37393
|
+
/**
|
|
37394
|
+
* Build `pkg`, check its declared tests, keep the full suite green, and close
|
|
37395
|
+
* it out, which removes it — and `.gtd/ARCHITECTURE.md` with the last one.
|
|
37396
|
+
*/
|
|
37397
|
+
const packageItem = async (pkg) => {
|
|
37501
37398
|
const from = head$1();
|
|
37502
37399
|
const declared = (turn) => async () => {
|
|
37503
37400
|
await turn();
|
|
37504
37401
|
requireDeclaredTests(pkg, from);
|
|
37505
37402
|
};
|
|
37506
|
-
await
|
|
37507
|
-
await healthy(
|
|
37508
|
-
await run("closing", removeScript([
|
|
37509
|
-
return {
|
|
37403
|
+
await declared(() => build(pkg))();
|
|
37404
|
+
await healthy(declared(fixSuite));
|
|
37405
|
+
await run("closing", removeScript([
|
|
37510
37406
|
pkg,
|
|
37511
|
-
|
|
37512
|
-
|
|
37513
|
-
};
|
|
37407
|
+
".gtd/SATISFIED.md",
|
|
37408
|
+
...glob(".gtd/packages/*.md").length === 1 ? [ARCHITECTURE] : []
|
|
37409
|
+
]), { label: "Closing out the package" });
|
|
37514
37410
|
};
|
|
37515
37411
|
/** The first queued package — the one a package step works on. */
|
|
37516
37412
|
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
37517
|
-
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37518
37413
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
37519
|
-
const packages = (
|
|
37520
|
-
let
|
|
37521
|
-
const ranges = [];
|
|
37522
|
-
let touched = [];
|
|
37523
|
-
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
37524
|
-
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
37525
|
-
ranges.push(range);
|
|
37526
|
-
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
37527
|
-
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
37528
|
-
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
37529
|
-
}
|
|
37530
|
-
}
|
|
37531
|
-
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
37532
|
-
return {
|
|
37533
|
-
ranges,
|
|
37534
|
-
scenarios: {
|
|
37535
|
-
added: paths("added"),
|
|
37536
|
-
changed: paths("modified")
|
|
37537
|
-
},
|
|
37538
|
-
frozen
|
|
37539
|
-
};
|
|
37414
|
+
const packages = () => scope("packages", async () => {
|
|
37415
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) await scope("item", () => packageItem(pkg));
|
|
37540
37416
|
});
|
|
37541
37417
|
//#endregion
|
|
37542
37418
|
//#region src/workflows/feature/planning.ts
|
|
@@ -37569,7 +37445,7 @@ const architecture = () => scope("architecture", async () => {
|
|
|
37569
37445
|
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
37570
37446
|
await decompose();
|
|
37571
37447
|
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
37572
|
-
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the
|
|
37448
|
+
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");
|
|
37573
37449
|
});
|
|
37574
37450
|
//#endregion
|
|
37575
37451
|
//#region src/workflows/feature/review.ts
|
|
@@ -37695,7 +37571,7 @@ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
|
37695
37571
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
37696
37572
|
* batch, praise dropped.
|
|
37697
37573
|
*/
|
|
37698
|
-
const review$1 = (base, escalations = { rounds: 0 }
|
|
37574
|
+
const review$1 = (base, escalations = { rounds: 0 }) => {
|
|
37699
37575
|
const severities = /* @__PURE__ */ new Map();
|
|
37700
37576
|
let deferred = [];
|
|
37701
37577
|
return review$2(base, {
|
|
@@ -37706,8 +37582,8 @@ const review$1 = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
37706
37582
|
deferred = [];
|
|
37707
37583
|
return reviewing(b, carry);
|
|
37708
37584
|
},
|
|
37709
|
-
fixRisks
|
|
37710
|
-
keepGreen: () => healthy(
|
|
37585
|
+
fixRisks,
|
|
37586
|
+
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
37711
37587
|
awaitReview: (b) => {
|
|
37712
37588
|
const notes = deferred;
|
|
37713
37589
|
deferred = [];
|
|
@@ -37726,7 +37602,7 @@ const review$1 = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
37726
37602
|
return deferNotes(deferred, head$1());
|
|
37727
37603
|
},
|
|
37728
37604
|
answerQuestions: answerReviewQuestions,
|
|
37729
|
-
fixNits
|
|
37605
|
+
fixNits,
|
|
37730
37606
|
close: closeReview,
|
|
37731
37607
|
captures: {
|
|
37732
37608
|
edits: reviewEditsCapture,
|
|
@@ -37748,8 +37624,8 @@ const review$1 = (base, escalations = { rounds: 0 }, frozen) => {
|
|
|
37748
37624
|
}
|
|
37749
37625
|
});
|
|
37750
37626
|
};
|
|
37751
|
-
/** The build tail: fix (when entered red), keep green
|
|
37752
|
-
const buildTail = (fixFirst, base,
|
|
37627
|
+
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
37628
|
+
const buildTail = (fixFirst, base, green = false) => tail(fixFirst, (escalations) => review$1(base, escalations), green);
|
|
37753
37629
|
//#endregion
|
|
37754
37630
|
//#region src/workflows/feature/feature.ts
|
|
37755
37631
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
@@ -37776,13 +37652,11 @@ const reUnwind = async (feedback) => {
|
|
|
37776
37652
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
37777
37653
|
const planAndBuild = async (firstBase) => {
|
|
37778
37654
|
let base = firstBase;
|
|
37779
|
-
let carried;
|
|
37780
37655
|
for (;;) {
|
|
37781
37656
|
await design(base);
|
|
37782
37657
|
await architecture();
|
|
37783
|
-
|
|
37784
|
-
|
|
37785
|
-
const outcome = await buildTail(false, base, built);
|
|
37658
|
+
await packages();
|
|
37659
|
+
const outcome = await buildTail(false, base, true);
|
|
37786
37660
|
if (outcome.verdict === "signoff") return;
|
|
37787
37661
|
await reUnwind(outcome);
|
|
37788
37662
|
base = outcome.base;
|
|
@@ -37918,4 +37792,4 @@ var workflows_default = feature;
|
|
|
37918
37792
|
*/
|
|
37919
37793
|
const doors = {};
|
|
37920
37794
|
//#endregion
|
|
37921
|
-
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, QUALITY, REQUIREMENTS, REVIEW, REVIEW_ORIG,
|
|
37795
|
+
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, QUALITY, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, handOffReview, handOffTail, healthy, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, requireDeclaredTests, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, skills, steering, summary, tail, threads, triage, unwind };
|