@pmelab/gtd 24.4.0 → 25.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.
Files changed (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +13 -18
  3. package/dist/chunks/{reserved-B9pG93Fc.mjs → reserved-6pAgFGgB.mjs} +69 -125
  4. package/dist/gtd.bundle.mjs +1 -1
  5. package/dist/testkit.mjs +3 -3
  6. package/dist/workflow-kit.mjs +47 -90
  7. package/dist/workflows/feature.mjs +25 -47
  8. package/dist/workflows/fix.mjs +1 -1
  9. package/dist/workflows/review.mjs +1 -1
  10. package/dist/workflows.mjs +135 -261
  11. package/package.json +1 -2
  12. package/src/replay/Replay.ts +2 -1
  13. package/src/workflow-kit/scenarios.test.ts +116 -0
  14. package/src/workflow-kit/scenarios.ts +53 -113
  15. package/src/workflows/feature/feature.ts +2 -5
  16. package/src/workflows/feature/packages.test.ts +17 -34
  17. package/src/workflows/feature/packages.ts +13 -52
  18. package/src/workflows/feature/planning.ts +1 -1
  19. package/src/workflows/feature/review.test.ts +0 -47
  20. package/src/workflows/feature/review.ts +6 -10
  21. package/src/workflows/index.ts +0 -2
  22. package/src/workflows/shared/health.test.ts +1 -21
  23. package/src/workflows/shared/health.ts +4 -39
  24. package/src/workflows/shared/options.ts +0 -1
  25. package/src/workflows/shared/steps.ts +16 -11
  26. package/src/workflows/shared/tail.test.ts +1 -41
  27. package/src/workflows/shared/tail.ts +14 -30
  28. package/src/workflows/shared/text.fixture.ts +1 -0
  29. package/src/workflows/shared/text.test.ts +23 -41
  30. package/src/workflows/shared/text.ts +54 -74
  31. package/src/workflows/shared/vars.ts +0 -1
  32. package/src/workflows/shared/scenarios.test.ts +0 -256
  33. package/src/workflows/shared/scenarios.ts +0 -35
@@ -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 = /^(\s*)("""|```)/;
393
- /**
394
- * Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
395
- * dropped — except inside a doc string, whose content is test input: kept
396
- * whole, indented relative to its opening delimiter (as Gherkin reads it).
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 doc;
401
- for (const line of source.split("\n")) {
402
- const delimiter = DOC_STRING.exec(line);
403
- if (doc === void 0 && delimiter !== null) {
404
- doc = {
405
- indent: delimiter[1].length,
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.join("\n");
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
- /** Paths whose wording differs from the snapshot (a deleted file counts). */
440
- const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
441
- const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
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 i = 0;
453
- let j = 0;
454
- while (i < a.length && j < b.length) if (a[i] === b[j]) {
455
- i++;
456
- j++;
457
- } else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
458
- else out.push(`+ ${b[j++]}`);
459
- return [
460
- ...out,
461
- ...a.slice(i).map((l) => `- ${l}`),
462
- ...b.slice(j).map((l) => `+ ${l}`)
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 wordingDrift = (frozen, path) => ({
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
- * After a turn: rest at `stop` while wording drifted from `frozen`. Whatever
471
- * still differs after it is accepted and moves the snapshot; what was
472
- * restored stays frozen as it was.
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 holdWording$1 = async (frozen, stop) => {
475
- if (frozen === void 0) return;
476
- const drifted = driftedScenarios(frozen);
477
- if (drifted.length === 0) return;
478
- await stop(frozen.at, drifted.map((path) => wordingDrift(frozen, path)));
479
- const accepted = driftedScenarios(frozen);
480
- if (accepted.length === 0) return;
481
- for (const path of accepted) {
482
- const text = textOf(path);
483
- if (text === void 0) delete frozen.texts[path];
484
- else frozen.texts[path] = text;
485
- }
486
- frozen.at = head$1();
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 the full run and the quality lap
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\` — Gherkin in fenced \`gherkin\` blocks
17162
- (whatever e2e framework the repository uses), each preceded
17163
- by a line \`- e2e: <path>\`. With no user-visible
17164
- behaviour change, the literal line \`No e2e change.\` plus a
17165
- one-line reason — never empty or missing
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\`: the build tail's full run
17192
- still needs it, and package files may reference it
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 a consecutive run of \`## Unit Tests\` entries that go
17200
- green together on the fast suite, at most ~8 unit-test entries
17201
- each. Keep the merge rule: concerns whose footprints centre on the
17202
- same files stay in one package unless the later one only consumes
17203
- an interface the earlier one creates. Prefer fewer, larger
17204
- packages. Order them so each stays green on the fast suite —
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
- - \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
17211
- \`## E2E Scenarios\` as tests in the repository's own e2e
17212
- framework and leaves them red. Write no such file when
17213
- \`## E2E Scenarios\` says "No e2e change."
17214
- - \`01-…\` onward — the unit-test packages. A requirement no test
17215
- covers (docs, README, deletions, config) becomes a chore
17216
- package, or a task of one; chore packages declare no tests
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
- - Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
17266
- from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
17267
- step code that typechecks, and leaves them red — no
17268
- implementation
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 buildContextPrompt = (built) => {
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
- ${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
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 buildReviewReviewingPrompt = (base, carry) => `${styleBlock}
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 = (built) => agentWithSkills("fix", buildFixPrompt(built), {
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") => untilSettled(() => check(name, env.testCommand ?? "", {
17793
+ const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
17873
17794
  report: FEEDBACK,
17874
- label,
17875
- preamble: fastSuiteGuard()
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
- const fast = options.suite === "fast";
17916
- return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
17917
- report: FEEDBACK,
17918
- label: "Running checks",
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: options.suite === "fast" ? () => untilSettled(() => runSuite(options), unconfigured) : () => runSuite(options),
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, frozen) => fixFindings({
37332
- fix: guarded(frozen, fixQuality),
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, built) => scope("build", async () => {
37351
- const frozen = built?.frozen;
37250
+ const tail = (fixFirst, finish, green = false) => scope("build", async () => {
37352
37251
  const escalations = { rounds: 0 };
37353
- const guardedFix = guarded(frozen, () => fixCheck());
37252
+ const fix = fixCheck;
37354
37253
  const repair = async () => {
37355
- await guardedFix();
37356
- await healthy(guardedFix, {
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(guarded(frozen, () => fixCheck(built)), {
37363
- escalations,
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, frozen);
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(() => fixCheck(), { escalations }),
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
- /** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
37500
- const packageItem = async (pkg, frozen) => {
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 guarded(frozen, declared(() => build(pkg)))();
37507
- await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
37508
- await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
37509
- return {
37403
+ await declared(() => build(pkg))();
37404
+ await healthy(declared(fixSuite));
37405
+ await run("closing", removeScript([
37510
37406
  pkg,
37511
- from,
37512
- to: head$1()
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 = (carried) => scope("packages", async () => {
37520
- let frozen = carried;
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 build tail's full run needs it; it is removed only once that run is green");
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 }, frozen) => {
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: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
37710
- keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
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: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
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, the quality lap, then human review since `base`. */
37752
- const buildTail = (fixFirst, base, built) => tail(fixFirst, (escalations, frozen) => review$1(base, escalations, frozen), built);
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
- const built = await packages(carried);
37784
- carried = built.frozen;
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, SCENARIO_PACKAGE, SETUP, 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, driftedScenarios, envDefaults, escalate, escalation, escalationExhausted, fastSuiteGuard, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, freezeScenarios, gate, guarded, handOffReview, handOffTail, healthy, holdWording, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, requireDeclaredTests, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, scenarioText, skills, steering, summary, tail, threads, triage, unwind, wordingDrift };
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 };