@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +28 -30
- package/dist/gtd.bundle.mjs +338 -460
- package/package.json +2 -1
- package/skills/authoring/SKILL.md +6 -7
- package/src/flows/helpers.ts +4 -2
- package/src/flows/scripts.test.ts +28 -1
- package/src/flows/scripts.ts +3 -0
- package/src/workflows/access.test.ts +0 -1
- package/src/workflows/access.ts +0 -1
- package/src/workflows/health.test.ts +21 -1
- package/src/workflows/health.ts +47 -8
- package/src/workflows/packages.test.ts +91 -0
- package/src/workflows/packages.ts +96 -76
- package/src/workflows/planning.ts +5 -49
- package/src/workflows/prose.ts +1 -9
- package/src/workflows/review.test.ts +88 -1
- package/src/workflows/review.ts +41 -16
- package/src/workflows/scenarios.test.ts +256 -0
- package/src/workflows/scenarios.ts +137 -0
- package/src/workflows/skills.test.ts +0 -4
- package/src/workflows/skills.ts +0 -2
- package/src/workflows/steps.test.ts +0 -10
- package/src/workflows/steps.ts +2 -20
- package/src/workflows/text.test.ts +60 -0
- package/src/workflows/text.ts +117 -95
- package/src/workflows/unified.ts +8 -4
- package/src/workflows/vars.ts +1 -2
- package/src/workflows/diff.test.ts +0 -115
- package/src/workflows/diff.ts +0 -306
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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
|
|
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
|
|
48558
|
-
|
|
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
|
|
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
|
|
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/\`
|
|
48585
|
-
|
|
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
|
|
48589
|
-
|
|
48590
|
-
|
|
48591
|
-
|
|
48592
|
-
|
|
48593
|
-
|
|
48594
|
-
|
|
48595
|
-
|
|
48596
|
-
|
|
48597
|
-
|
|
48598
|
-
|
|
48599
|
-
-
|
|
48600
|
-
|
|
48601
|
-
|
|
48602
|
-
|
|
48603
|
-
|
|
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
|
|
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
|
|
48704
|
-
|
|
48705
|
-
|
|
48706
|
-
|
|
48707
|
-
|
|
48708
|
-
|
|
48709
|
-
|
|
48710
|
-
|
|
48711
|
-
|
|
48712
|
-
|
|
48713
|
-
|
|
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
|
-
|
|
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
|
|
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") =>
|
|
49253
|
-
|
|
49254
|
-
|
|
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
|
-
|
|
49305
|
-
|
|
49306
|
-
|
|
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/
|
|
49333
|
-
const
|
|
49334
|
-
|
|
49335
|
-
|
|
49336
|
-
|
|
49337
|
-
|
|
49338
|
-
|
|
49339
|
-
|
|
49340
|
-
|
|
49341
|
-
|
|
49342
|
-
|
|
49343
|
-
|
|
49344
|
-
|
|
49345
|
-
|
|
49346
|
-
|
|
49347
|
-
|
|
49348
|
-
|
|
49349
|
-
|
|
49350
|
-
|
|
49351
|
-
|
|
49352
|
-
|
|
49353
|
-
|
|
49354
|
-
|
|
49355
|
-
|
|
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
|
-
|
|
49408
|
-
|
|
49383
|
+
const trimmed = line.trim();
|
|
49384
|
+
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
49409
49385
|
}
|
|
49410
49386
|
}
|
|
49411
|
-
return
|
|
49387
|
+
return out.join("\n");
|
|
49412
49388
|
};
|
|
49413
|
-
const
|
|
49414
|
-
|
|
49415
|
-
|
|
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
|
-
|
|
49422
|
-
|
|
49423
|
-
|
|
49424
|
-
|
|
49425
|
-
|
|
49426
|
-
const
|
|
49427
|
-
|
|
49428
|
-
|
|
49429
|
-
|
|
49430
|
-
|
|
49431
|
-
|
|
49432
|
-
|
|
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
|
-
/**
|
|
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
|
|
49439
|
-
|
|
49440
|
-
|
|
49441
|
-
|
|
49442
|
-
|
|
49443
|
-
|
|
49444
|
-
|
|
49445
|
-
const
|
|
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 (
|
|
49457
|
-
|
|
49458
|
-
|
|
49459
|
-
|
|
49460
|
-
})
|
|
49461
|
-
|
|
49462
|
-
|
|
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
|
|
49494
|
-
|
|
49495
|
-
|
|
49496
|
-
|
|
49497
|
-
|
|
49498
|
-
|
|
49499
|
-
|
|
49500
|
-
|
|
49501
|
-
|
|
49502
|
-
|
|
49503
|
-
|
|
49504
|
-
|
|
49505
|
-
|
|
49506
|
-
|
|
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
|
-
|
|
49525
|
-
|
|
49526
|
-
|
|
49527
|
-
|
|
49528
|
-
|
|
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
|
-
|
|
49584
|
-
const
|
|
49585
|
-
const
|
|
49586
|
-
|
|
49587
|
-
|
|
49588
|
-
|
|
49589
|
-
|
|
49590
|
-
|
|
49591
|
-
|
|
49592
|
-
|
|
49593
|
-
|
|
49594
|
-
|
|
49595
|
-
|
|
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
|
-
|
|
49613
|
-
|
|
49614
|
-
|
|
49615
|
-
|
|
49616
|
-
|
|
49617
|
-
|
|
49618
|
-
|
|
49619
|
-
|
|
49620
|
-
|
|
49621
|
-
|
|
49622
|
-
|
|
49623
|
-
|
|
49624
|
-
|
|
49625
|
-
if (
|
|
49626
|
-
|
|
49627
|
-
|
|
49628
|
-
|
|
49629
|
-
|
|
49630
|
-
const
|
|
49631
|
-
const
|
|
49632
|
-
|
|
49633
|
-
|
|
49634
|
-
|
|
49635
|
-
|
|
49636
|
-
|
|
49637
|
-
}
|
|
49638
|
-
|
|
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
|
-
|
|
49641
|
-
|
|
49642
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
49925
|
-
await healthy(
|
|
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(
|
|
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
|
-
|
|
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
|
|
50085
|
-
await packages();
|
|
50086
|
-
|
|
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 = {
|
|
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\"" }
|