@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.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -18
  3. package/dist/chunks/{reserved-B9pG93Fc.mjs → reserved-6pAgFGgB.mjs} +69 -125
  4. package/dist/gtd.bundle.mjs +28 -3
  5. package/dist/testkit.mjs +30 -5
  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 +2 -2
  12. package/src/cli/Cli.test.ts +25 -0
  13. package/src/cli/Cli.ts +12 -1
  14. package/src/flags/index.test.ts +67 -0
  15. package/src/flags/index.ts +39 -0
  16. package/src/replay/Replay.ts +2 -1
  17. package/src/workflow/vars.test.ts +16 -0
  18. package/src/workflow/vars.ts +4 -1
  19. package/src/workflow-kit/scenarios.test.ts +116 -0
  20. package/src/workflow-kit/scenarios.ts +53 -113
  21. package/src/workflows/feature/feature.ts +2 -5
  22. package/src/workflows/feature/packages.test.ts +17 -34
  23. package/src/workflows/feature/packages.ts +13 -52
  24. package/src/workflows/feature/planning.ts +1 -1
  25. package/src/workflows/feature/review.test.ts +0 -47
  26. package/src/workflows/feature/review.ts +6 -10
  27. package/src/workflows/index.ts +0 -2
  28. package/src/workflows/shared/health.test.ts +1 -21
  29. package/src/workflows/shared/health.ts +4 -39
  30. package/src/workflows/shared/options.ts +0 -1
  31. package/src/workflows/shared/steps.ts +16 -11
  32. package/src/workflows/shared/tail.test.ts +1 -41
  33. package/src/workflows/shared/tail.ts +14 -30
  34. package/src/workflows/shared/text.fixture.ts +1 -0
  35. package/src/workflows/shared/text.test.ts +23 -41
  36. package/src/workflows/shared/text.ts +54 -74
  37. package/src/workflows/shared/vars.ts +0 -1
  38. package/src/workflows/shared/scenarios.test.ts +0 -256
  39. package/src/workflows/shared/scenarios.ts +0 -35
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "24.4.0",
3
+ "version": "26.0.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -28,6 +28,8 @@ Or run without installing (prefix every `gtd` below with `npx`) — see
28
28
  for the settings most projects tune. A process setting's value is committed to
29
29
  Git history; keep secrets in an environment setting (see
30
30
  [Settings](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#settings)).
31
+ `GTD_FLAG_<NAME>` variables toggle experimental features — see
32
+ [Feature flags](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#feature-flags).
31
33
 
32
34
  Also install all three skill sources the bundled workflow names in its prompts —
33
35
  none is optional. See
@@ -44,12 +46,6 @@ the read side to its agent. `read` is not a security boundary unless your driver
44
46
  enforces it at the OS level. See
45
47
  [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#file-access).
46
48
 
47
- > **`fastTestCommand` is required, with no fallback to `testCommand`.** Set it
48
- > (everything but e2e) as a workflow option in `gtd.config.ts`
49
- > (`feature({ fastTestCommand: "npm run test:fast" })`) or as
50
- > `GTD_FASTTESTCOMMAND`; gtd stops at the start gate, writing `.gtd/SETUP.md`,
51
- > until it is.
52
-
53
49
  > **A repository's `gtd.config.ts` is code, and gtd runs it.** A custom workflow
54
50
  > is a TypeScript module, and every gtd command that looks at workflow state —
55
51
  > `gtd next`, `gtd lsp`, `gtd door` and `gtd doors` included, not just
@@ -400,20 +396,21 @@ noted in steps 3 and 4 below.
400
396
  questions and all. Every plan gets the technical pass: its document has four
401
397
  sections, in order — `## Interfaces`, `## Call Stacks`, `## E2E Scenarios`,
402
398
  `## Unit Tests` — behind a leading `## Open Questions` when there are any.
403
- `## E2E Scenarios` is never empty: it holds the scenarios, or, when nothing
399
+ `## E2E Scenarios` is never empty: it names each user-visible behaviour in
400
+ plain words (the builder writes the scenario itself), or, when nothing
404
401
  user-visible changes, the line `No e2e change.` with a one-line reason.
405
402
 
406
- 3. **You wait.** The work is split into packages and built one at a time, each
407
- one starting from the unit tests it declares (a build turn missing one is
408
- refused), checked against the fast suite and fixed until it passes before
409
- moving on. A package that rewords a frozen `.feature` step stops at a wording
410
- gate: accept the change, or reject it and the original is restored. After the
411
- last package a full run (e2e included) has its own fix loop before the
412
- quality lap. Two points along the process are judged rather than always
413
- asking you outright — each stops and hands you a verdict to make
414
- (`gtd judge answer`, or land with a clean tree to accept the conservative
415
- default, which never skips work;
416
- `gtd judge run --provider fixed --answers <path>` — or the
403
+ 3. **You wait.** The work is split into vertical slices and built one at a time,
404
+ each one starting from the unit and e2e tests it declares (a build turn
405
+ missing one is refused), checked against the full suite (`testCommand`) and
406
+ fixed until it passes before moving on — so keep that suite fast; gtd has no
407
+ lighter one. Scenario wording is not frozen: you sign the scenarios off once,
408
+ in the review's `## Scenarios` chunk, where any assertion an existing
409
+ scenario lost is marked `Weakened:`. After the last package the quality lap
410
+ runs. Two points along the process are judged rather than always asking you
411
+ outright — each stops and hands you a verdict to make (`gtd judge answer`, or
412
+ land with a clean tree to accept the conservative default, which never skips
413
+ work; `gtd judge run --provider fixed --answers <path>` — or the
417
414
  `GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
418
415
  between `gtd judge --json` and `gtd judge answer`;
419
416
  `gtd judge run --provider jev` asks TypeSafe's Jev instead, with the key in
@@ -1,7 +1,6 @@
1
- import { accessFor, agent, answered, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote, read, refuse, removeScript, run, scope, showScript, skillsFor, start, vars } from "../flows.mjs";
2
- import { escalation as escalation$1, fixFindings, gate as gate$1, healthy as healthy$1, holdWording as holdWording$1, lensScopes, lensesOf, qualityLap as qualityLap$1, reviewToHandOff, untilSettled } from "../workflow-kit.mjs";
1
+ import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote, read, refuse, removeScript, run, scope, showScript, skillsFor, start, vars } from "../flows.mjs";
2
+ import { escalation as escalation$1, fixFindings, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, qualityLap as qualityLap$1, removedAssertions, reviewToHandOff } from "../workflow-kit.mjs";
3
3
  import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct } from "./Schema-B5yK7QrT.mjs";
4
- import { dirname } from "node:path";
5
4
  //#region src/workflows/shared/prose.ts
6
5
  const skillsPreamble = `- Load whatever's listed here that your harness actually has installed,
7
6
  skip anything it doesn't — silently, never stopping or asking about a
@@ -457,7 +456,8 @@ ${footnoteFoldIn}${codeThreadReplies()}
457
456
  - Every open point here is TECHNICAL — triage already resolved
458
457
  the product ones, one phase earlier. PERMISSIVE: answer it
459
458
  yourself unless you genuinely cannot defend a default; a wrong
460
- technical call is still caught by the full run and the quality lap
459
+ technical call is still caught by each package's suite run and the
460
+ quality lap
461
461
  - The narrow exception: a simplification that drops something
462
462
  \`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
463
463
  silent default
@@ -469,11 +469,13 @@ ${footnoteFoldIn}${codeThreadReplies()}
469
469
  only, as fenced TypeScript (or the repository's language)
470
470
  3. \`## Call Stacks\` — seam level, entry point → module
471
471
  boundary; one line per hop, never per line of code
472
- 4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
473
- (whatever e2e framework the repository uses), each preceded
474
- by a line \`- e2e: <path>\`. With no user-visible
475
- behaviour change, the literal line \`No e2e change.\` plus a
476
- one-line reason — never empty or missing
472
+ 4. \`## E2E Scenarios\` — one line per user-visible behaviour,
473
+ \`- e2e: <path> — <behaviour> (covers <concern>)\`, in plain
474
+ words: the builder writes the scenario in the repository's
475
+ own e2e framework, so never spell out Gherkin here. With no
476
+ user-visible behaviour change, the literal line
477
+ \`No e2e change.\` plus a one-line reason — never empty or
478
+ missing
477
479
  5. \`## Unit Tests\` — one \`### <interface>\` per entry of
478
480
  \`## Interfaces\`, each test a line
479
481
  \`- unit: <path> — <behaviour> (covers <concern>)\`;
@@ -499,35 +501,31 @@ const architectureDecomposePrompt = () => `${styleBlock}
499
501
  ${stateFileRules}
500
502
  - The only state files this turn touches are the package files
501
503
  under \`.gtd/packages/\` — no other files for notes or output.
502
- Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
503
- still needs it, and package files may reference it
504
+ Never delete \`.gtd/ARCHITECTURE.md\`: package files may
505
+ reference it, and it is removed with the last package
504
506
  - Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
505
507
  conversation, otherwise read it (the converged technical
506
508
  plan). It already lists the concerns, interfaces, e2e scenarios
507
509
  and unit tests — this turn groups them into packages. A
508
510
  \`## Merged Concerns\` heading there records merges, never a
509
511
  concern of its own: write no package file for it
510
- - A package is a consecutive run of \`## Unit Tests\` entries that go
511
- green together on the fast suite, at most ~8 unit-test entries
512
- each. Keep the merge rule: concerns whose footprints centre on the
513
- same files stay in one package unless the later one only consumes
514
- an interface the earlier one creates. Prefer fewer, larger
515
- packages. Order them so each stays green on the fast suite —
516
- interface-introducing packages first
512
+ - A package is one concern's slice, and each must leave the full
513
+ suite green — every package boundary costs a full run, so write
514
+ as few as the concerns allow. Keep the merge rule: concerns whose
515
+ footprints centre on the same files stay in one package unless
516
+ the later one only consumes an interface the earlier one creates.
517
+ Order them interface-introducing packages first
517
518
  - Write the packages under \`.gtd/packages/\`, each carrying its
518
519
  requirement(s) — both, independently, if merged — its tasks, and
519
520
  each task's acceptance criteria as \`- [ ]\` checkboxes and
520
521
  relevant paths:
521
- - \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
522
- \`## E2E Scenarios\` as tests in the repository's own e2e
523
- framework and leaves them red. Write no such file when
524
- \`## E2E Scenarios\` says "No e2e change."
525
- - \`01-…\` onward — the unit-test packages. A requirement no test
526
- covers (docs, README, deletions, config) becomes a chore
527
- package, or a task of one; chore packages declare no tests
528
- - the last numbered package is the wiring package: CLI entry,
529
- workflow composition, config. A red e2e after it is a bug, not
530
- missing work
522
+ - \`01-…\` onward — one vertical slice each: the unit tests,
523
+ the code, the wiring (CLI entry, workflow composition, config)
524
+ and every \`## E2E Scenarios\` entry its concern covers, so the
525
+ slice ends with its scenarios passing. A requirement no test
526
+ covers (docs, README, deletions, config) is a task of the slice
527
+ it belongs to; only one no slice owns becomes a chore package,
528
+ which declares no tests
531
529
  - Every package with tests has a \`## Tests\` section, one line per
532
530
  declared test, copied from the architecture entries:
533
531
  \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
@@ -573,10 +571,10 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
573
571
  - Write the tests the package declares under \`## Tests\` first,
574
572
  then the code that makes them pass; the turn is refused unless
575
573
  every declared test is in your diff
576
- - Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
577
- from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
578
- step code that typechecks, and leaves them red — no
579
- implementation
574
+ - A declared e2e test is a scenario in the repository's own e2e
575
+ framework for the behaviour its \`## E2E Scenarios\` entry names.
576
+ Word it to fit the existing steps — reuse and generalise them
577
+ rather than adding one-off ones
580
578
  - Implement every task the package describes, no more, no less,
581
579
  fanning independent ones out to parallel subagents where your
582
580
  harness supports it; leave other package files untouched
@@ -630,22 +628,9 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
630
628
  turn, or land untouched to give it one more attempt at the same
631
629
  analysis.
632
630
  `;
633
- const buildContextPrompt = (built) => {
634
- const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
635
- return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
636
- - A red scenario that is NEW in this plan likely means missing wiring
637
- between packages; new scenarios:
638
- ${list(built.scenarios.added)}
639
- - A red CHANGED or existing scenario or test is a regression a package
640
- introduced; scenarios this plan changed:
641
- ${list(built.scenarios.changed)}
642
- - Each package's commits, to bisect a break:
643
- ${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
644
- `;
645
- };
646
- const buildFixPrompt = (built) => `${stateFileRules}
631
+ const buildFixPrompt = () => `${stateFileRules}
647
632
  ${fixFeedbackPrompt}
648
- ${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
633
+ - Leave everything uncommitted — do not commit
649
634
  `;
650
635
  const finisherSystem = () => `${finisherPersona}
651
636
 
@@ -966,7 +951,18 @@ actionability, and never dismiss a real note or edit as approval.
966
951
  - If not: finish without writing anything — changing nothing is
967
952
  the sign-off
968
953
  `;
969
- const buildReviewReviewingPrompt = (base, carry) => `${styleBlock}
954
+ const scenariosPrompt = (removed) => `- When a scenario in a \`.feature\` file was added or changed since the
955
+ base, the first chunk is \`## Scenarios\`: one pointer per such
956
+ scenario, its note saying in plain words what behaviour it now pins
957
+ down — the human signs off the scenarios here, nowhere else
958
+ ${removed.length === 0 ? "" : `- These assertions of scenarios that existed at the base are gone:
959
+ ${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:\`,
960
+ naming the lost line and whether \`.gtd/REQUIREMENTS.md\` asked
961
+ for it. A scenario that no longer exists has no pointer of its own:
962
+ put its \`Weakened:\` note on a pointer of the chunk covering its
963
+ removal
964
+ `}`;
965
+ const buildReviewReviewingPrompt = (base, carry, removed = []) => `${styleBlock}
970
966
 
971
967
  ${styleFormatContract}
972
968
 
@@ -1008,7 +1004,7 @@ review the changes:
1008
1004
  A note sitting entirely on the line(s) beneath the pointer is
1009
1005
  also valid. Either way, the note must never start with a bare \`./path\` token
1010
1006
  — that parses as a second pointer, not a note
1011
- No diff is given — read the changes yourself. The range runs
1007
+ ${scenariosPrompt(removed)}No diff is given — read the changes yourself. The range runs
1012
1008
  from \`${base}\` to the working tree (committed turns
1013
1009
  plus anything pending); on a feedback round that's the previous
1014
1010
  review's boundary, so it covers only what's new.
@@ -1055,20 +1051,6 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
1055
1051
  Print the closing message and stop — this writes nothing itself.
1056
1052
  `;
1057
1053
  };
1058
- const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
1059
- turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
1060
-
1061
- ${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
1062
-
1063
- To accept the change, land untouched.
1064
-
1065
- To reject it, run
1066
-
1067
- git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
1068
-
1069
- and land. Restoring only some of the files is a partial accept: every file
1070
- still differing from the frozen wording is accepted.
1071
- `;
1072
1054
  //#endregion
1073
1055
  //#region src/workflows/shared/steps.ts
1074
1056
  const FEEDBACK = ".gtd/FEEDBACK.md";
@@ -1079,7 +1061,7 @@ const REVIEW = ".gtd/REVIEW.md";
1079
1061
  const QUALITY = ".gtd/QUALITY.md";
1080
1062
  const planner = () => env.plannerModel ?? "";
1081
1063
  const coder = () => env.coderModel ?? "";
1082
- const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
1064
+ const fixCheck = () => agentWithSkills("fix", buildFixPrompt(), {
1083
1065
  label: "Fixing the check",
1084
1066
  file: FEEDBACK,
1085
1067
  model: coder(),
@@ -1135,7 +1117,7 @@ const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPr
1135
1117
  allowEmpty: true
1136
1118
  });
1137
1119
  /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
1138
- const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
1120
+ const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))), {
1139
1121
  label: "Reviewing",
1140
1122
  file: REVIEW,
1141
1123
  mode: "review",
@@ -1164,25 +1146,11 @@ const reviewMissing = (round) => human("review.review-missing", {
1164
1146
  message: buildReviewReviewMissingMessage(round),
1165
1147
  label: "Nothing to review"
1166
1148
  });
1167
- //#endregion
1168
- //#region src/workflows/shared/health.ts
1169
- /** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
1170
- const SETUP = ".gtd/SETUP.md";
1171
- 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.";
1172
- /** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
1173
- const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
1174
- `mkdir -p ${quote(dirname(SETUP))}`,
1175
- `printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
1176
- `printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
1177
- "exit 0"
1178
- ] : [`rm -f ${quote(SETUP)}`];
1179
- const unconfigured = () => read(SETUP) !== void 0;
1180
1149
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
1181
- const baseline = (name, label = "Checking the baseline") => untilSettled(() => check(name, env.testCommand ?? "", {
1150
+ const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
1182
1151
  report: FEEDBACK,
1183
- label,
1184
- preamble: fastSuiteGuard()
1185
- }), unconfigured);
1152
+ label
1153
+ });
1186
1154
  const escalationSteps = {
1187
1155
  limit: 2,
1188
1156
  describe: describeEscalation,
@@ -1220,22 +1188,18 @@ const sameFailure = async (previous, current) => {
1220
1188
  });
1221
1189
  return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
1222
1190
  };
1223
- const runSuite = (options) => {
1224
- const fast = options.suite === "fast";
1225
- return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
1226
- report: FEEDBACK,
1227
- label: "Running checks",
1228
- sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
1229
- ...fast ? { preamble: fastSuiteGuard() } : {}
1230
- });
1231
- };
1191
+ const runSuite = (options) => check("health.check", env.testCommand ?? "", {
1192
+ report: FEEDBACK,
1193
+ label: "Running checks",
1194
+ sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []]
1195
+ });
1232
1196
  /**
1233
1197
  * Loop until the suite is green: check, and on red run `fix` — escalating
1234
1198
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
1235
1199
  * identical to the previous round's.
1236
1200
  */
1237
1201
  const healthy = (fix, options = {}) => healthy$1(fix, {
1238
- check: options.suite === "fast" ? () => untilSettled(() => runSuite(options), unconfigured) : () => runSuite(options),
1202
+ check: () => runSuite(options),
1239
1203
  report: FEEDBACK,
1240
1204
  fixCap: 3,
1241
1205
  fixesSoFar: options.fixesSoFar ?? 0,
@@ -1263,7 +1227,6 @@ const defaults = {
1263
1227
  /** Environment settings. */
1264
1228
  const envDefaults = {
1265
1229
  testCommand: "npm test",
1266
- fastTestCommand: "",
1267
1230
  plannerModel: "smart",
1268
1231
  coderModel: "base"
1269
1232
  };
@@ -1278,7 +1241,6 @@ const ShippedOptions = Struct({
1278
1241
  reviewSeverityMinP: setting,
1279
1242
  qualityReviews: setting,
1280
1243
  testCommand: setting,
1281
- fastTestCommand: setting,
1282
1244
  plannerModel: setting,
1283
1245
  coderModel: setting
1284
1246
  });
@@ -1298,21 +1260,6 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
1298
1260
  base
1299
1261
  }));
1300
1262
  //#endregion
1301
- //#region src/workflows/shared/scenarios.ts
1302
- /** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
1303
- const holdWording = (frozen) => holdWording$1(frozen, (at, drifted) => human("scenario-wording", {
1304
- message: scenarioWordingMessage(at, drifted),
1305
- label: "Scenario wording changed",
1306
- acceptClean: true,
1307
- base: at
1308
- }));
1309
- /** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
1310
- const guarded = (frozen, turn, within) => async () => {
1311
- await turn();
1312
- if (within === void 0) await holdWording(frozen);
1313
- else await scope(within, () => holdWording(frozen));
1314
- };
1315
- //#endregion
1316
1263
  //#region src/workflows/shared/tail.ts
1317
1264
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
1318
1265
  const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
@@ -1323,8 +1270,8 @@ const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
1323
1270
  */
1324
1271
  const qualityLap = () => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY);
1325
1272
  /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
1326
- const fixQualityFindings = (escalations, frozen) => fixFindings({
1327
- fix: guarded(frozen, fixQuality),
1273
+ const fixQualityFindings = (escalations) => fixFindings({
1274
+ fix: fixQuality,
1328
1275
  findings: QUALITY,
1329
1276
  cap: 3,
1330
1277
  escalate: () => escalation(escalations)
@@ -1340,27 +1287,24 @@ const refusePendingHandOff = () => {
1340
1287
  };
1341
1288
  /**
1342
1289
  * The build tail: fix (when entered red), keep green, the quality lap, then
1343
- * `finish` — the human review, as the calling workflow runs it.
1290
+ * `finish` — the human review, as the calling workflow runs it. `green`: the
1291
+ * caller just left the full suite green (the package loop), so no run opens it.
1344
1292
  */
1345
- const tail = (fixFirst, finish, built) => scope("build", async () => {
1346
- const frozen = built?.frozen;
1293
+ const tail = (fixFirst, finish, green = false) => scope("build", async () => {
1347
1294
  const escalations = { rounds: 0 };
1348
- const guardedFix = guarded(frozen, () => fixCheck());
1295
+ const fix = fixCheck;
1349
1296
  const repair = async () => {
1350
- await guardedFix();
1351
- await healthy(guardedFix, {
1297
+ await fix();
1298
+ await healthy(fix, {
1352
1299
  fixesSoFar: 1,
1353
1300
  escalations
1354
1301
  });
1355
1302
  };
1356
1303
  if (fixFirst) await repair();
1357
- else await healthy(guarded(frozen, () => fixCheck(built)), {
1358
- escalations,
1359
- sweepOnGreen: [ARCHITECTURE]
1360
- });
1361
- if (await qualityLap() === "findings") if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
1304
+ else if (!green) await healthy(fix, { escalations });
1305
+ if (await qualityLap() === "findings") if (await fixQualityFindings(escalations)) await healthy(fix, { escalations });
1362
1306
  else await repair();
1363
- return finish(escalations, frozen);
1307
+ return finish(escalations);
1364
1308
  });
1365
1309
  /**
1366
1310
  * The review and fix workflows' human review since `base`: a sign-off closes
@@ -1371,7 +1315,7 @@ const handOffReview = (base, escalations) => reviewToHandOff(base, {
1371
1315
  file: REVIEW,
1372
1316
  reviewing,
1373
1317
  fixRisks,
1374
- keepGreen: () => healthy(() => fixCheck(), { escalations }),
1318
+ keepGreen: () => healthy(fixCheck, { escalations }),
1375
1319
  awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
1376
1320
  reviewMissing,
1377
1321
  close: closeReview,
@@ -1425,4 +1369,4 @@ const steering = {
1425
1369
  [REVIEW]: "review"
1426
1370
  };
1427
1371
  //#endregion
1428
- export { startGateBlockedMessage as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixCheck as C, reviewing as D, reviewMissing as E, buildReviewCollectingPrompt as F, idleMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, reviewEditNotesCapture as J, packagesItemBuildingPrompt as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewerSystem as Q, builderSystem as R, coder as S, planner as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, reviewGateBlockedMessage as X, reviewEditsCapture as Y, reviewNotesCapture as Z, ARCHITECTURE as _, REVIEW_ORIG as a, REVIEW as b, refusePendingHandOff as c, shipped as d, unwindFailedMessage as et, defaults as f, healthy as g, gate as h, skills as i, architectureDecomposePrompt as j, architectSystem as k, tail as l, baseline as m, summary as n, closeReview as o, envDefaults as p, packagesItemFixSuitePrompt as q, access as r, handOffTail as s, steering as t, unwindFailure as tt, guarded as u, FEEDBACK as v, fixRisks as w, awaitReview as x, REQUIREMENTS as y, deferNotesScript as z };
1372
+ export { unwindFailedMessage as $, architectureDecomposePrompt as A, designSystem as B, fixRisks as C, agentWithSkills as D, reviewing as E, buildReviewFixNitsPrompt as F, packagesItemBuildingPrompt as G, finisherSystem as H, buildReviewTriageMessage as I, reviewEditsCapture as J, packagesItemFixSuitePrompt as K, builderSystem as L, buildReviewAnswerQuestionsPrompt as M, buildReviewAwaitAfterDeferMessage as N, architectSystem as O, buildReviewCollectingPrompt as P, startGateBlockedMessage as Q, deferNotesScript as R, fixCheck as S, reviewMissing as T, fixReviewNotesPrompt as U, designTriagePrompt as V, idleMessage as W, reviewNotesCapture as X, reviewGateBlockedMessage as Y, reviewerSystem as Z, FEEDBACK as _, REVIEW_ORIG as a, awaitReview as b, refusePendingHandOff as c, defaults as d, unwindFailure as et, envDefaults as f, ARCHITECTURE as g, healthy as h, skills as i, architectureGateAnswerMessage as j, architectureAuthorPrompt as k, tail as l, gate as m, summary as n, closeReview as o, baseline as p, reviewEditNotesCapture as q, access as r, handOffTail as s, steering as t, shipped as u, REQUIREMENTS as v, planner as w, coder as x, REVIEW as y, designGateAnswerMessage as z };
@@ -20957,6 +20957,26 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
20957
20957
  };
20958
20958
  });
20959
20959
  //#endregion
20960
+ //#region src/flags/index.ts
20961
+ const FLAGS$1 = [];
20962
+ const BOOL = {
20963
+ "": false,
20964
+ "0": false,
20965
+ false: false,
20966
+ "1": true,
20967
+ true: true
20968
+ };
20969
+ const resolveFlags = (env, registry = FLAGS$1) => {
20970
+ const out = {};
20971
+ for (const { name } of registry) {
20972
+ const variable = `GTD_FLAG_${name.toUpperCase()}`;
20973
+ const key = (env[variable] ?? "").toLowerCase();
20974
+ if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
20975
+ out[name] = BOOL[key];
20976
+ }
20977
+ return out;
20978
+ };
20979
+ //#endregion
20960
20980
  //#region src/install/Version.ts
20961
20981
  const _require = createRequire(import.meta.url);
20962
20982
  const findPackageJson = (fromUrl) => {
@@ -37749,7 +37769,7 @@ const replay = async (input) => {
37749
37769
  ...c,
37750
37770
  parsed: parseCommitMessage(c.message)
37751
37771
  }));
37752
- const treeAt = (hash) => hash === input.episode.base.hash ? input.episode.base.tree : commits.find((c) => c.hash === hash)?.tree;
37772
+ const treeAt = (hash) => hash === input.episode.base.hash ? input.episode.base.tree : commits.find((c) => c.hash === hash)?.tree ?? (hash === input.start ? input.startTree : void 0);
37753
37773
  let cursor = 0;
37754
37774
  let pendingUsed = false;
37755
37775
  let position = input.episode.base;
@@ -38131,7 +38151,9 @@ const resolveVars = (workflowVars, entryVars, env) => {
38131
38151
  ...entryVars
38132
38152
  };
38133
38153
  for (const name of Object.keys(merged)) {
38134
- const value = env[PREFIX + name.toUpperCase()];
38154
+ const key = PREFIX + name.toUpperCase();
38155
+ if (key.startsWith("GTD_FLAG_")) continue;
38156
+ const value = env[key];
38135
38157
  if (value !== void 0) merged[name] = value;
38136
38158
  }
38137
38159
  return merged;
@@ -42051,7 +42073,10 @@ const runCli = (argv, io) => {
42051
42073
  io.exit(2);
42052
42074
  });
42053
42075
  const out = bufferedArtifactOut(io);
42054
- return runCommand(plan.command, plan.json, out).pipe(map$4(() => {
42076
+ return flatMap$4(Host, ({ env }) => try_({
42077
+ try: () => resolveFlags(env, io.flags),
42078
+ catch: (e) => new GtdError(e.message)
42079
+ })).pipe(flatMap$4(() => runCommand(plan.command, plan.json, out)), map$4(() => {
42055
42080
  out.flush();
42056
42081
  io.exit(0);
42057
42082
  }), provide$1(io.layers(plan.verbose)), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json.kind !== "off")(cause)));
package/dist/testkit.mjs CHANGED
@@ -5,13 +5,13 @@ import { $ as makeSemaphore, $i as toJSON, $n as mapBoth, $r as none$1, $t as pr
5
5
  import { createRequire } from "node:module";
6
6
  import * as Crypto from "node:crypto";
7
7
  import { createHash } from "node:crypto";
8
- import * as Path from "node:path";
9
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
8
  import { PassThrough } from "node:stream";
11
9
  import * as NFS from "node:fs";
12
10
  import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
13
11
  import * as OS from "node:os";
14
12
  import { homedir, tmpdir } from "node:os";
13
+ import * as Path from "node:path";
14
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
15
15
  import * as ChildProcess from "node:child_process";
16
16
  import { execFileSync, execSync, spawnSync } from "node:child_process";
17
17
  import * as NodeUrl from "node:url";
@@ -6085,6 +6085,26 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
6085
6085
  };
6086
6086
  });
6087
6087
  //#endregion
6088
+ //#region src/flags/index.ts
6089
+ const FLAGS$1 = [];
6090
+ const BOOL = {
6091
+ "": false,
6092
+ "0": false,
6093
+ false: false,
6094
+ "1": true,
6095
+ true: true
6096
+ };
6097
+ const resolveFlags = (env, registry = FLAGS$1) => {
6098
+ const out = {};
6099
+ for (const { name } of registry) {
6100
+ const variable = `GTD_FLAG_${name.toUpperCase()}`;
6101
+ const key = (env[variable] ?? "").toLowerCase();
6102
+ if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
6103
+ out[name] = BOOL[key];
6104
+ }
6105
+ return out;
6106
+ };
6107
+ //#endregion
6088
6108
  //#region src/install/Version.ts
6089
6109
  const _require = createRequire(import.meta.url);
6090
6110
  const findPackageJson = (fromUrl) => {
@@ -7027,7 +7047,7 @@ const replay = async (input) => {
7027
7047
  ...c,
7028
7048
  parsed: parseCommitMessage(c.message)
7029
7049
  }));
7030
- const treeAt = (hash) => hash === input.episode.base.hash ? input.episode.base.tree : commits.find((c) => c.hash === hash)?.tree;
7050
+ const treeAt = (hash) => hash === input.episode.base.hash ? input.episode.base.tree : commits.find((c) => c.hash === hash)?.tree ?? (hash === input.start ? input.startTree : void 0);
7031
7051
  let cursor = 0;
7032
7052
  let pendingUsed = false;
7033
7053
  let position = input.episode.base;
@@ -7408,7 +7428,9 @@ const resolveVars = (workflowVars, entryVars, env) => {
7408
7428
  ...entryVars
7409
7429
  };
7410
7430
  for (const name of Object.keys(merged)) {
7411
- const value = env[PREFIX + name.toUpperCase()];
7431
+ const key = PREFIX + name.toUpperCase();
7432
+ if (key.startsWith("GTD_FLAG_")) continue;
7433
+ const value = env[key];
7412
7434
  if (value !== void 0) merged[name] = value;
7413
7435
  }
7414
7436
  return merged;
@@ -11303,7 +11325,10 @@ const runCli = (argv, io) => {
11303
11325
  io.exit(2);
11304
11326
  });
11305
11327
  const out = bufferedArtifactOut(io);
11306
- return runCommand$1(plan.command, plan.json, out).pipe(map$4(() => {
11328
+ return flatMap$4(Host, ({ env }) => try_({
11329
+ try: () => resolveFlags(env, io.flags),
11330
+ catch: (e) => new GtdError(e.message)
11331
+ })).pipe(flatMap$4(() => runCommand$1(plan.command, plan.json, out)), map$4(() => {
11307
11332
  out.flush();
11308
11333
  io.exit(0);
11309
11334
  }), provide$2(io.layers(plan.verbose)), sandbox, catchAll$1((cause) => isInterruptedOnly(cause) ? failCause$4(cause) : report(io, plan.json.kind !== "off")(cause)));