@pmelab/gtd 18.1.0 → 19.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.
@@ -31013,6 +31013,7 @@ const checkScript = (command, options) => {
31013
31013
  return [
31014
31014
  "#!/usr/bin/env sh",
31015
31015
  "set +e",
31016
+ ...options.preamble ?? [],
31016
31017
  ...removal(options.sweep ?? []),
31017
31018
  `mkdir -p "$(dirname ${report})"`,
31018
31019
  "(",
@@ -31106,12 +31107,13 @@ const wrote = (path) => changes(path).some((c) => c.status !== "deleted");
31106
31107
  * command passed.
31107
31108
  */
31108
31109
  const check = async (name, command, options) => {
31109
- const { report, label, sweep, sweepOnGreen } = options;
31110
+ const { report, label, sweep, sweepOnGreen, preamble } = options;
31110
31111
  await run$3(name, checkScript(command, {
31111
31112
  report,
31112
31113
  stamp: head().slice(0, 7),
31113
31114
  sweep,
31114
- sweepOnGreen
31115
+ sweepOnGreen,
31116
+ preamble
31115
31117
  }), { label });
31116
31118
  return !wrote(report);
31117
31119
  };
@@ -48157,8 +48159,7 @@ const architectPersona = `You are the technical planning voice in gtd's build pi
48157
48159
  once product concerns are settled, reading them cold, no carried
48158
48160
  conversation. Work out the *how* per concern — structure, data models,
48159
48161
  tech-stack choices, error handling — raise the technical open
48160
- questions, then write one package spec per concern with no further
48161
- judgement call: the grouping is already decided.`;
48162
+ questions, then pin interfaces and tests in the architecture document.`;
48162
48163
  const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
48163
48164
  deliberately separate from whoever wrote the code, with no attachment
48164
48165
  to it. Write a structured review document grouping a diff into
@@ -48167,12 +48168,6 @@ just approving; answer the human's questions inline in the review;
48167
48168
  and, when asked, fix the small nits the human flagged and the risks
48168
48169
  you marked yourself, each in one batch.
48169
48170
  Beyond that you never fix or build anything yourself.`;
48170
- const specReviewerPersona = `You are the adversarial spec-conformance checker in gtd's build
48171
- pipeline, checking one freshly-built package against its spec. Verify
48172
- only: tasks done, criteria met, code sound and consistent with the
48173
- codebase. Write feedback only when something is genuinely wrong —
48174
- otherwise write nothing; a clean turn IS the approval. Never fix what
48175
- you find — naming it precisely enough for a fix turn is the whole job.`;
48176
48171
  const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
48177
48172
  package's declared scope end to end, tests first, and come back to fix
48178
48173
  things when redirected — a failing check, or reviewer feedback. Stay
@@ -48382,14 +48377,6 @@ carries it.
48382
48377
  What each change does next (then run \`gtd land\`):
48383
48378
  - **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
48384
48379
  `;
48385
- const architecturePreMessage = () => `Judging whether \`.gtd/REQUIREMENTS.md\`'s settled concerns need a
48386
- dedicated architecture pass — real structural decisions, multiple
48387
- integration points, or a non-obvious tradeoff — before packages
48388
- are written. Run \`gtd judge answer\` and pipe a verdict for
48389
- \`architectureWarranted\` — or land untouched to run the full pass
48390
- (the conservative default; a skipped judgment never suppresses
48391
- it).
48392
- `;
48393
48380
  const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
48394
48381
  \`.gtd/FEEDBACK.md\` holds the failing output.
48395
48382
 
@@ -48554,22 +48541,46 @@ ${footnoteFoldIn}${codeThreadReplies()}
48554
48541
  authority is to merge only, never to split — the whole problem
48555
48542
  is over-granularity
48556
48543
  - Record every merge under \`## Merged Concerns\`,
48557
- carrying both merged requirements verbatim so spec review
48558
- still covers each independently
48544
+ carrying both merged requirements verbatim so each requirement
48545
+ stays traceable to its tests
48559
48546
  - A merge raises no open question and stops for no
48560
48547
  human — do not route it to \`architecture.gate\` for a veto; the
48561
- human sees it when reviewing the plan, and spec review is the
48562
- real safety net
48548
+ human sees it when reviewing the plan
48563
48549
  - Prefer fewer, larger packages — the smallest independently
48564
48550
  valuable change, not the smallest change that compiles
48565
48551
  - Every open point here is TECHNICAL — triage already resolved
48566
48552
  the product ones, one phase earlier. PERMISSIVE: answer it
48567
48553
  yourself unless you genuinely cannot defend a default; a wrong
48568
- technical call is still caught at spec review
48554
+ technical call is still caught by the full run and the quality lap
48569
48555
  - The narrow exception: a simplification that drops something
48570
48556
  \`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
48571
48557
  silent default
48572
48558
 
48559
+ - \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
48560
+ off. It carries these sections, in this order:
48561
+ 1. \`## Open Questions\` (qa format, first, only when present)
48562
+ 2. \`## Interfaces\` — new and changed signatures/contracts
48563
+ only, as fenced TypeScript (or the repository's language)
48564
+ 3. \`## Call Stacks\` — seam level, entry point → module
48565
+ boundary; one line per hop, never per line of code
48566
+ 4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
48567
+ (whatever e2e framework the repository uses), each preceded
48568
+ by a line \`- e2e: <path>\`. With no user-visible
48569
+ behaviour change, the literal line \`No e2e change.\` plus a
48570
+ one-line reason — never empty or missing
48571
+ 5. \`## Unit Tests\` — one \`### <interface>\` per entry of
48572
+ \`## Interfaces\`, each test a line
48573
+ \`- unit: <path> — <behaviour> (covers <concern>)\`;
48574
+ never against an internal helper. A requirement no test can
48575
+ cover (docs, README, deletions, config) goes under a
48576
+ \`### Chores\` heading in this section as
48577
+ \`- chore: <path> — <what> (covers <concern>)\`
48578
+ 6. \`## Merged Concerns\` — kept, when any merge happened
48579
+ 7. \`## Answered Questions\` — last
48580
+ - Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
48581
+ in at least one \`(covers …)\` tail, as a test or a chore. Each
48582
+ test entry carries its file path and level (unit / e2e)
48583
+
48573
48584
  ${questionBar}
48574
48585
  ## Return lap
48575
48586
 
@@ -48581,26 +48592,41 @@ const architectureDecomposePrompt = () => `${styleBlock}
48581
48592
 
48582
48593
  ${stateFileRules}
48583
48594
  - The only state files this turn touches are the package files
48584
- under \`.gtd/packages/\` and \`.gtd/ARCHITECTURE.md\` (deleted) —
48585
- no other files for notes or output
48595
+ under \`.gtd/packages/\` — no other files for notes or output.
48596
+ Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
48597
+ still needs it, and package files may reference it
48586
48598
  - Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
48587
48599
  conversation, otherwise read it (the converged technical
48588
- plan). It already lists an ordered set of concerns with every
48589
- merge/split judgement made — this turn is a mechanical
48590
- write-out, not a planning one. A \`## Merged Concerns\` heading
48591
- there records those merges, never a concern of its own: write
48592
- no package file for it
48593
- - Write one package file per concern, in the settled order,
48594
- under \`.gtd/packages/\` (e.g. \`.gtd/packages/01-name.md\`,
48595
- \`02-name.md\`, ...), each carrying that concern's
48596
- requirement(s) — both, independently, if merged — its
48597
- independent tasks, and each task's acceptance criteria as
48598
- \`- [ ]\` checkboxes and relevant paths
48599
- - Do not merge or split concerns here — that judgement already
48600
- happened; carry the settled grouping over verbatim. No
48601
- package file may reference any other \`.gtd/\` file
48602
- - Once written, delete \`.gtd/ARCHITECTURE.md\`. Leave everything
48603
- uncommitted and finish
48600
+ plan). It already lists the concerns, interfaces, e2e scenarios
48601
+ and unit tests — this turn groups them into packages. A
48602
+ \`## Merged Concerns\` heading there records merges, never a
48603
+ concern of its own: write no package file for it
48604
+ - A package is a consecutive run of \`## Unit Tests\` entries that go
48605
+ green together on the fast suite, at most ~8 unit-test entries
48606
+ each. Keep the merge rule: concerns whose footprints centre on the
48607
+ same files stay in one package unless the later one only consumes
48608
+ an interface the earlier one creates. Prefer fewer, larger
48609
+ packages. Order them so each stays green on the fast suite —
48610
+ interface-introducing packages first
48611
+ - Write the packages under \`.gtd/packages/\`, each carrying its
48612
+ requirement(s) — both, independently, if merged — its tasks, and
48613
+ each task's acceptance criteria as \`- [ ]\` checkboxes and
48614
+ relevant paths:
48615
+ - \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
48616
+ \`## E2E Scenarios\` as tests in the repository's own e2e
48617
+ framework and leaves them red. Write no such file when
48618
+ \`## E2E Scenarios\` says "No e2e change."
48619
+ - \`01-…\` onward — the unit-test packages. A requirement no test
48620
+ covers (docs, README, deletions, config) becomes a chore
48621
+ package, or a task of one; chore packages declare no tests
48622
+ - the last numbered package is the wiring package: CLI entry,
48623
+ workflow composition, config. A red e2e after it is a bug, not
48624
+ missing work
48625
+ - Every package with tests has a \`## Tests\` section, one line per
48626
+ declared test, copied from the architecture entries:
48627
+ \`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
48628
+ none
48629
+ - Leave everything uncommitted and finish
48604
48630
  `;
48605
48631
  const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
48606
48632
  you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
@@ -48629,7 +48655,7 @@ What each change does next (then run \`gtd land\`):
48629
48655
  `;
48630
48656
  const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
48631
48657
  - The only state file this turn may write is \`.gtd/SATISFIED.md\`;
48632
- never delete the package file (the spec-review gate reads it
48658
+ never delete the package file (the declared-tests guard reads it
48633
48659
  after you)
48634
48660
  - The package to implement is \`${pkg}\`
48635
48661
  - First check its acceptance criteria against the current tree —
@@ -48638,6 +48664,13 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
48638
48664
  with each criterion's concrete evidence (commit, file, or
48639
48665
  symbol), change nothing else, and finish. Otherwise implement
48640
48666
  normally and skip that file
48667
+ - Write the tests the package declares under \`## Tests\` first,
48668
+ then the code that makes them pass; the turn is refused unless
48669
+ every declared test is in your diff
48670
+ - Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
48671
+ from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
48672
+ step code that typechecks, and leaves them red — no
48673
+ implementation
48641
48674
  - Implement every task the package describes, no more, no less,
48642
48675
  fanning independent ones out to parallel subagents where your
48643
48676
  harness supports it; leave other package files untouched
@@ -48654,15 +48687,6 @@ ${fixFeedbackPrompt}
48654
48687
  then legitimately report itself already satisfied later
48655
48688
  - Leave everything uncommitted and finish your turn — do not commit
48656
48689
  `;
48657
- const packagesItemFixSpecPrompt = (pkg) => `${stateFileRules}
48658
- - The only state file this turn touches is
48659
- \`.gtd/SPEC_FEEDBACK.md\` — address it, then delete it
48660
- - Read it (the reviewer's concerns) and the package spec
48661
- (\`${pkg}\`), then fix the code to
48662
- resolve every concern
48663
- - Delete \`.gtd/SPEC_FEEDBACK.md\` once resolved; leave everything
48664
- else uncommitted and finish your turn
48665
- `;
48666
48690
  const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
48667
48691
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
48668
48692
  is in history. Run \`gtd judge answer\` and pipe a
@@ -48700,45 +48724,22 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
48700
48724
  turn, or land untouched to give it one more attempt at the same
48701
48725
  analysis.
48702
48726
  `;
48703
- const packagesItemSpecPreMessage = () => `Judging whether the code already satisfies each requirement in the
48704
- package spec, before spending a full review turn. Run \`gtd judge
48705
- answer\` and pipe a verdict per section — or land untouched to run
48706
- the full review (the conservative default; a skipped judgment
48707
- never suppresses anything).
48708
- `;
48709
- /** The sections a pre-judge could not clear, when it cleared the rest. */
48710
- const specScope = (failing) => failing.length > 0 ? `- A pre-judge already found the other sections satisfied. Confine
48711
- your review to only these sections:
48712
- ${failing.map((title) => ` - ${title}\n`).join("")}` : "";
48713
- const packagesItemSpecReviewPrompt = (pkg, failing = []) => `${styleBlock}
48714
-
48715
- You are reviewing a freshly-built work package against its own
48716
- spec.
48717
-
48718
- ${stateFileRules}
48719
- - The only state file this turn touches is
48720
- \`.gtd/SPEC_FEEDBACK.md\` — write it only when you find problems
48721
- - The package spec is \`${pkg}\`
48722
- ${specScope(failing)}- Verify the implementation against it: tasks done, criteria
48723
- met, code sound and consistent with the codebase. No diff is
48724
- given — read the range yourself, from \`${start()}\`
48725
- to the working tree, process-wide (it can span earlier
48726
- packages)
48727
- - You own that bar; nothing downstream re-weighs your findings
48728
- - Write nothing when the package fully satisfies its spec —
48729
- silence is your approval. Otherwise write
48730
- \`.gtd/SPEC_FEEDBACK.md\` listing what would violate the spec if
48731
- it shipped unaddressed, specific enough to act on, each as its
48732
- own \`## \` heading
48733
- - Never fix anything yourself and never delete the package
48734
- file — a later step owns that
48727
+ const buildContextPrompt = (built) => {
48728
+ const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
48729
+ return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
48730
+ - A red scenario that is NEW in this plan likely means missing wiring
48731
+ between packages; new scenarios:
48732
+ ${list(built.scenarios.added)}
48733
+ - A red CHANGED or existing scenario or test is a regression a package
48734
+ introduced; scenarios this plan changed:
48735
+ ${list(built.scenarios.changed)}
48736
+ - Each package's commits, to bisect a break:
48737
+ ${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
48735
48738
  `;
48736
- const specReviewerSystem = () => `${specReviewerPersona}
48737
-
48738
- ${agentConduct}`;
48739
- const buildFixPrompt = () => `${stateFileRules}
48739
+ };
48740
+ const buildFixPrompt = (built) => `${stateFileRules}
48740
48741
  ${fixFeedbackPrompt}
48741
- - Leave everything uncommitted — do not commit
48742
+ ${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
48742
48743
  `;
48743
48744
  const finisherSystem = () => `${finisherPersona}
48744
48745
 
@@ -49071,6 +49072,20 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
49071
49072
  Print the closing message and stop — this writes nothing itself.
49072
49073
  `;
49073
49074
  };
49075
+ const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
49076
+ turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
49077
+
49078
+ ${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
49079
+
49080
+ To accept the change, land untouched.
49081
+
49082
+ To reject it, run
49083
+
49084
+ git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
49085
+
49086
+ and land. Restoring only some of the files is a partial accept: every file
49087
+ still differing from the frozen wording is accepted.
49088
+ `;
49074
49089
  //#endregion
49075
49090
  //#region src/workflows/steps.ts
49076
49091
  const FEEDBACK = ".gtd/FEEDBACK.md";
@@ -49079,7 +49094,6 @@ const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
49079
49094
  const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
49080
49095
  const REVIEW = ".gtd/REVIEW.md";
49081
49096
  const QUALITY = ".gtd/QUALITY.md";
49082
- const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md";
49083
49097
  const planner = () => env.plannerModel ?? "";
49084
49098
  const coder = () => env.coderModel ?? "";
49085
49099
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
@@ -49127,20 +49141,7 @@ const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuiteP
49127
49141
  model: coder(),
49128
49142
  system: builderSystem()
49129
49143
  });
49130
- const fixSpec = (pkg) => agentWithSkills("fix.spec.fixing", packagesItemFixSpecPrompt(pkg), {
49131
- label: "Fixing review feedback",
49132
- file: SPEC_FEEDBACK,
49133
- model: coder(),
49134
- system: builderSystem()
49135
- });
49136
- /** Review `pkg` against its spec, focused on the `failing` sections the pre-judge could not clear. */
49137
- const reviewPackage = (pkg, failing = []) => agentWithSkills("spec.review", packagesItemSpecReviewPrompt(pkg, failing), {
49138
- label: "Reviewing the package",
49139
- model: planner(),
49140
- system: specReviewerSystem(),
49141
- allowEmpty: true
49142
- });
49143
- const fix = () => agentWithSkills("fix", buildFixPrompt(), {
49144
+ const fix = (built) => agentWithSkills("fix", buildFixPrompt(built), {
49144
49145
  label: "Fixing the check",
49145
49146
  file: FEEDBACK,
49146
49147
  model: coder(),
@@ -49248,11 +49249,29 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
49248
49249
  system: reviewerSystem(),
49249
49250
  allowEmpty: true
49250
49251
  });
49252
+ //#endregion
49253
+ //#region src/workflows/health.ts
49254
+ /** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
49255
+ const SETUP = ".gtd/SETUP.md";
49256
+ const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it under `env:` in `.gtdrc` or as GTD_FASTTESTCOMMAND.";
49257
+ /** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
49258
+ const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
49259
+ `mkdir -p ${quote(dirname(SETUP))}`,
49260
+ `printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
49261
+ `printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
49262
+ "exit 0"
49263
+ ] : [`rm -f ${quote(SETUP)}`];
49251
49264
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
49252
- const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
49253
- report: FEEDBACK,
49254
- label
49255
- });
49265
+ const baseline = async (name, label = "Checking the baseline") => {
49266
+ for (;;) {
49267
+ const green = await check(name, env.testCommand ?? "", {
49268
+ report: FEEDBACK,
49269
+ label,
49270
+ preamble: fastSuiteGuard()
49271
+ });
49272
+ if (read(".gtd/SETUP.md") === void 0) return green;
49273
+ }
49274
+ };
49256
49275
  /**
49257
49276
  * Hand a red suite that repeated attempts could not fix to a person: an
49258
49277
  * agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
@@ -49291,6 +49310,15 @@ const sameFailure = async (previous, current) => {
49291
49310
  });
49292
49311
  return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
49293
49312
  };
49313
+ const runSuite = (options) => {
49314
+ const fast = options.suite === "fast";
49315
+ return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
49316
+ report: FEEDBACK,
49317
+ label: "Running checks",
49318
+ sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
49319
+ ...fast ? { preamble: fastSuiteGuard() } : {}
49320
+ });
49321
+ };
49294
49322
  /**
49295
49323
  * Loop until the suite is green: check, and on red run `fix` — escalating
49296
49324
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
@@ -49301,11 +49329,9 @@ const healthy = async (fix, options = {}) => {
49301
49329
  let fixes = options.fixesSoFar ?? 0;
49302
49330
  let previous;
49303
49331
  for (;;) {
49304
- if (await check("health.check", env.testCommand ?? "", {
49305
- report: ".gtd/FEEDBACK.md",
49306
- label: "Running checks",
49307
- sweepOnGreen: [".gtd/ESCALATION.md"]
49308
- })) {
49332
+ const green = await runSuite(options);
49333
+ if (options.suite === "fast" && read(".gtd/SETUP.md") !== void 0) continue;
49334
+ if (green) {
49309
49335
  escalations.rounds = 0;
49310
49336
  return;
49311
49337
  }
@@ -49329,324 +49355,184 @@ const gate = (name, message) => scope(name, async () => {
49329
49355
  });
49330
49356
  });
49331
49357
  //#endregion
49332
- //#region src/workflows/diff.ts
49333
- const CONTEXT = 3;
49334
- const TOO_LARGE_LINES = 1500;
49335
- const LOCKFILES = /* @__PURE__ */ new Set([
49336
- "package-lock.json",
49337
- "npm-shrinkwrap.json",
49338
- "yarn.lock",
49339
- "pnpm-lock.yaml",
49340
- "bun.lock",
49341
- "bun.lockb",
49342
- "Cargo.lock",
49343
- "composer.lock",
49344
- "Gemfile.lock",
49345
- "poetry.lock",
49346
- "uv.lock",
49347
- "Pipfile.lock",
49348
- "go.sum",
49349
- "flake.lock",
49350
- "mix.lock",
49351
- "pubspec.lock",
49352
- "Podfile.lock",
49353
- "gradle.lockfile"
49354
- ]);
49355
- const GENERATED_GLOBS = [
49356
- "dist/**",
49357
- "build/**",
49358
- "out/**",
49359
- "coverage/**",
49360
- "node_modules/**",
49361
- "vendor/**",
49362
- ".turbo/**",
49363
- "**/__snapshots__/**",
49364
- "**/*.snap",
49365
- "**/*.min.js",
49366
- "**/*.min.css",
49367
- "**/*.map"
49368
- ];
49369
- const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
49370
- "png",
49371
- "jpg",
49372
- "jpeg",
49373
- "gif",
49374
- "webp",
49375
- "ico",
49376
- "pdf",
49377
- "zip",
49378
- "gz",
49379
- "tar",
49380
- "woff",
49381
- "woff2",
49382
- "ttf",
49383
- "otf",
49384
- "mp4",
49385
- "wasm",
49386
- "bin",
49387
- "exe",
49388
- "so",
49389
- "dylib"
49390
- ]);
49391
- const globCache = /* @__PURE__ */ new Map();
49392
- const compileGlob = (glob) => {
49393
- let pattern = "^";
49394
- let i = 0;
49395
- while (i < glob.length) {
49396
- const char = glob[i];
49397
- if (char !== "*") {
49398
- pattern += char.replace(/[.+^${}()|[\]\\?]/g, "\\$&");
49399
- i += 1;
49400
- } else if (glob[i + 1] !== "*") {
49401
- pattern += "[^/]*";
49402
- i += 1;
49403
- } else if (glob[i + 2] === "/") {
49404
- pattern += "(?:.*/)?";
49405
- i += 3;
49358
+ //#region src/workflows/scenarios.ts
49359
+ const DOC_STRING = /^(\s*)("""|```)/;
49360
+ /**
49361
+ * Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
49362
+ * dropped — except inside a doc string, whose content is test input: kept
49363
+ * whole, indented relative to its opening delimiter (as Gherkin reads it).
49364
+ */
49365
+ const scenarioText = (source) => {
49366
+ const out = [];
49367
+ let doc;
49368
+ for (const line of source.split("\n")) {
49369
+ const delimiter = DOC_STRING.exec(line);
49370
+ if (doc === void 0 && delimiter !== null) {
49371
+ doc = {
49372
+ indent: delimiter[1].length,
49373
+ fence: delimiter[2]
49374
+ };
49375
+ out.push(line.trim());
49376
+ } else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
49377
+ doc = void 0;
49378
+ out.push(line.trim());
49379
+ } else if (doc !== void 0) {
49380
+ const lead = line.length - line.trimStart().length;
49381
+ out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
49406
49382
  } else {
49407
- pattern += ".*";
49408
- i += 2;
49383
+ const trimmed = line.trim();
49384
+ if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
49409
49385
  }
49410
49386
  }
49411
- return new RegExp(`${pattern}$`);
49387
+ return out.join("\n");
49412
49388
  };
49413
- const matchesGlob = (path, glob) => {
49414
- let regex = globCache.get(glob);
49415
- if (regex === void 0) {
49416
- regex = compileGlob(glob);
49417
- globCache.set(glob, regex);
49418
- }
49419
- return regex.test(path);
49389
+ const textOf = (path) => {
49390
+ const source = read(path);
49391
+ return source === void 0 ? void 0 : scenarioText(source);
49420
49392
  };
49421
- const extensionOf = (path) => {
49422
- const base = path.split("/").pop() ?? path;
49423
- const dot = base.lastIndexOf(".");
49424
- return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
49425
- };
49426
- const isBinary = (change) => BINARY_EXTENSIONS.has(extensionOf(change.path)) || (change.before?.includes("\0") ?? false) || (change.after?.includes("\0") ?? false);
49427
- const isExcluded = (change) => change.path === ".gtd" || change.path.startsWith(".gtd/") || LOCKFILES.has(change.path.split("/").pop() ?? change.path) || GENERATED_GLOBS.some((glob) => matchesGlob(change.path, glob)) || isBinary(change);
49428
- /** `changes` with lockfiles, generated trees and binaries dropped. */
49429
- const filterChanges = (changes) => changes.filter((change) => !isExcluded(change));
49430
- const linesOf = (content) => {
49431
- if (content === void 0 || content === "") return [];
49432
- const lines = content.split("\n");
49433
- if (lines[lines.length - 1] === "") lines.pop();
49434
- return lines;
49393
+ /** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
49394
+ const freezeScenarios = (paths, previous) => {
49395
+ if (paths.length === 0) return previous;
49396
+ const texts = { ...previous?.texts };
49397
+ for (const path of paths) {
49398
+ const text = textOf(path);
49399
+ if (text !== void 0) texts[path] = text;
49400
+ }
49401
+ return Object.keys(texts).length === 0 ? previous : {
49402
+ at: head(),
49403
+ texts
49404
+ };
49435
49405
  };
49436
- /** `dp[i][j]` = the LCS length of `a[i:]` and `b[j:]`. */
49406
+ /** Paths whose wording differs from the snapshot (a deleted file counts). */
49407
+ const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
49408
+ const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
49409
+ /** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
49437
49410
  const lcsTable = (a, b) => {
49438
- const n = a.length;
49439
- const m = b.length;
49440
- const dp = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }).fill(0));
49441
- for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
49442
- return dp;
49443
- };
49444
- /** Walk an LCS table into an edit script, favoring a deletion on a tie. */
49445
- const backtrack = (a, b, dp) => {
49446
- const ops = [];
49411
+ const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
49412
+ 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]);
49413
+ return table;
49414
+ };
49415
+ /** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
49416
+ const lineDiff = (a, b) => {
49417
+ const table = lcsTable(a, b);
49418
+ const out = [];
49447
49419
  let i = 0;
49448
49420
  let j = 0;
49449
49421
  while (i < a.length && j < b.length) if (a[i] === b[j]) {
49450
- ops.push({
49451
- type: "eq",
49452
- line: a[i]
49453
- });
49454
49422
  i++;
49455
49423
  j++;
49456
- } else if (dp[i + 1][j] >= dp[i][j + 1]) {
49457
- ops.push({
49458
- type: "del",
49459
- line: a[i]
49460
- });
49461
- i++;
49462
- } else {
49463
- ops.push({
49464
- type: "add",
49465
- line: b[j]
49466
- });
49467
- j++;
49468
- }
49469
- while (i < a.length) ops.push({
49470
- type: "del",
49471
- line: a[i++]
49472
- });
49473
- while (j < b.length) ops.push({
49474
- type: "add",
49475
- line: b[j++]
49476
- });
49477
- return ops;
49478
- };
49479
- /** Longest-common-subsequence edit script between two (already trimmed) line arrays. */
49480
- const lcsOps = (a, b) => backtrack(a, b, lcsTable(a, b));
49481
- const commonAffixes = (before, after) => {
49482
- const maxPrefix = Math.min(before.length, after.length);
49483
- let prefix = 0;
49484
- while (prefix < maxPrefix && before[prefix] === after[prefix]) prefix++;
49485
- const maxSuffix = maxPrefix - prefix;
49486
- let suffix = 0;
49487
- while (suffix < maxSuffix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
49488
- return {
49489
- prefix,
49490
- suffix
49491
- };
49424
+ } else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
49425
+ else out.push(`+ ${b[j++]}`);
49426
+ return [
49427
+ ...out,
49428
+ ...a.slice(i).map((l) => `- ${l}`),
49429
+ ...b.slice(j).map((l) => `+ ${l}`)
49430
+ ];
49492
49431
  };
49493
- const buildHunks = (ops) => {
49494
- const beforeLineAt = [];
49495
- const afterLineAt = [];
49496
- let bl = 1;
49497
- let al = 1;
49498
- for (const op of ops) {
49499
- beforeLineAt.push(bl);
49500
- afterLineAt.push(al);
49501
- if (op.type !== "add") bl++;
49502
- if (op.type !== "del") al++;
49503
- }
49504
- const ranges = [];
49505
- ops.forEach((op, idx) => {
49506
- if (op.type === "eq") return;
49507
- const start = Math.max(0, idx - CONTEXT);
49508
- const end = Math.min(ops.length - 1, idx + CONTEXT);
49509
- const last = ranges[ranges.length - 1];
49510
- if (last !== void 0 && start <= last[1] + 1) last[1] = Math.max(last[1], end);
49511
- else ranges.push([start, end]);
49512
- });
49513
- return ranges.map(([start, end]) => {
49514
- const slice = ops.slice(start, end + 1);
49515
- return {
49516
- beforeStart: beforeLineAt[start],
49517
- beforeCount: slice.filter((op) => op.type !== "add").length,
49518
- afterStart: afterLineAt[start],
49519
- afterCount: slice.filter((op) => op.type !== "del").length,
49520
- lines: slice.map((op) => `${op.type === "add" ? "+" : op.type === "del" ? "-" : " "}${op.line}`)
49521
- };
49432
+ const wordingDrift = (frozen, path) => ({
49433
+ path,
49434
+ lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
49435
+ });
49436
+ /** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
49437
+ const holdWording = async (frozen) => {
49438
+ if (frozen === void 0) return;
49439
+ const drifted = driftedScenarios(frozen);
49440
+ if (drifted.length === 0) return;
49441
+ await human("scenario-wording", {
49442
+ message: scenarioWordingMessage(frozen.at, drifted.map((path) => wordingDrift(frozen, path))),
49443
+ label: "Scenario wording changed",
49444
+ acceptClean: true,
49445
+ base: frozen.at
49522
49446
  });
49447
+ const accepted = driftedScenarios(frozen);
49448
+ if (accepted.length === 0) return;
49449
+ for (const path of accepted) {
49450
+ const text = textOf(path);
49451
+ if (text === void 0) delete frozen.texts[path];
49452
+ else frozen.texts[path] = text;
49453
+ }
49454
+ frozen.at = head();
49523
49455
  };
49524
- const renderHunk = (hunk) => [`@@ -${hunk.beforeStart},${hunk.beforeCount} +${hunk.afterStart},${hunk.afterCount} @@`, ...hunk.lines];
49525
- /** One file's unified diff, or a summary line when its changed region is too large to inline. */
49526
- const renderFileDiff = (change) => {
49527
- const before = linesOf(change.before);
49528
- const after = linesOf(change.after);
49529
- const { prefix, suffix } = commonAffixes(before, after);
49530
- const beforeMiddle = before.slice(prefix, before.length - suffix);
49531
- const afterMiddle = after.slice(prefix, after.length - suffix);
49532
- if (beforeMiddle.length > TOO_LARGE_LINES || afterMiddle.length > TOO_LARGE_LINES) return `${change.path}: +${afterMiddle.length}/-${beforeMiddle.length} lines, too large to inline`;
49533
- const prefixOps = before.slice(0, prefix).map((line) => ({
49534
- type: "eq",
49535
- line
49536
- }));
49537
- const suffixOps = before.slice(before.length - suffix).map((line) => ({
49538
- type: "eq",
49539
- line
49540
- }));
49541
- const ops = [
49542
- ...prefixOps,
49543
- ...lcsOps(beforeMiddle, afterMiddle),
49544
- ...suffixOps
49545
- ];
49546
- const beforePath = change.status === "added" ? "/dev/null" : `a/${change.path}`;
49547
- const afterPath = change.status === "deleted" ? "/dev/null" : `b/${change.path}`;
49548
- return [
49549
- `--- ${beforePath}`,
49550
- `+++ ${afterPath}`,
49551
- ...buildHunks(ops).flatMap(renderHunk)
49552
- ].join("\n");
49553
- };
49554
- const omissionTrailer = (paths) => `${paths.length} file(s) omitted for the judge's byte budget: ${paths.join(", ")}`;
49555
- /** The byte size of keeping `files`' first `keepCount` entries plus the trailer the rest would need — the trailer counts against the cap too, not just the kept text. */
49556
- const sizeWithTrailer = (files, keepCount) => {
49557
- let used = 0;
49558
- for (let i = 0; i < keepCount; i++) used += Buffer.byteLength(files[i].text, "utf8") + (i > 0 ? 1 : 0);
49559
- if (files.length - keepCount === 0) return used;
49560
- const trailer = omissionTrailer(files.slice(keepCount).map((f) => f.path));
49561
- return used + (keepCount > 0 ? 1 : 0) + Buffer.byteLength(trailer, "utf8");
49562
- };
49563
- /**
49564
- * The filtered `changes`, rendered as one unified diff (files in path order)
49565
- * and bounded to `capBytes`: over the cap, whole files are dropped from the
49566
- * end — never bytes off a hunk — and named in a trailer line, so the
49567
- * judge is told what it did not see rather than handed a silently short diff.
49568
- * The trailer itself is sized against the cap along with the files it keeps,
49569
- * so a package with many touched files can never push the total past it.
49570
- */
49571
- const packageDiff = (changes, capBytes) => {
49572
- const files = filterChanges(changes).slice().sort((a, b) => a.path.localeCompare(b.path)).map((change) => ({
49573
- path: change.path,
49574
- text: renderFileDiff(change)
49575
- }));
49576
- let keepCount = files.length;
49577
- while (keepCount > 0 && sizeWithTrailer(files, keepCount) > capBytes) keepCount--;
49578
- const dropped = files.slice(keepCount).map((f) => f.path);
49579
- return [...files.slice(0, keepCount).map((f) => f.text), ...dropped.length > 0 ? [omissionTrailer(dropped)] : []].join("\n");
49456
+ /** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
49457
+ const guarded = (frozen, turn, within) => async () => {
49458
+ await turn();
49459
+ if (within === void 0) await holdWording(frozen);
49460
+ else await scope(within, () => holdWording(frozen));
49580
49461
  };
49581
49462
  //#endregion
49582
49463
  //#region src/workflows/packages.ts
49583
- const MAX_SECTIONS = 8;
49584
- const DIFF_KEY = "diff";
49585
- const sectionQuestion = (id, title) => ({
49586
- id,
49587
- primitive: "noul",
49588
- instructions: `Is the requirement "${title}" — the "${id}" evidence — already fully satisfied by the code in the "${DIFF_KEY}" evidence?`,
49589
- criteria: "Judge from the requirement text and the diff evidence alone. Only answer yes at a probability clearing the threshold below if genuinely confident nothing in this section is missing."
49590
- });
49591
- /**
49592
- * Review one freshly built package against its spec: a pre-judge per
49593
- * section, then an agent review of the sections it did not confidently
49594
- * clear. `since` is the commit the package's own build started from — the
49595
- * pre-judge's evidence is a diff over exactly that range, never an earlier
49596
- * package's commits. Resolves `true` when approved.
49597
- */
49598
- const specReview = async (pkg, since) => {
49599
- const found = sectionBodies(read(pkg) ?? "");
49600
- const titles = found.map((section) => section.title);
49601
- const judged = titles.length > 0 && titles.length <= MAX_SECTIONS;
49602
- const evidence = {};
49603
- const questions = [];
49604
- if (judged) {
49605
- found.forEach(({ title, body }, i) => {
49606
- evidence[`section-${i + 1}`] = body;
49607
- questions.push(sectionQuestion(`section-${i + 1}`, title));
49464
+ /** Package 0: the e2e scenarios, written red. */
49465
+ const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
49466
+ const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
49467
+ /** The `## Tests` entries of a package file; malformed lines are ignored. */
49468
+ const declaredTests = (packageText) => {
49469
+ const found = [];
49470
+ let inTests = false;
49471
+ for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
49472
+ else if (inTests) {
49473
+ const match = TEST_LINE.exec(line);
49474
+ if (match) found.push({
49475
+ level: match[1],
49476
+ path: match[2]
49608
49477
  });
49609
- const capBytes = Math.floor(numeric(vars.judgeBudgetBytes, 32768) / (found.length + 1));
49610
- evidence[DIFF_KEY] = packageDiff(changesSince(since), capBytes);
49611
49478
  }
49612
- const { answers, truncated } = await judge("spec.pre", {
49613
- questions,
49614
- evidence,
49615
- message: packagesItemSpecPreMessage(),
49616
- label: "Judging spec coverage"
49617
- });
49618
- const clearMinP = numeric(vars.specPreJudge, Infinity);
49619
- const failing = titles.filter((_, i) => {
49620
- const id = `section-${i + 1}`;
49621
- return !judged || truncated.includes(id) || truncated.includes(DIFF_KEY) || !answered(answers[id], "yes", clearMinP);
49622
- });
49623
- if (titles.length > 0 && failing.length === 0) return true;
49624
- await reviewPackage(pkg, failing);
49625
- if (wrote(".gtd/SPEC_FEEDBACK.md")) return false;
49626
- if (changes(".gtd/SPEC_FEEDBACK.md").length > 0 || changes().length === 0) return true;
49627
- return refuse("gtd land: no declared pattern matches the pending changes — write .gtd/SPEC_FEEDBACK.md to request changes, or change nothing to approve");
49628
- };
49629
- /** Build `pkg`, keep the suite green, review it against its spec, and close it out, which removes it. */
49630
- const packageItem = async (pkg) => {
49631
- const since = head();
49632
- await build(pkg);
49633
- for (;;) {
49634
- await healthy(fixSuite);
49635
- if (await specReview(pkg, since)) break;
49636
- await fixSpec(pkg);
49637
- }
49638
- await run$3("closing", removeScript([
49479
+ return found;
49480
+ };
49481
+ /**
49482
+ * Refuse a build or fix turn unless every test `pkg` declares exists and is in the
49483
+ * diff since `since`. The package text is read as of `since`, so a builder
49484
+ * editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
49485
+ * written, existing in the tree suffices.
49486
+ */
49487
+ const requireDeclaredTests = (pkg, since) => {
49488
+ const range = changesSince(since);
49489
+ const text = range.get(pkg)?.before ?? read(pkg) ?? "";
49490
+ const satisfied = read(".gtd/SATISFIED.md") !== void 0;
49491
+ const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
49492
+ if (missing.length === 0) return;
49493
+ 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")}`);
49494
+ };
49495
+ /** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
49496
+ const packageItem = async (pkg, frozen) => {
49497
+ const from = head();
49498
+ const declared = (turn) => async () => {
49499
+ await turn();
49500
+ requireDeclaredTests(pkg, from);
49501
+ };
49502
+ await guarded(frozen, declared(() => build(pkg)))();
49503
+ await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
49504
+ await run$3("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
49505
+ return {
49639
49506
  pkg,
49640
- SPEC_FEEDBACK,
49641
- ".gtd/SATISFIED.md",
49642
- ARCHITECTURE
49643
- ]), { label: "Closing out the package" });
49507
+ from,
49508
+ to: head()
49509
+ };
49644
49510
  };
49645
49511
  /** The first queued package — the one a package step works on. */
49646
49512
  const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
49513
+ const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
49647
49514
  /** Build every package file under `.gtd/packages/`, in name order. */
49648
- const packages = () => scope("packages", async () => {
49649
- for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) await scope("item", () => packageItem(pkg));
49515
+ const packages = (carried) => scope("packages", async () => {
49516
+ let frozen = carried;
49517
+ const ranges = [];
49518
+ let touched = [];
49519
+ for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
49520
+ const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
49521
+ ranges.push(range);
49522
+ if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
49523
+ touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
49524
+ frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
49525
+ }
49526
+ }
49527
+ const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
49528
+ return {
49529
+ ranges,
49530
+ scenarios: {
49531
+ added: paths("added"),
49532
+ changed: paths("modified")
49533
+ },
49534
+ frozen
49535
+ };
49650
49536
  });
49651
49537
  //#endregion
49652
49538
  //#region src/workflows/planning.ts
@@ -49679,30 +49565,8 @@ const architecture = () => scope("architecture", async () => {
49679
49565
  } while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
49680
49566
  await decompose();
49681
49567
  if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
49568
+ 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");
49682
49569
  });
49683
- /** A package file name from a plan's first heading. */
49684
- const slug = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "package";
49685
- /** Whether the settled plan needs its own architecture pass, or goes straight to one package. */
49686
- const architecturePass = async () => {
49687
- const { answers, truncated } = await judge("architecture-pre", {
49688
- questions: [{
49689
- id: "architectureWarranted",
49690
- primitive: "noul",
49691
- instructions: "Given the settled concerns in `.gtd/REQUIREMENTS.md` (in state), does this plan warrant a dedicated architecture pass — real structural decisions, multiple integration points, or a non-obvious tradeoff — before packages are written?",
49692
- criteria: "Answer yes if uncertain; a trivial, single-concern, mechanical plan with no real design decision answers no."
49693
- }],
49694
- evidence: { requirements: read(".gtd/REQUIREMENTS.md") ?? "" },
49695
- message: architecturePreMessage(),
49696
- label: "Judging whether this plan warrants an architecture pass"
49697
- });
49698
- const skip = truncated.length === 0 && answered(answers.architectureWarranted, "no", numeric(vars.architectureSkipMinP, Infinity));
49699
- const plan = read(REQUIREMENTS);
49700
- if (skip && plan !== void 0) {
49701
- await run$3("architecture-promote", moveScript(REQUIREMENTS, `.gtd/packages/01-${slug(sections(plan)[0] ?? "")}.md`), { label: "Promoting the plan straight to a package" });
49702
- return;
49703
- }
49704
- await architecture();
49705
- };
49706
49570
  //#endregion
49707
49571
  //#region src/workflows/review.ts
49708
49572
  /** Split a `qualityReviews` value into its lenses: on `,`, trimmed, blanks dropped. */
@@ -49717,10 +49581,10 @@ const qualityLap = async () => {
49717
49581
  return (read(".gtd/QUALITY.md") ?? "").length > 0 ? "findings" : "clean";
49718
49582
  };
49719
49583
  /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
49720
- const fixQualityFindings = async (escalations) => {
49584
+ const fixQualityFindings = async (escalations, frozen) => {
49721
49585
  for (let turns = 0; turns < 3; turns++) {
49722
- await fixQuality();
49723
- if (changes(".gtd/QUALITY.md").some((c) => c.status === "deleted")) return true;
49586
+ await guarded(frozen, fixQuality)();
49587
+ if (read(".gtd/QUALITY.md") === void 0) return true;
49724
49588
  }
49725
49589
  await escalation(escalations);
49726
49590
  return false;
@@ -49822,8 +49686,8 @@ const routeNotes = async (notes, r) => {
49822
49686
  answeredAt = head();
49823
49687
  }
49824
49688
  if (nits.length > 0) {
49825
- await fixNits(nits);
49826
- await healthy(fix, { escalations: r.escalations });
49689
+ await guarded(r.frozen, () => fixNits(nits), "review")();
49690
+ await healthy(guarded(r.frozen, fix, "review"), { escalations: r.escalations });
49827
49691
  }
49828
49692
  if (edits.length > 0) {
49829
49693
  await r.close();
@@ -49841,7 +49705,7 @@ const routeNotes = async (notes, r) => {
49841
49705
  reviewed: answeredAt
49842
49706
  };
49843
49707
  };
49844
- const finish = async (reviewed, collectedAt, escalations) => {
49708
+ const finish = async (reviewed, collectedAt, escalations, frozen) => {
49845
49709
  const round = head();
49846
49710
  const since = changesSince(reviewed);
49847
49711
  const edited = since.filter(isCodeEdit);
@@ -49874,18 +49738,19 @@ const finish = async (reviewed, collectedAt, escalations) => {
49874
49738
  return routeNotes(notes, {
49875
49739
  round,
49876
49740
  escalations,
49741
+ frozen,
49877
49742
  close,
49878
49743
  outcome,
49879
49744
  unfolded
49880
49745
  });
49881
49746
  };
49882
49747
  /** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
49883
- const reviewOnce = async (base, carry, escalations) => {
49748
+ const reviewOnce = async (base, carry, escalations, frozen) => {
49884
49749
  await reviewing(base, carry);
49885
49750
  const risks = reviewRisks(read(".gtd/REVIEW.md") ?? "");
49886
49751
  if (risks.length === 0) return;
49887
- await fixRisks(risks);
49888
- await healthy(fix, { escalations });
49752
+ await guarded(frozen, () => fixRisks(risks), "review")();
49753
+ await healthy(guarded(frozen, fix, "review"), { escalations });
49889
49754
  await reviewing(base, carry);
49890
49755
  };
49891
49756
  /**
@@ -49894,17 +49759,17 @@ const reviewOnce = async (base, carry, escalations) => {
49894
49759
  * another planning lap, questions are answered in place, nits fixed in one
49895
49760
  * batch, praise dropped.
49896
49761
  */
49897
- const review = async (base, escalations = { rounds: 0 }) => {
49762
+ const review = async (base, escalations = { rounds: 0 }, frozen) => {
49898
49763
  let carry;
49899
49764
  for (;;) {
49900
- await reviewOnce(base, carry, escalations);
49765
+ await reviewOnce(base, carry, escalations, frozen);
49901
49766
  carry = void 0;
49902
49767
  let reviewed = head();
49903
49768
  let collectedAt;
49904
49769
  for (;;) {
49905
49770
  collectedAt = await converse(base, collectedAt);
49906
49771
  if (collectedAt === "missing") break;
49907
- const result = await finish(reviewed, collectedAt, escalations);
49772
+ const result = await finish(reviewed, collectedAt, escalations, frozen);
49908
49773
  if (!("next" in result)) return result;
49909
49774
  if (result.next === "rereview") {
49910
49775
  carry = result.carry;
@@ -49915,14 +49780,20 @@ const review = async (base, escalations = { rounds: 0 }) => {
49915
49780
  }
49916
49781
  };
49917
49782
  /** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
49918
- const buildTail = (fixFirst, base) => scope("build", async () => {
49783
+ const buildTail = (fixFirst, base, built) => scope("build", async () => {
49784
+ const frozen = built?.frozen;
49919
49785
  const escalations = { rounds: 0 };
49786
+ const guardedFix = guarded(frozen, () => fix());
49920
49787
  let redFirst = fixFirst;
49788
+ if (!fixFirst) await healthy(guarded(frozen, () => fix(built)), {
49789
+ escalations,
49790
+ sweepOnGreen: [ARCHITECTURE]
49791
+ });
49921
49792
  let lapped = false;
49922
49793
  for (;;) {
49923
49794
  if (redFirst) {
49924
- await fix();
49925
- await healthy(fix, {
49795
+ await guardedFix();
49796
+ await healthy(guardedFix, {
49926
49797
  fixesSoFar: 1,
49927
49798
  escalations
49928
49799
  });
@@ -49930,8 +49801,8 @@ const buildTail = (fixFirst, base) => scope("build", async () => {
49930
49801
  }
49931
49802
  const lap = lapped ? "clean" : await qualityLap();
49932
49803
  lapped = true;
49933
- if (lap === "clean") return review(base, escalations);
49934
- if (await fixQualityFindings(escalations)) await healthy(fix, { escalations });
49804
+ if (lap === "clean") return review(base, escalations, frozen);
49805
+ if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
49935
49806
  else redFirst = true;
49936
49807
  }
49937
49808
  });
@@ -49942,14 +49813,13 @@ const defaults = {
49942
49813
  reviewBase: "",
49943
49814
  judgeBudgetBytes: "32768",
49944
49815
  judgeIdenticalMinP: "0.7",
49945
- specPreJudge: "0.9",
49946
49816
  reviewNoteActionable: "0.7",
49947
- architectureSkipMinP: "0.85",
49948
49817
  qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
49949
49818
  };
49950
49819
  /** Environment settings. */
49951
49820
  const envDefaults = {
49952
49821
  testCommand: "npm test",
49822
+ fastTestCommand: "",
49953
49823
  plannerModel: "smart",
49954
49824
  coderModel: "base"
49955
49825
  };
@@ -49965,8 +49835,6 @@ const skills = (vars) => ({
49965
49835
  "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
49966
49836
  "packages.item": ["test-driven-development", "incremental-implementation"],
49967
49837
  "packages.item.fix.suite": ["debugging-and-error-recovery"],
49968
- "packages.item.fix.spec": ["incremental-implementation", "code-simplification"],
49969
- "packages.item.spec": ["code-review-and-quality", "spec-driven-development"],
49970
49838
  "packages.item.health": ["debugging-and-error-recovery"],
49971
49839
  build: ["debugging-and-error-recovery"],
49972
49840
  "build.health": ["debugging-and-error-recovery"],
@@ -49982,7 +49850,6 @@ const access = (vars) => ({
49982
49850
  design: { write: [] },
49983
49851
  architecture: { write: [".gtd/REQUIREMENTS.md"] },
49984
49852
  "architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
49985
- "packages.item.spec": { write: [".gtd/SPEC_FEEDBACK.md"] },
49986
49853
  "build.review": { write: [".gtd/REVIEW.md"] },
49987
49854
  "build.review.fix.nits": {},
49988
49855
  "build.review.fix.risks": {},
@@ -49998,7 +49865,8 @@ var unified_exports = /* @__PURE__ */ __exportAll({
49998
49865
  QUALITY: () => QUALITY,
49999
49866
  REQUIREMENTS: () => REQUIREMENTS,
50000
49867
  REVIEW: () => REVIEW,
50001
- SPEC_FEEDBACK: () => SPEC_FEEDBACK,
49868
+ SCENARIO_PACKAGE: () => SCENARIO_PACKAGE,
49869
+ SETUP: () => SETUP,
50002
49870
  access: () => access,
50003
49871
  afterTail: () => afterTail,
50004
49872
  agentWithSkills: () => agentWithSkills,
@@ -50006,7 +49874,6 @@ var unified_exports = /* @__PURE__ */ __exportAll({
50006
49874
  answerReviewQuestions: () => answerReviewQuestions,
50007
49875
  answerTechnicalQuestions: () => answerTechnicalQuestions,
50008
49876
  architecture: () => architecture,
50009
- architecturePass: () => architecturePass,
50010
49877
  author: () => author,
50011
49878
  awaitReview: () => awaitReview,
50012
49879
  base: () => base,
@@ -50015,24 +49882,29 @@ var unified_exports = /* @__PURE__ */ __exportAll({
50015
49882
  buildTail: () => buildTail,
50016
49883
  builtInLenses: () => builtInLenses,
50017
49884
  collecting: () => collecting,
49885
+ declaredTests: () => declaredTests,
50018
49886
  decompose: () => decompose,
50019
49887
  default: () => unified,
50020
49888
  defaults: () => defaults,
50021
49889
  describeEscalation: () => describeEscalation,
50022
49890
  design: () => design,
49891
+ driftedScenarios: () => driftedScenarios,
50023
49892
  envDefaults: () => envDefaults,
50024
49893
  escalate: () => escalate,
50025
49894
  escalation: () => escalation,
50026
49895
  escalationExhausted: () => escalationExhausted,
49896
+ fastSuiteGuard: () => fastSuiteGuard,
50027
49897
  fix: () => fix,
50028
49898
  fixNits: () => fixNits,
50029
49899
  fixQuality: () => fixQuality,
50030
49900
  fixQualityFindings: () => fixQualityFindings,
50031
49901
  fixRisks: () => fixRisks,
50032
- fixSpec: () => fixSpec,
50033
49902
  fixSuite: () => fixSuite,
49903
+ freezeScenarios: () => freezeScenarios,
50034
49904
  gate: () => gate,
49905
+ guarded: () => guarded,
50035
49906
  healthy: () => healthy,
49907
+ holdWording: () => holdWording,
50036
49908
  lensesOf: () => lensesOf,
50037
49909
  nextPackage: () => nextPackage,
50038
49910
  ordinaryStart: () => ordinaryStart,
@@ -50041,19 +49913,20 @@ var unified_exports = /* @__PURE__ */ __exportAll({
50041
49913
  planAndBuild: () => planAndBuild,
50042
49914
  qualityLap: () => qualityLap,
50043
49915
  reUnwind: () => reUnwind,
49916
+ requireDeclaredTests: () => requireDeclaredTests,
50044
49917
  retryQuestion: () => retryQuestion,
50045
49918
  review: () => review,
50046
49919
  reviewMissing: () => reviewMissing,
50047
- reviewPackage: () => reviewPackage,
50048
49920
  reviewQuality: () => reviewQuality,
50049
49921
  reviewing: () => reviewing,
49922
+ scenarioText: () => scenarioText,
50050
49923
  skills: () => skills,
50051
- specReview: () => specReview,
50052
49924
  steering: () => steering,
50053
49925
  summary: () => summary,
50054
49926
  threads: () => threads,
50055
49927
  triage: () => triage$1,
50056
- unwind: () => unwind
49928
+ unwind: () => unwind,
49929
+ wordingDrift: () => wordingDrift
50057
49930
  });
50058
49931
  /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
50059
49932
  const unwind = () => {
@@ -50079,11 +49952,13 @@ const reUnwind = async (feedback) => {
50079
49952
  /** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
50080
49953
  const planAndBuild = async (firstBase) => {
50081
49954
  let base = firstBase;
49955
+ let carried;
50082
49956
  for (;;) {
50083
49957
  await design(base);
50084
- await architecturePass();
50085
- await packages();
50086
- const outcome = await buildTail(false, base);
49958
+ await architecture();
49959
+ const built = await packages(carried);
49960
+ carried = built.frozen;
49961
+ const outcome = await buildTail(false, base, built);
50087
49962
  if (outcome.verdict === "signoff") return;
50088
49963
  await reUnwind(outcome);
50089
49964
  base = outcome.base;
@@ -58495,7 +58370,10 @@ var ConfigService = class ConfigService extends Tag("ConfigService")() {
58495
58370
  //#endregion
58496
58371
  //#region src/workflow/init.ts
58497
58372
  const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
58498
- const INIT_ENV = { testCommand: "npm test" };
58373
+ const INIT_ENV = {
58374
+ testCommand: "npm test",
58375
+ fastTestCommand: ""
58376
+ };
58499
58377
  const MODES_SUGGESTION = {
58500
58378
  qa: { format: "npx prettier --write \"$GTD_FILE\"" },
58501
58379
  review: { format: "npx prettier --write \"$GTD_FILE\"" }