@pmelab/gtd 18.1.0 → 20.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 +69 -69
- package/claude/hooks/doors.ts +34 -0
- package/claude/hooks/drive.ts +1 -0
- package/claude/hooks/register.tsx +12 -13
- package/claude/hooks/ship.ts +3 -2
- package/dist/gtd.bundle.mjs +847 -721
- package/package.json +3 -2
- package/skills/authoring/SKILL.md +43 -32
- package/src/flows/helpers.ts +4 -2
- package/src/flows/runtime.ts +22 -17
- package/src/flows/scripts.test.ts +28 -1
- package/src/flows/scripts.ts +3 -0
- package/src/workflows/access.test.ts +3 -4
- package/src/workflows/access.ts +0 -1
- package/src/workflows/{unified.ts → bundled.ts} +38 -31
- package/src/workflows/health.test.ts +21 -1
- package/src/workflows/health.ts +47 -8
- package/src/workflows/index.ts +1 -1
- 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 +91 -4
- package/src/workflows/review.ts +42 -17
- package/src/workflows/scenarios.test.ts +256 -0
- package/src/workflows/scenarios.ts +137 -0
- package/src/workflows/skills.test.ts +2 -6
- package/src/workflows/skills.ts +0 -2
- package/src/workflows/steps.test.ts +2 -12
- package/src/workflows/steps.ts +2 -20
- package/src/workflows/text.test.ts +60 -0
- package/src/workflows/text.ts +118 -96
- package/src/workflows/vars.ts +1 -2
- package/claude/hooks/entry.ts +0 -34
- package/src/workflows/diff.test.ts +0 -115
- package/src/workflows/diff.ts +0 -306
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -30964,7 +30964,7 @@ const changesSince = (hash, pattern) => {
|
|
|
30964
30964
|
};
|
|
30965
30965
|
/** The commit the process stands on at this point of the flow. */
|
|
30966
30966
|
const head = () => ctx().head();
|
|
30967
|
-
/** The process's diff base: the commit before it began, or the base `gtd --
|
|
30967
|
+
/** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
|
|
30968
30968
|
const start = () => ctx().start();
|
|
30969
30969
|
/** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
|
|
30970
30970
|
const skillsFor = (localName) => ctx().skillsFor(localName);
|
|
@@ -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
|
};
|
|
@@ -47620,6 +47622,9 @@ const TRAILER_READERS = {
|
|
|
47620
47622
|
"Gtd-Step": (value, into) => {
|
|
47621
47623
|
into.step ??= parseStepId(value);
|
|
47622
47624
|
},
|
|
47625
|
+
"Gtd-Workflow": (value, into) => {
|
|
47626
|
+
if (value !== "") into.workflow ??= value;
|
|
47627
|
+
},
|
|
47623
47628
|
"Gtd-Judge": (value, into) => {
|
|
47624
47629
|
const parsed = parseJson(value);
|
|
47625
47630
|
if (isVerdict(parsed)) into.judge.push(parsed);
|
|
@@ -47644,6 +47649,7 @@ const parseCommitMessage = (message) => {
|
|
|
47644
47649
|
const subject = (newline === -1 ? message : message.slice(0, newline)).trim();
|
|
47645
47650
|
const trailers = {
|
|
47646
47651
|
step: void 0,
|
|
47652
|
+
workflow: void 0,
|
|
47647
47653
|
reviewBase: void 0,
|
|
47648
47654
|
judge: [],
|
|
47649
47655
|
vars: {},
|
|
@@ -47660,6 +47666,7 @@ const parseCommitMessage = (message) => {
|
|
|
47660
47666
|
const formatCommitMessage = (spec) => {
|
|
47661
47667
|
const lines = [];
|
|
47662
47668
|
if (spec.step !== void 0) lines.push(`Gtd-Step: ${formatStepId(spec.step)}`);
|
|
47669
|
+
if (spec.workflow !== void 0) lines.push(`Gtd-Workflow: ${spec.workflow}`);
|
|
47663
47670
|
if (spec.reviewBase !== void 0) lines.push(`Gtd-Review-Base: ${spec.reviewBase}`);
|
|
47664
47671
|
for (const [name, value] of Object.entries(spec.vars ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(`Gtd-Var: ${name}=${value}`);
|
|
47665
47672
|
if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
|
|
@@ -47767,11 +47774,6 @@ const unknownOptions = (step) => {
|
|
|
47767
47774
|
return `gtd: step "${step.name}": unknown key(s) ${unknown.join(", ")} in ${step.kind}() options`;
|
|
47768
47775
|
};
|
|
47769
47776
|
const replay = async (input) => {
|
|
47770
|
-
let entryRead = false;
|
|
47771
|
-
const args = { get entry() {
|
|
47772
|
-
entryRead = true;
|
|
47773
|
-
return input.episode.entry;
|
|
47774
|
-
} };
|
|
47775
47777
|
const commits = input.episode.commits.map((c) => ({
|
|
47776
47778
|
...c,
|
|
47777
47779
|
parsed: parseCommitMessage(c.message)
|
|
@@ -47962,8 +47964,7 @@ const replay = async (input) => {
|
|
|
47962
47964
|
kind: "rest",
|
|
47963
47965
|
rest: step,
|
|
47964
47966
|
trace,
|
|
47965
|
-
landed
|
|
47966
|
-
entryRead
|
|
47967
|
+
landed
|
|
47967
47968
|
});
|
|
47968
47969
|
pendingUsed = true;
|
|
47969
47970
|
landed = step;
|
|
@@ -48070,7 +48071,7 @@ const replay = async (input) => {
|
|
|
48070
48071
|
};
|
|
48071
48072
|
installContext(context);
|
|
48072
48073
|
try {
|
|
48073
|
-
const flowDone = input.flow(
|
|
48074
|
+
const flowDone = input.flow({}).then(() => {
|
|
48074
48075
|
if (outcome === void 0 && trace.length === 0) outcome = {
|
|
48075
48076
|
kind: "failed",
|
|
48076
48077
|
message: "gtd: the flow returned without reaching any step"
|
|
@@ -48157,8 +48158,7 @@ const architectPersona = `You are the technical planning voice in gtd's build pi
|
|
|
48157
48158
|
once product concerns are settled, reading them cold, no carried
|
|
48158
48159
|
conversation. Work out the *how* per concern — structure, data models,
|
|
48159
48160
|
tech-stack choices, error handling — raise the technical open
|
|
48160
|
-
questions, then
|
|
48161
|
-
judgement call: the grouping is already decided.`;
|
|
48161
|
+
questions, then pin interfaces and tests in the architecture document.`;
|
|
48162
48162
|
const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
|
|
48163
48163
|
deliberately separate from whoever wrote the code, with no attachment
|
|
48164
48164
|
to it. Write a structured review document grouping a diff into
|
|
@@ -48167,12 +48167,6 @@ just approving; answer the human's questions inline in the review;
|
|
|
48167
48167
|
and, when asked, fix the small nits the human flagged and the risks
|
|
48168
48168
|
you marked yourself, each in one batch.
|
|
48169
48169
|
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
48170
|
const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
|
|
48177
48171
|
package's declared scope end to end, tests first, and come back to fix
|
|
48178
48172
|
things when redirected — a failing check, or reviewer feedback. Stay
|
|
@@ -48382,19 +48376,11 @@ carries it.
|
|
|
48382
48376
|
What each change does next (then run \`gtd land\`):
|
|
48383
48377
|
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
48384
48378
|
`;
|
|
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
48379
|
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
48394
48380
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
48395
48381
|
|
|
48396
48382
|
What each change does next (then run \`gtd land\`):
|
|
48397
|
-
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --
|
|
48383
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --workflow fix\` from a clean \`idle\`.
|
|
48398
48384
|
`;
|
|
48399
48385
|
const reviewGateBlockedMessage = () => `The test baseline is red — gtd will not start a review on a broken suite.
|
|
48400
48386
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
@@ -48554,22 +48540,46 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
48554
48540
|
authority is to merge only, never to split — the whole problem
|
|
48555
48541
|
is over-granularity
|
|
48556
48542
|
- Record every merge under \`## Merged Concerns\`,
|
|
48557
|
-
carrying both merged requirements verbatim so
|
|
48558
|
-
|
|
48543
|
+
carrying both merged requirements verbatim so each requirement
|
|
48544
|
+
stays traceable to its tests
|
|
48559
48545
|
- A merge raises no open question and stops for no
|
|
48560
48546
|
human — do not route it to \`architecture.gate\` for a veto; the
|
|
48561
|
-
human sees it when reviewing the plan
|
|
48562
|
-
real safety net
|
|
48547
|
+
human sees it when reviewing the plan
|
|
48563
48548
|
- Prefer fewer, larger packages — the smallest independently
|
|
48564
48549
|
valuable change, not the smallest change that compiles
|
|
48565
48550
|
- Every open point here is TECHNICAL — triage already resolved
|
|
48566
48551
|
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
48567
48552
|
yourself unless you genuinely cannot defend a default; a wrong
|
|
48568
|
-
technical call is still caught
|
|
48553
|
+
technical call is still caught by the full run and the quality lap
|
|
48569
48554
|
- The narrow exception: a simplification that drops something
|
|
48570
48555
|
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
48571
48556
|
silent default
|
|
48572
48557
|
|
|
48558
|
+
- \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
|
|
48559
|
+
off. It carries these sections, in this order:
|
|
48560
|
+
1. \`## Open Questions\` (qa format, first, only when present)
|
|
48561
|
+
2. \`## Interfaces\` — new and changed signatures/contracts
|
|
48562
|
+
only, as fenced TypeScript (or the repository's language)
|
|
48563
|
+
3. \`## Call Stacks\` — seam level, entry point → module
|
|
48564
|
+
boundary; one line per hop, never per line of code
|
|
48565
|
+
4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
|
|
48566
|
+
(whatever e2e framework the repository uses), each preceded
|
|
48567
|
+
by a line \`- e2e: <path>\`. With no user-visible
|
|
48568
|
+
behaviour change, the literal line \`No e2e change.\` plus a
|
|
48569
|
+
one-line reason — never empty or missing
|
|
48570
|
+
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
48571
|
+
\`## Interfaces\`, each test a line
|
|
48572
|
+
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
48573
|
+
never against an internal helper. A requirement no test can
|
|
48574
|
+
cover (docs, README, deletions, config) goes under a
|
|
48575
|
+
\`### Chores\` heading in this section as
|
|
48576
|
+
\`- chore: <path> — <what> (covers <concern>)\`
|
|
48577
|
+
6. \`## Merged Concerns\` — kept, when any merge happened
|
|
48578
|
+
7. \`## Answered Questions\` — last
|
|
48579
|
+
- Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
|
|
48580
|
+
in at least one \`(covers …)\` tail, as a test or a chore. Each
|
|
48581
|
+
test entry carries its file path and level (unit / e2e)
|
|
48582
|
+
|
|
48573
48583
|
${questionBar}
|
|
48574
48584
|
## Return lap
|
|
48575
48585
|
|
|
@@ -48581,26 +48591,41 @@ const architectureDecomposePrompt = () => `${styleBlock}
|
|
|
48581
48591
|
|
|
48582
48592
|
${stateFileRules}
|
|
48583
48593
|
- The only state files this turn touches are the package files
|
|
48584
|
-
under \`.gtd/packages/\`
|
|
48585
|
-
|
|
48594
|
+
under \`.gtd/packages/\` — no other files for notes or output.
|
|
48595
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
|
|
48596
|
+
still needs it, and package files may reference it
|
|
48586
48597
|
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
48587
48598
|
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
|
-
|
|
48599
|
+
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
48600
|
+
and unit tests — this turn groups them into packages. A
|
|
48601
|
+
\`## Merged Concerns\` heading there records merges, never a
|
|
48602
|
+
concern of its own: write no package file for it
|
|
48603
|
+
- A package is a consecutive run of \`## Unit Tests\` entries that go
|
|
48604
|
+
green together on the fast suite, at most ~8 unit-test entries
|
|
48605
|
+
each. Keep the merge rule: concerns whose footprints centre on the
|
|
48606
|
+
same files stay in one package unless the later one only consumes
|
|
48607
|
+
an interface the earlier one creates. Prefer fewer, larger
|
|
48608
|
+
packages. Order them so each stays green on the fast suite —
|
|
48609
|
+
interface-introducing packages first
|
|
48610
|
+
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
48611
|
+
requirement(s) — both, independently, if merged — its tasks, and
|
|
48612
|
+
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
48613
|
+
relevant paths:
|
|
48614
|
+
- \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
|
|
48615
|
+
\`## E2E Scenarios\` as tests in the repository's own e2e
|
|
48616
|
+
framework and leaves them red. Write no such file when
|
|
48617
|
+
\`## E2E Scenarios\` says "No e2e change."
|
|
48618
|
+
- \`01-…\` onward — the unit-test packages. A requirement no test
|
|
48619
|
+
covers (docs, README, deletions, config) becomes a chore
|
|
48620
|
+
package, or a task of one; chore packages declare no tests
|
|
48621
|
+
- the last numbered package is the wiring package: CLI entry,
|
|
48622
|
+
workflow composition, config. A red e2e after it is a bug, not
|
|
48623
|
+
missing work
|
|
48624
|
+
- Every package with tests has a \`## Tests\` section, one line per
|
|
48625
|
+
declared test, copied from the architecture entries:
|
|
48626
|
+
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
48627
|
+
none
|
|
48628
|
+
- Leave everything uncommitted and finish
|
|
48604
48629
|
`;
|
|
48605
48630
|
const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
48606
48631
|
you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
|
|
@@ -48629,7 +48654,7 @@ What each change does next (then run \`gtd land\`):
|
|
|
48629
48654
|
`;
|
|
48630
48655
|
const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
48631
48656
|
- The only state file this turn may write is \`.gtd/SATISFIED.md\`;
|
|
48632
|
-
never delete the package file (the
|
|
48657
|
+
never delete the package file (the declared-tests guard reads it
|
|
48633
48658
|
after you)
|
|
48634
48659
|
- The package to implement is \`${pkg}\`
|
|
48635
48660
|
- First check its acceptance criteria against the current tree —
|
|
@@ -48638,6 +48663,13 @@ const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
|
48638
48663
|
with each criterion's concrete evidence (commit, file, or
|
|
48639
48664
|
symbol), change nothing else, and finish. Otherwise implement
|
|
48640
48665
|
normally and skip that file
|
|
48666
|
+
- Write the tests the package declares under \`## Tests\` first,
|
|
48667
|
+
then the code that makes them pass; the turn is refused unless
|
|
48668
|
+
every declared test is in your diff
|
|
48669
|
+
- Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
|
|
48670
|
+
from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
|
|
48671
|
+
step code that typechecks, and leaves them red — no
|
|
48672
|
+
implementation
|
|
48641
48673
|
- Implement every task the package describes, no more, no less,
|
|
48642
48674
|
fanning independent ones out to parallel subagents where your
|
|
48643
48675
|
harness supports it; leave other package files untouched
|
|
@@ -48654,15 +48686,6 @@ ${fixFeedbackPrompt}
|
|
|
48654
48686
|
then legitimately report itself already satisfied later
|
|
48655
48687
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
48656
48688
|
`;
|
|
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
48689
|
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
48667
48690
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
48668
48691
|
is in history. Run \`gtd judge answer\` and pipe a
|
|
@@ -48700,45 +48723,22 @@ Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
|
48700
48723
|
turn, or land untouched to give it one more attempt at the same
|
|
48701
48724
|
analysis.
|
|
48702
48725
|
`;
|
|
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
|
|
48726
|
+
const buildContextPrompt = (built) => {
|
|
48727
|
+
const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
|
|
48728
|
+
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
48729
|
+
- A red scenario that is NEW in this plan likely means missing wiring
|
|
48730
|
+
between packages; new scenarios:
|
|
48731
|
+
${list(built.scenarios.added)}
|
|
48732
|
+
- A red CHANGED or existing scenario or test is a regression a package
|
|
48733
|
+
introduced; scenarios this plan changed:
|
|
48734
|
+
${list(built.scenarios.changed)}
|
|
48735
|
+
- Each package's commits, to bisect a break:
|
|
48736
|
+
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
48735
48737
|
`;
|
|
48736
|
-
|
|
48737
|
-
|
|
48738
|
-
${agentConduct}`;
|
|
48739
|
-
const buildFixPrompt = () => `${stateFileRules}
|
|
48738
|
+
};
|
|
48739
|
+
const buildFixPrompt = (built) => `${stateFileRules}
|
|
48740
48740
|
${fixFeedbackPrompt}
|
|
48741
|
-
- Leave everything uncommitted — do not commit
|
|
48741
|
+
${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
|
|
48742
48742
|
`;
|
|
48743
48743
|
const finisherSystem = () => `${finisherPersona}
|
|
48744
48744
|
|
|
@@ -49071,6 +49071,20 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
|
49071
49071
|
Print the closing message and stop — this writes nothing itself.
|
|
49072
49072
|
`;
|
|
49073
49073
|
};
|
|
49074
|
+
const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
|
|
49075
|
+
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
49076
|
+
|
|
49077
|
+
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
49078
|
+
|
|
49079
|
+
To accept the change, land untouched.
|
|
49080
|
+
|
|
49081
|
+
To reject it, run
|
|
49082
|
+
|
|
49083
|
+
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
49084
|
+
|
|
49085
|
+
and land. Restoring only some of the files is a partial accept: every file
|
|
49086
|
+
still differing from the frozen wording is accepted.
|
|
49087
|
+
`;
|
|
49074
49088
|
//#endregion
|
|
49075
49089
|
//#region src/workflows/steps.ts
|
|
49076
49090
|
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
@@ -49079,7 +49093,6 @@ const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
|
49079
49093
|
const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
|
|
49080
49094
|
const REVIEW = ".gtd/REVIEW.md";
|
|
49081
49095
|
const QUALITY = ".gtd/QUALITY.md";
|
|
49082
|
-
const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md";
|
|
49083
49096
|
const planner = () => env.plannerModel ?? "";
|
|
49084
49097
|
const coder = () => env.coderModel ?? "";
|
|
49085
49098
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
@@ -49127,20 +49140,7 @@ const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuiteP
|
|
|
49127
49140
|
model: coder(),
|
|
49128
49141
|
system: builderSystem()
|
|
49129
49142
|
});
|
|
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(), {
|
|
49143
|
+
const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
|
|
49144
49144
|
label: "Fixing the check",
|
|
49145
49145
|
file: FEEDBACK,
|
|
49146
49146
|
model: coder(),
|
|
@@ -49248,11 +49248,29 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
|
|
|
49248
49248
|
system: reviewerSystem(),
|
|
49249
49249
|
allowEmpty: true
|
|
49250
49250
|
});
|
|
49251
|
+
//#endregion
|
|
49252
|
+
//#region src/workflows/health.ts
|
|
49253
|
+
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
49254
|
+
const SETUP = ".gtd/SETUP.md";
|
|
49255
|
+
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.";
|
|
49256
|
+
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
49257
|
+
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
49258
|
+
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
49259
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
49260
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
49261
|
+
"exit 0"
|
|
49262
|
+
] : [`rm -f ${quote(SETUP)}`];
|
|
49251
49263
|
/** 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
|
-
|
|
49264
|
+
const baseline = async (name, label = "Checking the baseline") => {
|
|
49265
|
+
for (;;) {
|
|
49266
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
49267
|
+
report: FEEDBACK,
|
|
49268
|
+
label,
|
|
49269
|
+
preamble: fastSuiteGuard()
|
|
49270
|
+
});
|
|
49271
|
+
if (read(".gtd/SETUP.md") === void 0) return green;
|
|
49272
|
+
}
|
|
49273
|
+
};
|
|
49256
49274
|
/**
|
|
49257
49275
|
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
49258
49276
|
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
@@ -49291,6 +49309,15 @@ const sameFailure = async (previous, current) => {
|
|
|
49291
49309
|
});
|
|
49292
49310
|
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
49293
49311
|
};
|
|
49312
|
+
const runSuite = (options) => {
|
|
49313
|
+
const fast = options.suite === "fast";
|
|
49314
|
+
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
49315
|
+
report: FEEDBACK,
|
|
49316
|
+
label: "Running checks",
|
|
49317
|
+
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
|
|
49318
|
+
...fast ? { preamble: fastSuiteGuard() } : {}
|
|
49319
|
+
});
|
|
49320
|
+
};
|
|
49294
49321
|
/**
|
|
49295
49322
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
49296
49323
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
@@ -49301,11 +49328,9 @@ const healthy = async (fix, options = {}) => {
|
|
|
49301
49328
|
let fixes = options.fixesSoFar ?? 0;
|
|
49302
49329
|
let previous;
|
|
49303
49330
|
for (;;) {
|
|
49304
|
-
|
|
49305
|
-
|
|
49306
|
-
|
|
49307
|
-
sweepOnGreen: [".gtd/ESCALATION.md"]
|
|
49308
|
-
})) {
|
|
49331
|
+
const green = await runSuite(options);
|
|
49332
|
+
if (options.suite === "fast" && read(".gtd/SETUP.md") !== void 0) continue;
|
|
49333
|
+
if (green) {
|
|
49309
49334
|
escalations.rounds = 0;
|
|
49310
49335
|
return;
|
|
49311
49336
|
}
|
|
@@ -49329,324 +49354,184 @@ const gate = (name, message) => scope(name, async () => {
|
|
|
49329
49354
|
});
|
|
49330
49355
|
});
|
|
49331
49356
|
//#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;
|
|
49357
|
+
//#region src/workflows/scenarios.ts
|
|
49358
|
+
const DOC_STRING = /^(\s*)("""|```)/;
|
|
49359
|
+
/**
|
|
49360
|
+
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
49361
|
+
* dropped — except inside a doc string, whose content is test input: kept
|
|
49362
|
+
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
49363
|
+
*/
|
|
49364
|
+
const scenarioText = (source) => {
|
|
49365
|
+
const out = [];
|
|
49366
|
+
let doc;
|
|
49367
|
+
for (const line of source.split("\n")) {
|
|
49368
|
+
const delimiter = DOC_STRING.exec(line);
|
|
49369
|
+
if (doc === void 0 && delimiter !== null) {
|
|
49370
|
+
doc = {
|
|
49371
|
+
indent: delimiter[1].length,
|
|
49372
|
+
fence: delimiter[2]
|
|
49373
|
+
};
|
|
49374
|
+
out.push(line.trim());
|
|
49375
|
+
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
49376
|
+
doc = void 0;
|
|
49377
|
+
out.push(line.trim());
|
|
49378
|
+
} else if (doc !== void 0) {
|
|
49379
|
+
const lead = line.length - line.trimStart().length;
|
|
49380
|
+
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
49406
49381
|
} else {
|
|
49407
|
-
|
|
49408
|
-
|
|
49382
|
+
const trimmed = line.trim();
|
|
49383
|
+
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
49409
49384
|
}
|
|
49410
49385
|
}
|
|
49411
|
-
return
|
|
49386
|
+
return out.join("\n");
|
|
49412
49387
|
};
|
|
49413
|
-
const
|
|
49414
|
-
|
|
49415
|
-
|
|
49416
|
-
regex = compileGlob(glob);
|
|
49417
|
-
globCache.set(glob, regex);
|
|
49418
|
-
}
|
|
49419
|
-
return regex.test(path);
|
|
49388
|
+
const textOf = (path) => {
|
|
49389
|
+
const source = read(path);
|
|
49390
|
+
return source === void 0 ? void 0 : scenarioText(source);
|
|
49420
49391
|
};
|
|
49421
|
-
|
|
49422
|
-
|
|
49423
|
-
|
|
49424
|
-
|
|
49425
|
-
|
|
49426
|
-
const
|
|
49427
|
-
|
|
49428
|
-
|
|
49429
|
-
|
|
49430
|
-
|
|
49431
|
-
|
|
49432
|
-
|
|
49433
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
49434
|
-
return lines;
|
|
49392
|
+
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
49393
|
+
const freezeScenarios = (paths, previous) => {
|
|
49394
|
+
if (paths.length === 0) return previous;
|
|
49395
|
+
const texts = { ...previous?.texts };
|
|
49396
|
+
for (const path of paths) {
|
|
49397
|
+
const text = textOf(path);
|
|
49398
|
+
if (text !== void 0) texts[path] = text;
|
|
49399
|
+
}
|
|
49400
|
+
return Object.keys(texts).length === 0 ? previous : {
|
|
49401
|
+
at: head(),
|
|
49402
|
+
texts
|
|
49403
|
+
};
|
|
49435
49404
|
};
|
|
49436
|
-
/**
|
|
49405
|
+
/** Paths whose wording differs from the snapshot (a deleted file counts). */
|
|
49406
|
+
const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
|
|
49407
|
+
const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
|
|
49408
|
+
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
49437
49409
|
const lcsTable = (a, b) => {
|
|
49438
|
-
const
|
|
49439
|
-
|
|
49440
|
-
|
|
49441
|
-
|
|
49442
|
-
|
|
49443
|
-
|
|
49444
|
-
|
|
49445
|
-
const
|
|
49446
|
-
const ops = [];
|
|
49410
|
+
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
49411
|
+
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]);
|
|
49412
|
+
return table;
|
|
49413
|
+
};
|
|
49414
|
+
/** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
|
|
49415
|
+
const lineDiff = (a, b) => {
|
|
49416
|
+
const table = lcsTable(a, b);
|
|
49417
|
+
const out = [];
|
|
49447
49418
|
let i = 0;
|
|
49448
49419
|
let j = 0;
|
|
49449
49420
|
while (i < a.length && j < b.length) if (a[i] === b[j]) {
|
|
49450
|
-
ops.push({
|
|
49451
|
-
type: "eq",
|
|
49452
|
-
line: a[i]
|
|
49453
|
-
});
|
|
49454
|
-
i++;
|
|
49455
|
-
j++;
|
|
49456
|
-
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
49457
|
-
ops.push({
|
|
49458
|
-
type: "del",
|
|
49459
|
-
line: a[i]
|
|
49460
|
-
});
|
|
49461
49421
|
i++;
|
|
49462
|
-
} else {
|
|
49463
|
-
ops.push({
|
|
49464
|
-
type: "add",
|
|
49465
|
-
line: b[j]
|
|
49466
|
-
});
|
|
49467
49422
|
j++;
|
|
49468
|
-
}
|
|
49469
|
-
|
|
49470
|
-
|
|
49471
|
-
|
|
49472
|
-
|
|
49473
|
-
|
|
49474
|
-
|
|
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
|
-
};
|
|
49423
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
|
|
49424
|
+
else out.push(`+ ${b[j++]}`);
|
|
49425
|
+
return [
|
|
49426
|
+
...out,
|
|
49427
|
+
...a.slice(i).map((l) => `- ${l}`),
|
|
49428
|
+
...b.slice(j).map((l) => `+ ${l}`)
|
|
49429
|
+
];
|
|
49492
49430
|
};
|
|
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
|
-
};
|
|
49431
|
+
const wordingDrift = (frozen, path) => ({
|
|
49432
|
+
path,
|
|
49433
|
+
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
49434
|
+
});
|
|
49435
|
+
/** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
|
|
49436
|
+
const holdWording = async (frozen) => {
|
|
49437
|
+
if (frozen === void 0) return;
|
|
49438
|
+
const drifted = driftedScenarios(frozen);
|
|
49439
|
+
if (drifted.length === 0) return;
|
|
49440
|
+
await human("scenario-wording", {
|
|
49441
|
+
message: scenarioWordingMessage(frozen.at, drifted.map((path) => wordingDrift(frozen, path))),
|
|
49442
|
+
label: "Scenario wording changed",
|
|
49443
|
+
acceptClean: true,
|
|
49444
|
+
base: frozen.at
|
|
49522
49445
|
});
|
|
49446
|
+
const accepted = driftedScenarios(frozen);
|
|
49447
|
+
if (accepted.length === 0) return;
|
|
49448
|
+
for (const path of accepted) {
|
|
49449
|
+
const text = textOf(path);
|
|
49450
|
+
if (text === void 0) delete frozen.texts[path];
|
|
49451
|
+
else frozen.texts[path] = text;
|
|
49452
|
+
}
|
|
49453
|
+
frozen.at = head();
|
|
49523
49454
|
};
|
|
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");
|
|
49455
|
+
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
49456
|
+
const guarded = (frozen, turn, within) => async () => {
|
|
49457
|
+
await turn();
|
|
49458
|
+
if (within === void 0) await holdWording(frozen);
|
|
49459
|
+
else await scope(within, () => holdWording(frozen));
|
|
49580
49460
|
};
|
|
49581
49461
|
//#endregion
|
|
49582
49462
|
//#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));
|
|
49463
|
+
/** Package 0: the e2e scenarios, written red. */
|
|
49464
|
+
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
49465
|
+
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
49466
|
+
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
49467
|
+
const declaredTests = (packageText) => {
|
|
49468
|
+
const found = [];
|
|
49469
|
+
let inTests = false;
|
|
49470
|
+
for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
|
|
49471
|
+
else if (inTests) {
|
|
49472
|
+
const match = TEST_LINE.exec(line);
|
|
49473
|
+
if (match) found.push({
|
|
49474
|
+
level: match[1],
|
|
49475
|
+
path: match[2]
|
|
49608
49476
|
});
|
|
49609
|
-
const capBytes = Math.floor(numeric(vars.judgeBudgetBytes, 32768) / (found.length + 1));
|
|
49610
|
-
evidence[DIFF_KEY] = packageDiff(changesSince(since), capBytes);
|
|
49611
49477
|
}
|
|
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
|
-
|
|
49478
|
+
return found;
|
|
49479
|
+
};
|
|
49480
|
+
/**
|
|
49481
|
+
* Refuse a build or fix turn unless every test `pkg` declares exists and is in the
|
|
49482
|
+
* diff since `since`. The package text is read as of `since`, so a builder
|
|
49483
|
+
* editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
|
|
49484
|
+
* written, existing in the tree suffices.
|
|
49485
|
+
*/
|
|
49486
|
+
const requireDeclaredTests = (pkg, since) => {
|
|
49487
|
+
const range = changesSince(since);
|
|
49488
|
+
const text = range.get(pkg)?.before ?? read(pkg) ?? "";
|
|
49489
|
+
const satisfied = read(".gtd/SATISFIED.md") !== void 0;
|
|
49490
|
+
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);
|
|
49491
|
+
if (missing.length === 0) return;
|
|
49492
|
+
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")}`);
|
|
49493
|
+
};
|
|
49494
|
+
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
49495
|
+
const packageItem = async (pkg, frozen) => {
|
|
49496
|
+
const from = head();
|
|
49497
|
+
const declared = (turn) => async () => {
|
|
49498
|
+
await turn();
|
|
49499
|
+
requireDeclaredTests(pkg, from);
|
|
49500
|
+
};
|
|
49501
|
+
await guarded(frozen, declared(() => build(pkg)))();
|
|
49502
|
+
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
|
|
49503
|
+
await run$3("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
|
|
49504
|
+
return {
|
|
49639
49505
|
pkg,
|
|
49640
|
-
|
|
49641
|
-
|
|
49642
|
-
|
|
49643
|
-
]), { label: "Closing out the package" });
|
|
49506
|
+
from,
|
|
49507
|
+
to: head()
|
|
49508
|
+
};
|
|
49644
49509
|
};
|
|
49645
49510
|
/** The first queued package — the one a package step works on. */
|
|
49646
49511
|
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
49512
|
+
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
49647
49513
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
49648
|
-
const packages = () => scope("packages", async () => {
|
|
49649
|
-
|
|
49514
|
+
const packages = (carried) => scope("packages", async () => {
|
|
49515
|
+
let frozen = carried;
|
|
49516
|
+
const ranges = [];
|
|
49517
|
+
let touched = [];
|
|
49518
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
49519
|
+
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
49520
|
+
ranges.push(range);
|
|
49521
|
+
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
49522
|
+
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
49523
|
+
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
49524
|
+
}
|
|
49525
|
+
}
|
|
49526
|
+
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
49527
|
+
return {
|
|
49528
|
+
ranges,
|
|
49529
|
+
scenarios: {
|
|
49530
|
+
added: paths("added"),
|
|
49531
|
+
changed: paths("modified")
|
|
49532
|
+
},
|
|
49533
|
+
frozen
|
|
49534
|
+
};
|
|
49650
49535
|
});
|
|
49651
49536
|
//#endregion
|
|
49652
49537
|
//#region src/workflows/planning.ts
|
|
@@ -49679,30 +49564,8 @@ const architecture = () => scope("architecture", async () => {
|
|
|
49679
49564
|
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
49680
49565
|
await decompose();
|
|
49681
49566
|
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
49567
|
+
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
49568
|
});
|
|
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
49569
|
//#endregion
|
|
49707
49570
|
//#region src/workflows/review.ts
|
|
49708
49571
|
/** Split a `qualityReviews` value into its lenses: on `,`, trimmed, blanks dropped. */
|
|
@@ -49717,10 +49580,10 @@ const qualityLap = async () => {
|
|
|
49717
49580
|
return (read(".gtd/QUALITY.md") ?? "").length > 0 ? "findings" : "clean";
|
|
49718
49581
|
};
|
|
49719
49582
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
49720
|
-
const fixQualityFindings = async (escalations) => {
|
|
49583
|
+
const fixQualityFindings = async (escalations, frozen) => {
|
|
49721
49584
|
for (let turns = 0; turns < 3; turns++) {
|
|
49722
|
-
await fixQuality();
|
|
49723
|
-
if (
|
|
49585
|
+
await guarded(frozen, fixQuality)();
|
|
49586
|
+
if (read(".gtd/QUALITY.md") === void 0) return true;
|
|
49724
49587
|
}
|
|
49725
49588
|
await escalation(escalations);
|
|
49726
49589
|
return false;
|
|
@@ -49822,8 +49685,8 @@ const routeNotes = async (notes, r) => {
|
|
|
49822
49685
|
answeredAt = head();
|
|
49823
49686
|
}
|
|
49824
49687
|
if (nits.length > 0) {
|
|
49825
|
-
await fixNits(nits);
|
|
49826
|
-
await healthy(
|
|
49688
|
+
await guarded(r.frozen, () => fixNits(nits), "review")();
|
|
49689
|
+
await healthy(guarded(r.frozen, fixCheck, "review"), { escalations: r.escalations });
|
|
49827
49690
|
}
|
|
49828
49691
|
if (edits.length > 0) {
|
|
49829
49692
|
await r.close();
|
|
@@ -49841,7 +49704,7 @@ const routeNotes = async (notes, r) => {
|
|
|
49841
49704
|
reviewed: answeredAt
|
|
49842
49705
|
};
|
|
49843
49706
|
};
|
|
49844
|
-
const finish = async (reviewed, collectedAt, escalations) => {
|
|
49707
|
+
const finish = async (reviewed, collectedAt, escalations, frozen) => {
|
|
49845
49708
|
const round = head();
|
|
49846
49709
|
const since = changesSince(reviewed);
|
|
49847
49710
|
const edited = since.filter(isCodeEdit);
|
|
@@ -49874,18 +49737,19 @@ const finish = async (reviewed, collectedAt, escalations) => {
|
|
|
49874
49737
|
return routeNotes(notes, {
|
|
49875
49738
|
round,
|
|
49876
49739
|
escalations,
|
|
49740
|
+
frozen,
|
|
49877
49741
|
close,
|
|
49878
49742
|
outcome,
|
|
49879
49743
|
unfolded
|
|
49880
49744
|
});
|
|
49881
49745
|
};
|
|
49882
49746
|
/** 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) => {
|
|
49747
|
+
const reviewOnce = async (base, carry, escalations, frozen) => {
|
|
49884
49748
|
await reviewing(base, carry);
|
|
49885
49749
|
const risks = reviewRisks(read(".gtd/REVIEW.md") ?? "");
|
|
49886
49750
|
if (risks.length === 0) return;
|
|
49887
|
-
await fixRisks(risks);
|
|
49888
|
-
await healthy(
|
|
49751
|
+
await guarded(frozen, () => fixRisks(risks), "review")();
|
|
49752
|
+
await healthy(guarded(frozen, fixCheck, "review"), { escalations });
|
|
49889
49753
|
await reviewing(base, carry);
|
|
49890
49754
|
};
|
|
49891
49755
|
/**
|
|
@@ -49894,17 +49758,17 @@ const reviewOnce = async (base, carry, escalations) => {
|
|
|
49894
49758
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
49895
49759
|
* batch, praise dropped.
|
|
49896
49760
|
*/
|
|
49897
|
-
const review = async (base, escalations = { rounds: 0 }) => {
|
|
49761
|
+
const review$1 = async (base, escalations = { rounds: 0 }, frozen) => {
|
|
49898
49762
|
let carry;
|
|
49899
49763
|
for (;;) {
|
|
49900
|
-
await reviewOnce(base, carry, escalations);
|
|
49764
|
+
await reviewOnce(base, carry, escalations, frozen);
|
|
49901
49765
|
carry = void 0;
|
|
49902
49766
|
let reviewed = head();
|
|
49903
49767
|
let collectedAt;
|
|
49904
49768
|
for (;;) {
|
|
49905
49769
|
collectedAt = await converse(base, collectedAt);
|
|
49906
49770
|
if (collectedAt === "missing") break;
|
|
49907
|
-
const result = await finish(reviewed, collectedAt, escalations);
|
|
49771
|
+
const result = await finish(reviewed, collectedAt, escalations, frozen);
|
|
49908
49772
|
if (!("next" in result)) return result;
|
|
49909
49773
|
if (result.next === "rereview") {
|
|
49910
49774
|
carry = result.carry;
|
|
@@ -49915,14 +49779,20 @@ const review = async (base, escalations = { rounds: 0 }) => {
|
|
|
49915
49779
|
}
|
|
49916
49780
|
};
|
|
49917
49781
|
/** 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 () => {
|
|
49782
|
+
const buildTail = (fixFirst, base, built) => scope("build", async () => {
|
|
49783
|
+
const frozen = built?.frozen;
|
|
49919
49784
|
const escalations = { rounds: 0 };
|
|
49785
|
+
const guardedFix = guarded(frozen, () => fixCheck());
|
|
49920
49786
|
let redFirst = fixFirst;
|
|
49787
|
+
if (!fixFirst) await healthy(guarded(frozen, () => fixCheck(built)), {
|
|
49788
|
+
escalations,
|
|
49789
|
+
sweepOnGreen: [ARCHITECTURE]
|
|
49790
|
+
});
|
|
49921
49791
|
let lapped = false;
|
|
49922
49792
|
for (;;) {
|
|
49923
49793
|
if (redFirst) {
|
|
49924
|
-
await
|
|
49925
|
-
await healthy(
|
|
49794
|
+
await guardedFix();
|
|
49795
|
+
await healthy(guardedFix, {
|
|
49926
49796
|
fixesSoFar: 1,
|
|
49927
49797
|
escalations
|
|
49928
49798
|
});
|
|
@@ -49930,8 +49800,8 @@ const buildTail = (fixFirst, base) => scope("build", async () => {
|
|
|
49930
49800
|
}
|
|
49931
49801
|
const lap = lapped ? "clean" : await qualityLap();
|
|
49932
49802
|
lapped = true;
|
|
49933
|
-
if (lap === "clean") return review(base, escalations);
|
|
49934
|
-
if (await fixQualityFindings(escalations)) await healthy(
|
|
49803
|
+
if (lap === "clean") return review$1(base, escalations, frozen);
|
|
49804
|
+
if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
|
|
49935
49805
|
else redFirst = true;
|
|
49936
49806
|
}
|
|
49937
49807
|
});
|
|
@@ -49942,14 +49812,13 @@ const defaults = {
|
|
|
49942
49812
|
reviewBase: "",
|
|
49943
49813
|
judgeBudgetBytes: "32768",
|
|
49944
49814
|
judgeIdenticalMinP: "0.7",
|
|
49945
|
-
specPreJudge: "0.9",
|
|
49946
49815
|
reviewNoteActionable: "0.7",
|
|
49947
|
-
architectureSkipMinP: "0.85",
|
|
49948
49816
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
49949
49817
|
};
|
|
49950
49818
|
/** Environment settings. */
|
|
49951
49819
|
const envDefaults = {
|
|
49952
49820
|
testCommand: "npm test",
|
|
49821
|
+
fastTestCommand: "",
|
|
49953
49822
|
plannerModel: "smart",
|
|
49954
49823
|
coderModel: "base"
|
|
49955
49824
|
};
|
|
@@ -49965,8 +49834,6 @@ const skills = (vars) => ({
|
|
|
49965
49834
|
"architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
|
|
49966
49835
|
"packages.item": ["test-driven-development", "incremental-implementation"],
|
|
49967
49836
|
"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
49837
|
"packages.item.health": ["debugging-and-error-recovery"],
|
|
49971
49838
|
build: ["debugging-and-error-recovery"],
|
|
49972
49839
|
"build.health": ["debugging-and-error-recovery"],
|
|
@@ -49982,15 +49849,14 @@ const access = (vars) => ({
|
|
|
49982
49849
|
design: { write: [] },
|
|
49983
49850
|
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
49984
49851
|
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
49985
|
-
"packages.item.spec": { write: [".gtd/SPEC_FEEDBACK.md"] },
|
|
49986
49852
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
49987
49853
|
"build.review.fix.nits": {},
|
|
49988
49854
|
"build.review.fix.risks": {},
|
|
49989
49855
|
...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, { write: [] }]))
|
|
49990
49856
|
});
|
|
49991
49857
|
//#endregion
|
|
49992
|
-
//#region src/workflows/
|
|
49993
|
-
var
|
|
49858
|
+
//#region src/workflows/bundled.ts
|
|
49859
|
+
var bundled_exports = /* @__PURE__ */ __exportAll({
|
|
49994
49860
|
ARCHITECTURE: () => ARCHITECTURE,
|
|
49995
49861
|
ESCALATION: () => ESCALATION,
|
|
49996
49862
|
FEEDBACK: () => FEEDBACK,
|
|
@@ -49998,7 +49864,8 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
49998
49864
|
QUALITY: () => QUALITY,
|
|
49999
49865
|
REQUIREMENTS: () => REQUIREMENTS,
|
|
50000
49866
|
REVIEW: () => REVIEW,
|
|
50001
|
-
|
|
49867
|
+
SCENARIO_PACKAGE: () => SCENARIO_PACKAGE,
|
|
49868
|
+
SETUP: () => SETUP,
|
|
50002
49869
|
access: () => access,
|
|
50003
49870
|
afterTail: () => afterTail,
|
|
50004
49871
|
agentWithSkills: () => agentWithSkills,
|
|
@@ -50006,7 +49873,6 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
50006
49873
|
answerReviewQuestions: () => answerReviewQuestions,
|
|
50007
49874
|
answerTechnicalQuestions: () => answerTechnicalQuestions,
|
|
50008
49875
|
architecture: () => architecture,
|
|
50009
|
-
architecturePass: () => architecturePass,
|
|
50010
49876
|
author: () => author,
|
|
50011
49877
|
awaitReview: () => awaitReview,
|
|
50012
49878
|
base: () => base,
|
|
@@ -50015,24 +49881,32 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
50015
49881
|
buildTail: () => buildTail,
|
|
50016
49882
|
builtInLenses: () => builtInLenses,
|
|
50017
49883
|
collecting: () => collecting,
|
|
49884
|
+
declaredTests: () => declaredTests,
|
|
50018
49885
|
decompose: () => decompose,
|
|
50019
|
-
default: () =>
|
|
49886
|
+
default: () => feature,
|
|
50020
49887
|
defaults: () => defaults,
|
|
50021
49888
|
describeEscalation: () => describeEscalation,
|
|
50022
49889
|
design: () => design,
|
|
49890
|
+
doors: () => doors,
|
|
49891
|
+
driftedScenarios: () => driftedScenarios,
|
|
50023
49892
|
envDefaults: () => envDefaults,
|
|
50024
49893
|
escalate: () => escalate,
|
|
50025
49894
|
escalation: () => escalation,
|
|
50026
49895
|
escalationExhausted: () => escalationExhausted,
|
|
49896
|
+
fastSuiteGuard: () => fastSuiteGuard,
|
|
49897
|
+
feature: () => feature,
|
|
50027
49898
|
fix: () => fix,
|
|
49899
|
+
fixCheck: () => fixCheck,
|
|
50028
49900
|
fixNits: () => fixNits,
|
|
50029
49901
|
fixQuality: () => fixQuality,
|
|
50030
49902
|
fixQualityFindings: () => fixQualityFindings,
|
|
50031
49903
|
fixRisks: () => fixRisks,
|
|
50032
|
-
fixSpec: () => fixSpec,
|
|
50033
49904
|
fixSuite: () => fixSuite,
|
|
49905
|
+
freezeScenarios: () => freezeScenarios,
|
|
50034
49906
|
gate: () => gate,
|
|
49907
|
+
guarded: () => guarded,
|
|
50035
49908
|
healthy: () => healthy,
|
|
49909
|
+
holdWording: () => holdWording,
|
|
50036
49910
|
lensesOf: () => lensesOf,
|
|
50037
49911
|
nextPackage: () => nextPackage,
|
|
50038
49912
|
ordinaryStart: () => ordinaryStart,
|
|
@@ -50041,19 +49915,20 @@ var unified_exports = /* @__PURE__ */ __exportAll({
|
|
|
50041
49915
|
planAndBuild: () => planAndBuild,
|
|
50042
49916
|
qualityLap: () => qualityLap,
|
|
50043
49917
|
reUnwind: () => reUnwind,
|
|
49918
|
+
requireDeclaredTests: () => requireDeclaredTests,
|
|
50044
49919
|
retryQuestion: () => retryQuestion,
|
|
50045
49920
|
review: () => review,
|
|
50046
49921
|
reviewMissing: () => reviewMissing,
|
|
50047
|
-
reviewPackage: () => reviewPackage,
|
|
50048
49922
|
reviewQuality: () => reviewQuality,
|
|
50049
49923
|
reviewing: () => reviewing,
|
|
49924
|
+
scenarioText: () => scenarioText,
|
|
50050
49925
|
skills: () => skills,
|
|
50051
|
-
specReview: () => specReview,
|
|
50052
49926
|
steering: () => steering,
|
|
50053
49927
|
summary: () => summary,
|
|
50054
49928
|
threads: () => threads,
|
|
50055
49929
|
triage: () => triage$1,
|
|
50056
|
-
unwind: () => unwind
|
|
49930
|
+
unwind: () => unwind,
|
|
49931
|
+
wordingDrift: () => wordingDrift
|
|
50057
49932
|
});
|
|
50058
49933
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
50059
49934
|
const unwind = () => {
|
|
@@ -50079,11 +49954,13 @@ const reUnwind = async (feedback) => {
|
|
|
50079
49954
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
50080
49955
|
const planAndBuild = async (firstBase) => {
|
|
50081
49956
|
let base = firstBase;
|
|
49957
|
+
let carried;
|
|
50082
49958
|
for (;;) {
|
|
50083
49959
|
await design(base);
|
|
50084
|
-
await
|
|
50085
|
-
await packages();
|
|
50086
|
-
|
|
49960
|
+
await architecture();
|
|
49961
|
+
const built = await packages(carried);
|
|
49962
|
+
carried = built.frozen;
|
|
49963
|
+
const outcome = await buildTail(false, base, built);
|
|
50087
49964
|
if (outcome.verdict === "signoff") return;
|
|
50088
49965
|
await reUnwind(outcome);
|
|
50089
49966
|
base = outcome.base;
|
|
@@ -50111,34 +49988,35 @@ const ordinaryStart = async () => {
|
|
|
50111
49988
|
await gate("start-gate", startGateBlockedMessage());
|
|
50112
49989
|
await planAndBuild(start());
|
|
50113
49990
|
};
|
|
50114
|
-
|
|
50115
|
-
|
|
50116
|
-
"
|
|
50117
|
-
|
|
50118
|
-
|
|
50119
|
-
|
|
50120
|
-
|
|
50121
|
-
|
|
50122
|
-
|
|
50123
|
-
|
|
50124
|
-
|
|
50125
|
-
if (entry === "review-gate.check") {
|
|
50126
|
-
await gate("review-gate", reviewGateBlockedMessage());
|
|
50127
|
-
return afterTail(await buildTail(false, start()));
|
|
50128
|
-
}
|
|
50129
|
-
if (entry === "start-gate.check") {
|
|
50130
|
-
await gate("start-gate", startGateBlockedMessage());
|
|
50131
|
-
return planAndBuild(start());
|
|
50132
|
-
}
|
|
50133
|
-
refuse(`"${entry}" is not an enterable state — enterable states:\n${ENTRIES.map((name) => ` ${name}`).join("\n")}`);
|
|
50134
|
-
}
|
|
49991
|
+
/** Repair a red baseline through the build tail, as its own reviewed commit. */
|
|
49992
|
+
const fix = async () => {
|
|
49993
|
+
if (await baseline("fix-precheck")) return;
|
|
49994
|
+
return afterTail(await buildTail(true, start()));
|
|
49995
|
+
};
|
|
49996
|
+
/** Pure review of everything since `reviewBase`. */
|
|
49997
|
+
const review = async () => {
|
|
49998
|
+
await gate("review-gate", reviewGateBlockedMessage());
|
|
49999
|
+
return afterTail(await buildTail(false, start()));
|
|
50000
|
+
};
|
|
50001
|
+
const feature = ordinaryStart;
|
|
50135
50002
|
const summary = summaryPrompt;
|
|
50136
50003
|
const steering = {
|
|
50137
50004
|
[REQUIREMENTS]: "qa",
|
|
50138
50005
|
[ARCHITECTURE]: "qa",
|
|
50139
50006
|
[REVIEW]: "review"
|
|
50140
50007
|
};
|
|
50141
|
-
const base = (
|
|
50008
|
+
const base = (workflow, vars) => workflow === "review" ? vars.reviewBase ?? "" : void 0;
|
|
50009
|
+
const doors = {
|
|
50010
|
+
fix: { workflow: "fix" },
|
|
50011
|
+
review: {
|
|
50012
|
+
workflow: "review",
|
|
50013
|
+
args: [{
|
|
50014
|
+
name: "base",
|
|
50015
|
+
optional: true
|
|
50016
|
+
}],
|
|
50017
|
+
vars: ({ base }) => base === void 0 ? {} : { reviewBase: base }
|
|
50018
|
+
}
|
|
50019
|
+
};
|
|
50142
50020
|
//#endregion
|
|
50143
50021
|
//#region src/platform/Host.ts
|
|
50144
50022
|
const toError$1 = (e) => e instanceof Error ? e : new Error(String(e));
|
|
@@ -50331,6 +50209,8 @@ const makeGitImpl = (executor, root) => {
|
|
|
50331
50209
|
resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
|
|
50332
50210
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
50333
50211
|
isAncestor: (a, b) => run$2(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
|
|
50212
|
+
mergeBase: (a, b) => exec("git", "merge-base", a, b).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
50213
|
+
defaultBranch: () => exec("git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD").pipe(map$4((s) => s.trim()), catchAll(() => succeed$3("")), map$4((name) => name === "" ? "main" : name)),
|
|
50334
50214
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
50335
50215
|
gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
|
|
50336
50216
|
gitCommonDir: () => exec("git", "rev-parse", "--path-format=absolute", "--git-common-dir").pipe(map$4((s) => s.trim())),
|
|
@@ -58237,45 +58117,71 @@ const load = gen(function* () {
|
|
|
58237
58117
|
const { levels, compiled, decodeDiagnostics } = yield* readRc;
|
|
58238
58118
|
const module = yield* discovery.workflowModule(host.root, host.home);
|
|
58239
58119
|
const loaded = yield* try_({
|
|
58240
|
-
try: () =>
|
|
58120
|
+
try: () => loadWorkflows(module),
|
|
58241
58121
|
catch: (e) => new GtdError(`gtd config:\n - ${module?.filepath ?? "(built-in default)"}: ${e instanceof Error ? e.message : String(e)}`)
|
|
58242
58122
|
});
|
|
58243
58123
|
const layerOrder = [...levels.map((level) => level.filepath), ...module ? [module.filepath] : []];
|
|
58244
58124
|
const diagnostics = dedupeDiagnostics(sortDiagnostics([
|
|
58245
58125
|
...decodeDiagnostics,
|
|
58246
58126
|
...compiled.diagnostics,
|
|
58247
|
-
...wrongKindDiagnostics(loaded, compiled)
|
|
58127
|
+
...wrongKindDiagnostics(loaded.files, compiled)
|
|
58248
58128
|
], layerOrder));
|
|
58249
58129
|
yield* failOnErrors(diagnostics);
|
|
58250
|
-
const initial = yield* firstStep(loaded, headTree(yield* Workspace));
|
|
58130
|
+
const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
|
|
58131
|
+
const steering = Object.assign({}, ...loaded.files.map((f) => f.steering));
|
|
58132
|
+
const scopesOf = (file) => {
|
|
58133
|
+
const vars = resolveVars(file.defaults, compiled.rcVars, {}, host.env);
|
|
58134
|
+
try {
|
|
58135
|
+
return [...Object.keys(file.skills(vars)), ...Object.keys(file.access(vars))];
|
|
58136
|
+
} catch {
|
|
58137
|
+
return [];
|
|
58138
|
+
}
|
|
58139
|
+
};
|
|
58140
|
+
const define = (entry) => ({
|
|
58141
|
+
flow: entry.flow,
|
|
58142
|
+
summary: entry.file.summary,
|
|
58143
|
+
base: entry.file.base,
|
|
58144
|
+
steering,
|
|
58145
|
+
modes: compiled.modes,
|
|
58146
|
+
skills: entry.file.skills,
|
|
58147
|
+
configuredSkills: compiled.rcSkills,
|
|
58148
|
+
skillsKeys: compiled.skillsKeys,
|
|
58149
|
+
access: entry.file.access,
|
|
58150
|
+
configuredAccess: compiled.access,
|
|
58151
|
+
accessKeys: compiled.accessKeys,
|
|
58152
|
+
knownScopes: () => loaded.files.filter((f) => f !== entry.file).flatMap(scopesOf),
|
|
58153
|
+
skillsOrigin: entry.file.origin,
|
|
58154
|
+
initial
|
|
58155
|
+
});
|
|
58156
|
+
const names = [...loaded.catalogue.keys()].sort();
|
|
58157
|
+
const resolve = (name, entry) => ({
|
|
58158
|
+
name,
|
|
58159
|
+
def: define(entry),
|
|
58160
|
+
vars: { ...entry.file.defaults },
|
|
58161
|
+
env: { ...entry.file.envDefaults }
|
|
58162
|
+
});
|
|
58163
|
+
const defaultWorkflow = resolve(loaded.default.name, loaded.default);
|
|
58251
58164
|
return {
|
|
58252
|
-
workflow:
|
|
58253
|
-
|
|
58254
|
-
|
|
58255
|
-
|
|
58256
|
-
|
|
58257
|
-
|
|
58258
|
-
|
|
58259
|
-
configuredSkills: compiled.rcSkills,
|
|
58260
|
-
skillsKeys: compiled.skillsKeys,
|
|
58261
|
-
access: loaded.access,
|
|
58262
|
-
configuredAccess: compiled.access,
|
|
58263
|
-
accessKeys: compiled.accessKeys,
|
|
58264
|
-
skillsOrigin: module?.filepath ?? "(built-in default)",
|
|
58265
|
-
initial
|
|
58165
|
+
workflow: defaultWorkflow.def,
|
|
58166
|
+
defaultWorkflow,
|
|
58167
|
+
workflowNames: names,
|
|
58168
|
+
doors: loaded.doors,
|
|
58169
|
+
workflowNamed: (name) => {
|
|
58170
|
+
const entry = loaded.catalogue.get(name);
|
|
58171
|
+
return entry === void 0 ? void 0 : resolve(name, entry);
|
|
58266
58172
|
},
|
|
58267
|
-
workflowVars: { ...loaded.defaults },
|
|
58268
58173
|
rcVars: compiled.rcVars,
|
|
58269
|
-
workflowEnv: { ...loaded.envDefaults },
|
|
58270
58174
|
rcEnv: compiled.rcEnv,
|
|
58271
58175
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
58272
58176
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
58273
58177
|
configFiles: levels.map((level) => level.filepath),
|
|
58274
|
-
workflowFiles: loaded.dependencyFiles
|
|
58178
|
+
workflowFiles: loaded.files.flatMap((f) => f.dependencyFiles)
|
|
58275
58179
|
};
|
|
58276
58180
|
});
|
|
58277
58181
|
/** A name under the wrong key (or under both) in a `.gtdrc` layer: each is an error at its own origin. */
|
|
58278
|
-
const wrongKindDiagnostics = (
|
|
58182
|
+
const wrongKindDiagnostics = (files, compiled) => {
|
|
58183
|
+
const isEnvOnly = (key) => files.some((f) => Object.hasOwn(f.envDefaults, key)) && !files.some((f) => Object.hasOwn(f.defaults, key));
|
|
58184
|
+
const isVarOnly = (key) => files.some((f) => Object.hasOwn(f.defaults, key)) && !files.some((f) => Object.hasOwn(f.envDefaults, key));
|
|
58279
58185
|
const error = (path, message, origin) => ({
|
|
58280
58186
|
severity: "error",
|
|
58281
58187
|
path,
|
|
@@ -58283,9 +58189,9 @@ const wrongKindDiagnostics = (loaded, compiled) => {
|
|
|
58283
58189
|
origin
|
|
58284
58190
|
});
|
|
58285
58191
|
const diagnostics = [];
|
|
58286
|
-
for (const { key, origin } of compiled.varsKeys) if (
|
|
58192
|
+
for (const { key, origin } of compiled.varsKeys) if (isEnvOnly(key)) diagnostics.push(error(["vars", key], `"vars.${key}" is an environment setting — move it under "env:"`, origin));
|
|
58287
58193
|
for (const { key, origin } of compiled.envKeys) {
|
|
58288
|
-
if (
|
|
58194
|
+
if (isVarOnly(key)) diagnostics.push(error(["env", key], `"env.${key}" is a process setting — move it under "vars:"`, origin));
|
|
58289
58195
|
if (compiled.varsKeys.some((v) => v.key === key)) diagnostics.push(error(["env", key], `"${key}" is declared under both "vars:" and "env:" — a setting is one kind or the other`, origin));
|
|
58290
58196
|
}
|
|
58291
58197
|
return diagnostics;
|
|
@@ -58313,7 +58219,7 @@ const jiti = () => {
|
|
|
58313
58219
|
return jitiModule.createJiti(import.meta.url, {
|
|
58314
58220
|
virtualModules: {
|
|
58315
58221
|
"@pmelab/gtd/flows": flows_exports,
|
|
58316
|
-
"@pmelab/gtd/workflow":
|
|
58222
|
+
"@pmelab/gtd/workflow": bundled_exports
|
|
58317
58223
|
},
|
|
58318
58224
|
fsCache: false,
|
|
58319
58225
|
moduleCache: false,
|
|
@@ -58350,14 +58256,19 @@ const optionalFunction = (exports, name) => {
|
|
|
58350
58256
|
if (typeof value !== "function") throw new Error(`the "${name}" export is not a function`);
|
|
58351
58257
|
return value;
|
|
58352
58258
|
};
|
|
58353
|
-
|
|
58354
|
-
|
|
58355
|
-
|
|
58356
|
-
|
|
58357
|
-
|
|
58358
|
-
|
|
58359
|
-
|
|
58360
|
-
|
|
58259
|
+
const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
|
|
58260
|
+
"default",
|
|
58261
|
+
"summary",
|
|
58262
|
+
"base",
|
|
58263
|
+
"defaults",
|
|
58264
|
+
"envDefaults",
|
|
58265
|
+
"steering",
|
|
58266
|
+
"skills",
|
|
58267
|
+
"access",
|
|
58268
|
+
"doors"
|
|
58269
|
+
]);
|
|
58270
|
+
/** A module's per-file exports: `defaults`, `envDefaults`, `steering`, `skills`, `access`, `summary`, `base`. */
|
|
58271
|
+
const fileFrom = (exports, origin, dependencyFiles) => {
|
|
58361
58272
|
const defaults = stringRecord(exports, "defaults");
|
|
58362
58273
|
const envDefaults = stringRecord(exports, "envDefaults");
|
|
58363
58274
|
for (const [exportName, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) {
|
|
@@ -58367,7 +58278,6 @@ const fromModule = (exported, origin, dependencyFiles) => {
|
|
|
58367
58278
|
const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
|
|
58368
58279
|
if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
|
|
58369
58280
|
return {
|
|
58370
|
-
flow,
|
|
58371
58281
|
defaults,
|
|
58372
58282
|
envDefaults,
|
|
58373
58283
|
steering: stringRecord(exports, "steering"),
|
|
@@ -58379,23 +58289,120 @@ const fromModule = (exported, origin, dependencyFiles) => {
|
|
|
58379
58289
|
dependencyFiles
|
|
58380
58290
|
};
|
|
58381
58291
|
};
|
|
58292
|
+
const DOOR_NAME_RULE = /^[a-z][a-z0-9-]*$/;
|
|
58293
|
+
/** Why one door's `args` are malformed, or `undefined` when they are well-formed. */
|
|
58294
|
+
const argsProblem = (args) => {
|
|
58295
|
+
if (args === void 0) return void 0;
|
|
58296
|
+
if (!Array.isArray(args)) return `has "args" that is not an array`;
|
|
58297
|
+
const seen = /* @__PURE__ */ new Set();
|
|
58298
|
+
let optional = false;
|
|
58299
|
+
for (const arg of args) {
|
|
58300
|
+
if (typeof arg?.name !== "string") return `has an arg without a "name"`;
|
|
58301
|
+
if (seen.has(arg.name)) return `declares duplicate arg "${arg.name}"`;
|
|
58302
|
+
seen.add(arg.name);
|
|
58303
|
+
if (arg.optional === true) optional = true;
|
|
58304
|
+
else if (optional) return `declares required arg "${arg.name}" after an optional one`;
|
|
58305
|
+
}
|
|
58306
|
+
};
|
|
58307
|
+
/** Why one door is malformed, or `undefined` when it is well-formed. */
|
|
58308
|
+
const doorProblem = (name, door) => {
|
|
58309
|
+
if (!DOOR_NAME_RULE.test(name)) return `is not a valid door name — it must match ${DOOR_NAME_RULE}`;
|
|
58310
|
+
const { workflow, args, vars } = door ?? {};
|
|
58311
|
+
if (typeof workflow !== "string") return `must be an object with a "workflow" name`;
|
|
58312
|
+
const varsOk = vars === void 0 || typeof vars === "function";
|
|
58313
|
+
return argsProblem(args) ?? (varsOk ? void 0 : `has "vars" that is not a function`);
|
|
58314
|
+
};
|
|
58315
|
+
/** A module's `doors` export, shape-checked; the workflow names are checked later, against the merged catalogue. */
|
|
58316
|
+
const doorsFrom = (exports) => {
|
|
58317
|
+
const value = exports["doors"];
|
|
58318
|
+
if (value === void 0) return /* @__PURE__ */ new Map();
|
|
58319
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`the "doors" export is not a record of doors`);
|
|
58320
|
+
for (const [name, door] of Object.entries(value)) {
|
|
58321
|
+
const problem = doorProblem(name, door);
|
|
58322
|
+
if (problem !== void 0) throw new Error(`door "${name}" ${problem}`);
|
|
58323
|
+
}
|
|
58324
|
+
return new Map(Object.entries(value));
|
|
58325
|
+
};
|
|
58326
|
+
/** Merge bundled and repo doors (repo wins), then check each names a workflow the merged catalogue holds. */
|
|
58327
|
+
const mergeDoors = (bundled, repo, catalogue) => {
|
|
58328
|
+
const doors = new Map([...bundled, ...repo].sort(([a], [b]) => a.localeCompare(b)));
|
|
58329
|
+
for (const [name, door] of doors) if (!catalogue.has(door.workflow)) throw new Error(`door "${name}" names workflow "${door.workflow}", which is not startable — known: ${[...catalogue.keys()].sort().join(", ")}`);
|
|
58330
|
+
return doors;
|
|
58331
|
+
};
|
|
58332
|
+
const namespaceOf = (exported) => typeof exported === "object" && exported !== null ? exported : {};
|
|
58333
|
+
/**
|
|
58334
|
+
* The bundled module is never scanned: its helper exports stay helpers, and
|
|
58335
|
+
* exactly these three are startable.
|
|
58336
|
+
*/
|
|
58337
|
+
const loadBundled = () => {
|
|
58338
|
+
const file = fileFrom(namespaceOf(bundled_exports), BUILT_IN_ORIGIN, []);
|
|
58339
|
+
const { feature, review, fix } = bundled_exports;
|
|
58340
|
+
return {
|
|
58341
|
+
file,
|
|
58342
|
+
doors: doorsFrom(namespaceOf(bundled_exports)),
|
|
58343
|
+
catalogue: /* @__PURE__ */ new Map([
|
|
58344
|
+
["feature", {
|
|
58345
|
+
flow: feature,
|
|
58346
|
+
file
|
|
58347
|
+
}],
|
|
58348
|
+
["review", {
|
|
58349
|
+
flow: review,
|
|
58350
|
+
file
|
|
58351
|
+
}],
|
|
58352
|
+
["fix", {
|
|
58353
|
+
flow: fix,
|
|
58354
|
+
file
|
|
58355
|
+
}]
|
|
58356
|
+
])
|
|
58357
|
+
};
|
|
58358
|
+
};
|
|
58382
58359
|
/**
|
|
58383
|
-
* Evaluate `gtd.config.ts
|
|
58384
|
-
*
|
|
58385
|
-
*
|
|
58386
|
-
*
|
|
58387
|
-
* collecting every real file jiti resolved evaluating this
|
|
58388
|
-
* AND every relative import it pulled in, transitively.
|
|
58389
|
-
* (`@pmelab/gtd/flows`) never touch it — no real file backs them.
|
|
58360
|
+
* Evaluate `gtd.config.ts` and overlay it on the bundled workflows. Every
|
|
58361
|
+
* exported function of the repo file but the reserved names is a workflow
|
|
58362
|
+
* named by its export. `cache` is jiti's OWN per-call module cache (unrelated
|
|
58363
|
+
* to the instance's `moduleCache: false`) — passing an empty object in has
|
|
58364
|
+
* the side effect of collecting every real file jiti resolved evaluating this
|
|
58365
|
+
* module, module AND every relative import it pulled in, transitively.
|
|
58390
58366
|
*/
|
|
58391
|
-
const
|
|
58392
|
-
|
|
58367
|
+
const loadWorkflows = (module) => {
|
|
58368
|
+
const bundled = loadBundled();
|
|
58369
|
+
const bundledDefault = {
|
|
58370
|
+
flow: feature,
|
|
58371
|
+
file: bundled.file,
|
|
58372
|
+
name: "feature"
|
|
58373
|
+
};
|
|
58374
|
+
if (module === void 0) return {
|
|
58375
|
+
files: [bundled.file],
|
|
58376
|
+
catalogue: bundled.catalogue,
|
|
58377
|
+
doors: mergeDoors(bundled.doors, /* @__PURE__ */ new Map(), bundled.catalogue),
|
|
58378
|
+
default: bundledDefault
|
|
58379
|
+
};
|
|
58393
58380
|
const cache = {};
|
|
58394
|
-
const
|
|
58381
|
+
const exports = namespaceOf(jiti().evalModule(module.source, {
|
|
58395
58382
|
filename: module.filepath,
|
|
58396
58383
|
cache
|
|
58397
|
-
});
|
|
58398
|
-
|
|
58384
|
+
}));
|
|
58385
|
+
const file = fileFrom(exports, module.filepath, Object.keys(cache));
|
|
58386
|
+
const catalogue = new Map(bundled.catalogue);
|
|
58387
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
58388
|
+
if (RESERVED_EXPORTS.has(name) || typeof value !== "function") continue;
|
|
58389
|
+
catalogue.set(name, {
|
|
58390
|
+
flow: value,
|
|
58391
|
+
file
|
|
58392
|
+
});
|
|
58393
|
+
}
|
|
58394
|
+
const flow = exports.default;
|
|
58395
|
+
if (flow !== void 0 && typeof flow !== "function") throw new Error("the default export is not a function");
|
|
58396
|
+
return {
|
|
58397
|
+
files: [bundled.file, file],
|
|
58398
|
+
catalogue,
|
|
58399
|
+
doors: mergeDoors(bundled.doors, doorsFrom(exports), catalogue),
|
|
58400
|
+
default: flow === void 0 ? bundledDefault : {
|
|
58401
|
+
flow,
|
|
58402
|
+
file,
|
|
58403
|
+
name: "default"
|
|
58404
|
+
}
|
|
58405
|
+
};
|
|
58399
58406
|
};
|
|
58400
58407
|
const headTree = (workspace) => {
|
|
58401
58408
|
let entries;
|
|
@@ -58451,7 +58458,6 @@ const settingReads = (kind, watch) => {
|
|
|
58451
58458
|
const replayToFirstStep = (loaded, vars, env, tree) => flatMap$4(promise(() => replay({
|
|
58452
58459
|
flow: loaded.flow,
|
|
58453
58460
|
episode: {
|
|
58454
|
-
entry: void 0,
|
|
58455
58461
|
base: {
|
|
58456
58462
|
hash: "",
|
|
58457
58463
|
tree
|
|
@@ -58465,7 +58471,7 @@ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$4(promise(() => r
|
|
|
58465
58471
|
})), (outcome) => {
|
|
58466
58472
|
if (outcome.kind === "rest") return succeed$3(outcome.rest.name);
|
|
58467
58473
|
const problem = outcome.kind === "failed" || outcome.kind === "refused" ? outcome.message.replace(/^gtd: /, "") : "the default entry must begin at a step — that step is where a finished process waits";
|
|
58468
|
-
return fail$7(new GtdError(`gtd config:\n - ${loaded.origin}: ${problem}`));
|
|
58474
|
+
return fail$7(new GtdError(`gtd config:\n - ${loaded.file.origin}: ${problem}`));
|
|
58469
58475
|
});
|
|
58470
58476
|
/**
|
|
58471
58477
|
* The default entry's first step — where a finished process waits — found the
|
|
@@ -58481,13 +58487,13 @@ const firstStep = (loaded, head) => gen(function* () {
|
|
|
58481
58487
|
const env = watchedEmptySettings();
|
|
58482
58488
|
const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
|
|
58483
58489
|
const reads = [...settingReads("process", vars), ...settingReads("environment", env)];
|
|
58484
|
-
if (reads.length > 0) return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
|
|
58490
|
+
if (reads.length > 0) return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
|
|
58485
58491
|
if (outcome._tag === "Left") return yield* fail$7(outcome.left);
|
|
58486
58492
|
const name = outcome.right;
|
|
58487
58493
|
if (!empty.read()) return name;
|
|
58488
58494
|
const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
|
|
58489
58495
|
if (atHead === name) return name;
|
|
58490
|
-
return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
|
|
58496
|
+
return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
|
|
58491
58497
|
});
|
|
58492
58498
|
var ConfigService = class ConfigService extends Tag("ConfigService")() {
|
|
58493
58499
|
static Live = succeed$2(ConfigService, { load });
|
|
@@ -58495,7 +58501,10 @@ var ConfigService = class ConfigService extends Tag("ConfigService")() {
|
|
|
58495
58501
|
//#endregion
|
|
58496
58502
|
//#region src/workflow/init.ts
|
|
58497
58503
|
const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
|
|
58498
|
-
const INIT_ENV = {
|
|
58504
|
+
const INIT_ENV = {
|
|
58505
|
+
testCommand: "npm test",
|
|
58506
|
+
fastTestCommand: ""
|
|
58507
|
+
};
|
|
58499
58508
|
const MODES_SUGGESTION = {
|
|
58500
58509
|
qa: { format: "npx prettier --write \"$GTD_FILE\"" },
|
|
58501
58510
|
review: { format: "npx prettier --write \"$GTD_FILE\"" }
|
|
@@ -58523,6 +58532,7 @@ const checkKeys = (keys, known, message, section) => {
|
|
|
58523
58532
|
origin
|
|
58524
58533
|
})), [...new Set(keys.map((k) => k.origin))])));
|
|
58525
58534
|
};
|
|
58535
|
+
const knownTo = (def, own) => [.../* @__PURE__ */ new Set([...own, ...def.knownScopes?.() ?? []])];
|
|
58526
58536
|
/**
|
|
58527
58537
|
* Skill lists for the settings actually in use. `.gtdrc` `skills:` keys are
|
|
58528
58538
|
* judged here, not at config load: only now are the pinned/entry vars known.
|
|
@@ -58532,7 +58542,8 @@ const resolveScopeSkills = (def, vars) => gen(function* () {
|
|
|
58532
58542
|
try: () => checkShape(def.skills(vars)),
|
|
58533
58543
|
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58534
58544
|
});
|
|
58535
|
-
|
|
58545
|
+
const known = knownTo(def, Object.keys(skills));
|
|
58546
|
+
yield* checkKeys(def.skillsKeys, known, unknownSkillsKeyMessage, "skills");
|
|
58536
58547
|
return skills;
|
|
58537
58548
|
});
|
|
58538
58549
|
const checkAccessShape = (value) => {
|
|
@@ -58552,7 +58563,8 @@ const resolveScopeAccess = (def, skills, vars) => gen(function* () {
|
|
|
58552
58563
|
try: () => checkAccessShape(def.access(vars)),
|
|
58553
58564
|
catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
|
|
58554
58565
|
});
|
|
58555
|
-
|
|
58566
|
+
const known = knownTo(def, [...Object.keys(skills), ...Object.keys(access)]);
|
|
58567
|
+
yield* checkKeys(def.accessKeys, known, unknownAccessKeyMessage, "access");
|
|
58556
58568
|
return access;
|
|
58557
58569
|
});
|
|
58558
58570
|
//#endregion
|
|
@@ -63941,14 +63953,16 @@ const demandOf = (input) => {
|
|
|
63941
63953
|
* instead of failing loudly.
|
|
63942
63954
|
*/
|
|
63943
63955
|
const statusOf = (input) => {
|
|
63944
|
-
const { rendered, idle, log, changes, next, cost, costByModel } = input;
|
|
63956
|
+
const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
|
|
63945
63957
|
return {
|
|
63946
63958
|
idle,
|
|
63959
|
+
initial,
|
|
63947
63960
|
model: rendered.model,
|
|
63948
63961
|
system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
|
|
63949
63962
|
log,
|
|
63950
63963
|
state: rendered.state,
|
|
63951
63964
|
actor: rendered.actor,
|
|
63965
|
+
workflow,
|
|
63952
63966
|
label: rendered.label,
|
|
63953
63967
|
memory: rendered.memory,
|
|
63954
63968
|
file: rendered.file,
|
|
@@ -63997,7 +64011,7 @@ const dispatchFieldsOf = (demand) => {
|
|
|
63997
64011
|
}
|
|
63998
64012
|
};
|
|
63999
64013
|
/**
|
|
64000
|
-
* Flatten a `Demand` plus its `BeatStatus` into the single
|
|
64014
|
+
* Flatten a `Demand` plus its `BeatStatus` into the single 23-key document
|
|
64001
64015
|
* `gtd next --json` emits — the ONLY place the two are joined. `cost`/
|
|
64002
64016
|
* `costByModel` are omitted together, exactly when no cost was recorded
|
|
64003
64017
|
* (`cost <= 0`).
|
|
@@ -64009,6 +64023,7 @@ const beatDocument = (demand, status) => {
|
|
|
64009
64023
|
kind: demand.kind,
|
|
64010
64024
|
content: demand.content,
|
|
64011
64025
|
idle: status.idle,
|
|
64026
|
+
initial: status.initial,
|
|
64012
64027
|
session,
|
|
64013
64028
|
model: status.model,
|
|
64014
64029
|
system: status.system,
|
|
@@ -64016,6 +64031,7 @@ const beatDocument = (demand, status) => {
|
|
|
64016
64031
|
log: status.log,
|
|
64017
64032
|
state: status.state,
|
|
64018
64033
|
actor: status.actor,
|
|
64034
|
+
workflow: status.workflow,
|
|
64019
64035
|
label: status.label,
|
|
64020
64036
|
memory: status.memory,
|
|
64021
64037
|
file: status.file,
|
|
@@ -64054,7 +64070,7 @@ const definedFields = (entries) => {
|
|
|
64054
64070
|
return result;
|
|
64055
64071
|
};
|
|
64056
64072
|
/**
|
|
64057
|
-
* The header block's own lines — `State:`/`Awaits:`/optional `Label:`/
|
|
64073
|
+
* The header block's own lines — `State:`/`Awaits:`/`Workflow:`/optional `Label:`/
|
|
64058
64074
|
* `Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`(+breakdown)/`Pending:`/`Next:`,
|
|
64059
64075
|
* in that order. Used by `renderBeatPlain`'s header-showing kinds below.
|
|
64060
64076
|
*/
|
|
@@ -64069,6 +64085,7 @@ const beatHeaderLines = (document) => {
|
|
|
64069
64085
|
return [
|
|
64070
64086
|
`State: ${document.state}`,
|
|
64071
64087
|
`Awaits: ${document.actor}`,
|
|
64088
|
+
`Workflow: ${document.workflow}`,
|
|
64072
64089
|
...Object.entries(optional).map(([key, value]) => `${key}: ${value}`),
|
|
64073
64090
|
...costStatusLines(document.cost ?? 0, document.costByModel ?? []),
|
|
64074
64091
|
...pendingStatusLines(document.changes),
|
|
@@ -64270,7 +64287,7 @@ const accessRefusal = (changes, write) => {
|
|
|
64270
64287
|
].join("\n");
|
|
64271
64288
|
};
|
|
64272
64289
|
//#endregion
|
|
64273
|
-
//#region src/step/
|
|
64290
|
+
//#region src/step/planStart.ts
|
|
64274
64291
|
const refusal = (message) => ({
|
|
64275
64292
|
kind: "refusal",
|
|
64276
64293
|
message
|
|
@@ -64282,43 +64299,42 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
|
|
|
64282
64299
|
if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
|
|
64283
64300
|
});
|
|
64284
64301
|
/**
|
|
64285
|
-
* `gtd --
|
|
64286
|
-
*
|
|
64287
|
-
* `Gtd-
|
|
64302
|
+
* `gtd --workflow <name>`: start a brand NEW process on a workflow, writing an
|
|
64303
|
+
* opening commit that names the workflow's first step and carries a
|
|
64304
|
+
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
64305
|
+
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
|
|
64288
64306
|
* All validation is a REFUSAL, not an Effect failure.
|
|
64289
64307
|
*/
|
|
64290
|
-
const
|
|
64291
|
-
const {
|
|
64292
|
-
|
|
64293
|
-
if (current.
|
|
64308
|
+
const planStart = (current, actor, start) => gen(function* () {
|
|
64309
|
+
const { workflow, first, commandLabel, vars: varOverrides } = start;
|
|
64310
|
+
const name = workflow.name;
|
|
64311
|
+
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before starting another`);
|
|
64312
|
+
if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`);
|
|
64313
|
+
const { firstStep } = first;
|
|
64294
64314
|
const config = yield* (yield* ConfigService).load;
|
|
64295
64315
|
const declaredNames = Object.keys({
|
|
64296
|
-
...
|
|
64316
|
+
...workflow.vars,
|
|
64297
64317
|
...config.rcVars
|
|
64298
64318
|
});
|
|
64299
64319
|
const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
|
|
64300
|
-
...
|
|
64320
|
+
...workflow.env,
|
|
64301
64321
|
...config.rcEnv
|
|
64302
64322
|
}));
|
|
64303
64323
|
if (overrideRefusal !== void 0) return overrideRefusal;
|
|
64304
|
-
|
|
64305
|
-
const
|
|
64306
|
-
const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
|
|
64324
|
+
const baseOf = workflow.def.base;
|
|
64325
|
+
const vars = resolveVars(workflow.vars, config.rcVars, varOverrides, (yield* Host).env);
|
|
64307
64326
|
const multiline = multilineSetting(vars);
|
|
64308
64327
|
if (multiline !== void 0) return refusal(`${commandLabel}: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`);
|
|
64309
64328
|
const template = yield* try_({
|
|
64310
64329
|
try: () => baseOf?.(name, vars),
|
|
64311
64330
|
catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
|
|
64312
64331
|
});
|
|
64313
|
-
|
|
64314
|
-
|
|
64315
|
-
|
|
64316
|
-
base = resolved;
|
|
64317
|
-
}
|
|
64318
|
-
const subject = formatSubject(actor, name);
|
|
64332
|
+
const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
|
|
64333
|
+
if (typeof base === "object") return base;
|
|
64334
|
+
const subject = formatSubject(actor, firstStep);
|
|
64319
64335
|
return {
|
|
64320
|
-
kind: "
|
|
64321
|
-
|
|
64336
|
+
kind: "start",
|
|
64337
|
+
workflow: name,
|
|
64322
64338
|
subject,
|
|
64323
64339
|
steps: [{
|
|
64324
64340
|
kind: "gitWrite",
|
|
@@ -64326,7 +64342,8 @@ const planEntry = (current, actor, entry) => gen(function* () {
|
|
|
64326
64342
|
kind: "commitAll",
|
|
64327
64343
|
message: formatCommitMessage({
|
|
64328
64344
|
actor,
|
|
64329
|
-
to:
|
|
64345
|
+
to: firstStep,
|
|
64346
|
+
workflow: name,
|
|
64330
64347
|
...base !== void 0 ? { reviewBase: base } : {},
|
|
64331
64348
|
vars
|
|
64332
64349
|
})
|
|
@@ -64340,16 +64357,21 @@ const planEntry = (current, actor, entry) => gen(function* () {
|
|
|
64340
64357
|
}]
|
|
64341
64358
|
};
|
|
64342
64359
|
});
|
|
64343
|
-
/**
|
|
64344
|
-
|
|
64345
|
-
|
|
64346
|
-
|
|
64360
|
+
/**
|
|
64361
|
+
* The workflow's diff base: `git merge-base <base> HEAD`, where a blank base
|
|
64362
|
+
* means the default branch. The merge-base is what gets pinned, so a base on
|
|
64363
|
+
* another branch reviews exactly this branch's own commits.
|
|
64364
|
+
*/
|
|
64365
|
+
const resolveBase = (commandLabel, value) => gen(function* () {
|
|
64347
64366
|
const git = yield* GitService;
|
|
64348
|
-
const
|
|
64349
|
-
|
|
64350
|
-
|
|
64351
|
-
if (resolvedBase.
|
|
64352
|
-
|
|
64367
|
+
const rendered = value.trim();
|
|
64368
|
+
const target = rendered === "" ? yield* git.defaultBranch() : rendered;
|
|
64369
|
+
const resolvedBase = yield* either(git.resolveRef(target));
|
|
64370
|
+
if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${target}" does not resolve to a commit`);
|
|
64371
|
+
const mergeBase = yield* git.mergeBase(resolvedBase.right, "HEAD");
|
|
64372
|
+
if (isNone(mergeBase)) return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`);
|
|
64373
|
+
if (mergeBase.value === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: nothing to review: HEAD has no commits beyond ${target}`);
|
|
64374
|
+
return mergeBase.value;
|
|
64353
64375
|
});
|
|
64354
64376
|
//#endregion
|
|
64355
64377
|
//#region src/Edge.ts
|
|
@@ -64362,9 +64384,10 @@ const ACTORS = /* @__PURE__ */ new Set([
|
|
|
64362
64384
|
]);
|
|
64363
64385
|
/**
|
|
64364
64386
|
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
64365
|
-
* back from HEAD: a
|
|
64366
|
-
* `gtd --
|
|
64367
|
-
* episode as its first commit
|
|
64387
|
+
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
64388
|
+
* `gtd --workflow` writes — no landing ever produces one — and opens an
|
|
64389
|
+
* episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
|
|
64390
|
+
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
64368
64391
|
* entering the flow's first step (a finished episode), bounds the episode
|
|
64369
64392
|
* from below.
|
|
64370
64393
|
*/
|
|
@@ -64373,18 +64396,21 @@ const locateEpisode = (def, history) => {
|
|
|
64373
64396
|
const message = parseCommitMessage(history[i].message);
|
|
64374
64397
|
const subject = message.parsed;
|
|
64375
64398
|
if (subject !== void 0 && message.step === void 0 && subject.from === void 0 && subject.actor === "human") return {
|
|
64376
|
-
|
|
64399
|
+
workflow: message.workflow,
|
|
64400
|
+
legacyOpening: message.workflow === void 0,
|
|
64377
64401
|
baseIndex: i,
|
|
64378
64402
|
processStart: i
|
|
64379
64403
|
};
|
|
64380
64404
|
if (subject === void 0 || !ACTORS.has(subject.actor) || subject.to === def.initial) return {
|
|
64381
|
-
|
|
64405
|
+
workflow: void 0,
|
|
64406
|
+
legacyOpening: false,
|
|
64382
64407
|
baseIndex: i,
|
|
64383
64408
|
processStart: i + 1
|
|
64384
64409
|
};
|
|
64385
64410
|
}
|
|
64386
64411
|
return {
|
|
64387
|
-
|
|
64412
|
+
workflow: void 0,
|
|
64413
|
+
legacyOpening: false,
|
|
64388
64414
|
baseIndex: -1,
|
|
64389
64415
|
processStart: 0
|
|
64390
64416
|
};
|
|
@@ -64418,7 +64444,8 @@ const runOf = (history, location, closingHash = void 0) => {
|
|
|
64418
64444
|
const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
|
|
64419
64445
|
const head = history[history.length - 1];
|
|
64420
64446
|
return {
|
|
64421
|
-
|
|
64447
|
+
workflow: location.workflow,
|
|
64448
|
+
legacyOpening: location.legacyOpening,
|
|
64422
64449
|
startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
|
|
64423
64450
|
startParentHash,
|
|
64424
64451
|
diffBase: first?.reviewBase ?? startParentHash,
|
|
@@ -64445,10 +64472,10 @@ const PAGE_TIERS = [
|
|
|
64445
64472
|
* gtd step commit, or one landing the flow's initial state, bounds an
|
|
64446
64473
|
* episode from below — EXCEPT a bare `gtd(human): <x>` subject (no `→`),
|
|
64447
64474
|
* which is NEVER a boundary here, whatever `<x>` is. That bare shape is
|
|
64448
|
-
* `locateEpisode`'s own `opening` branch (
|
|
64475
|
+
* `locateEpisode`'s own `opening` branch (a started episode's first commit),
|
|
64449
64476
|
* which takes priority there over the initial-state check no matter what the
|
|
64450
|
-
*
|
|
64451
|
-
* where the two branches would otherwise disagree. A bare human commit can
|
|
64477
|
+
* workflow's first step is named — including one named after `def.initial`
|
|
64478
|
+
* itself, where the two branches would otherwise disagree. A bare human commit can
|
|
64452
64479
|
* also be a self-loop step landing rather than a real opening (this
|
|
64453
64480
|
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
64454
64481
|
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
@@ -64584,16 +64611,24 @@ const pendingTree = (workspace, entries, rewrite) => {
|
|
|
64584
64611
|
* The one resolver of both setting kinds. Process settings: the recorded
|
|
64585
64612
|
* (pinned) value wins per name, anything unrecorded resolves live — the
|
|
64586
64613
|
* fallback for a process started before pinning existed. Environment settings
|
|
64587
|
-
* are always live. `
|
|
64588
|
-
* process is being
|
|
64614
|
+
* are always live. `startOverrides` are `--var` values, passed only while a
|
|
64615
|
+
* process is being started.
|
|
64589
64616
|
*/
|
|
64590
|
-
const settingsFor = (config, pinnedVars, hostEnv,
|
|
64617
|
+
const settingsFor = (config, workflow, pinnedVars, hostEnv, startOverrides = {}) => ({
|
|
64591
64618
|
vars: {
|
|
64592
|
-
...resolveVars(
|
|
64619
|
+
...resolveVars(workflow.vars, config.rcVars, startOverrides, hostEnv),
|
|
64593
64620
|
...pinnedVars
|
|
64594
64621
|
},
|
|
64595
|
-
env: resolveVars(
|
|
64622
|
+
env: resolveVars(workflow.env, config.rcEnv, {}, hostEnv)
|
|
64596
64623
|
});
|
|
64624
|
+
/** The workflow a run replays on: its pinned one, else the default. */
|
|
64625
|
+
const workflowOf = (config, run) => {
|
|
64626
|
+
if (run.legacyOpening) return fail$7(/* @__PURE__ */ new Error("gtd: this process was opened by `gtd --entry`, which no longer exists — run `gtd abandon` to start over"));
|
|
64627
|
+
if (run.workflow === void 0) return succeed$3(config.defaultWorkflow);
|
|
64628
|
+
const named = config.workflowNamed(run.workflow);
|
|
64629
|
+
if (named !== void 0) return succeed$3(named);
|
|
64630
|
+
return fail$7(/* @__PURE__ */ new Error(`gtd: this process runs workflow "${run.workflow}", which gtd.config.ts no longer defines — run \`gtd abandon\` to start over`));
|
|
64631
|
+
};
|
|
64597
64632
|
/**
|
|
64598
64633
|
* `judgeBudgetBytes` is the one var that refuses rather than disabling its
|
|
64599
64634
|
* mechanism when blanked: without a bound the judge payload is rejected
|
|
@@ -64610,7 +64645,6 @@ const judgeBudgetBytes = (vars) => {
|
|
|
64610
64645
|
const replayFor = (setup, pending) => replay({
|
|
64611
64646
|
flow: setup.def.flow,
|
|
64612
64647
|
episode: {
|
|
64613
|
-
entry: setup.run.entry,
|
|
64614
64648
|
base: setup.base,
|
|
64615
64649
|
commits: setup.episode
|
|
64616
64650
|
},
|
|
@@ -64628,7 +64662,7 @@ const replayFor = (setup, pending) => replay({
|
|
|
64628
64662
|
const replayError = (outcome) => {
|
|
64629
64663
|
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
64630
64664
|
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
64631
|
-
if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default
|
|
64665
|
+
if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default workflow's first step — the workflow changed under this process; run `gtd abandon` to start over");
|
|
64632
64666
|
};
|
|
64633
64667
|
const judgeDocument = (step) => step.request.kind === "judge" ? JSON.stringify({
|
|
64634
64668
|
state: step.request.evidence,
|
|
@@ -64755,14 +64789,15 @@ const costByModel = (entries) => {
|
|
|
64755
64789
|
})).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
64756
64790
|
};
|
|
64757
64791
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
64758
|
-
const restAt = (ref,
|
|
64792
|
+
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
64759
64793
|
const git = yield* GitService;
|
|
64760
64794
|
const config = yield* (yield* ConfigService).load;
|
|
64761
64795
|
const workspace = yield* Workspace;
|
|
64762
64796
|
const host = yield* Host;
|
|
64763
|
-
const
|
|
64764
|
-
const
|
|
64765
|
-
const
|
|
64797
|
+
const run = yield* computeProcessRun(git, config.workflow, ref);
|
|
64798
|
+
const resolved = yield* workflowOf(config, run);
|
|
64799
|
+
const def = resolved.def;
|
|
64800
|
+
const { vars, env } = settingsFor(config, resolved, run.pinnedVars, host.env, startOverrides);
|
|
64766
64801
|
const skills = yield* resolveScopeSkills(def, vars);
|
|
64767
64802
|
const access = yield* resolveScopeAccess(def, skills, vars);
|
|
64768
64803
|
const budget = yield* try_({
|
|
@@ -64810,6 +64845,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64810
64845
|
const memory = memoryOf(outcome.trace, run);
|
|
64811
64846
|
return {
|
|
64812
64847
|
def,
|
|
64848
|
+
workflowName: resolved.name,
|
|
64813
64849
|
state: step.name,
|
|
64814
64850
|
stepDef,
|
|
64815
64851
|
actor: step.actor,
|
|
@@ -64831,21 +64867,20 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
|
|
|
64831
64867
|
});
|
|
64832
64868
|
const currentRest = restAt(void 0);
|
|
64833
64869
|
/**
|
|
64834
|
-
*
|
|
64835
|
-
*
|
|
64836
|
-
* the opening commit
|
|
64870
|
+
* Whether `gtd --workflow <name>` can start a process here: the workflow must
|
|
64871
|
+
* reach a step from the tree the opening commit would capture. That step is
|
|
64872
|
+
* what the opening commit's subject names.
|
|
64837
64873
|
*/
|
|
64838
|
-
const
|
|
64874
|
+
const startRefusal = (rest, resolved, startVars) => gen(function* () {
|
|
64839
64875
|
const config = yield* (yield* ConfigService).load;
|
|
64840
64876
|
const host = yield* Host;
|
|
64841
|
-
const { vars, env } = settingsFor(config, {}, host.env,
|
|
64842
|
-
const skills = yield* resolveScopeSkills(
|
|
64843
|
-
const access = yield* resolveScopeAccess(
|
|
64877
|
+
const { vars, env } = settingsFor(config, resolved, {}, host.env, startVars);
|
|
64878
|
+
const skills = yield* resolveScopeSkills(resolved.def, vars);
|
|
64879
|
+
const access = yield* resolveScopeAccess(resolved.def, skills, vars);
|
|
64844
64880
|
const workspace = rest.setup.workspace;
|
|
64845
64881
|
const outcome = yield* promise(() => replay({
|
|
64846
|
-
flow:
|
|
64882
|
+
flow: resolved.def.flow,
|
|
64847
64883
|
episode: {
|
|
64848
|
-
entry: name,
|
|
64849
64884
|
base: {
|
|
64850
64885
|
hash: "",
|
|
64851
64886
|
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
@@ -64857,13 +64892,14 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
64857
64892
|
start: "",
|
|
64858
64893
|
budgetBytes: rest.setup.budget,
|
|
64859
64894
|
skills,
|
|
64860
|
-
configuredSkills:
|
|
64895
|
+
configuredSkills: resolved.def.configuredSkills,
|
|
64861
64896
|
access,
|
|
64862
|
-
configuredAccess:
|
|
64897
|
+
configuredAccess: resolved.def.configuredAccess
|
|
64863
64898
|
}));
|
|
64864
|
-
if (outcome.kind === "
|
|
64865
|
-
if (outcome.kind === "
|
|
64866
|
-
|
|
64899
|
+
if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
|
|
64900
|
+
if (outcome.kind === "refused") return { refusal: outcome.message };
|
|
64901
|
+
const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
|
|
64902
|
+
return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
|
|
64867
64903
|
});
|
|
64868
64904
|
/** The review window's diff base at the rest. */
|
|
64869
64905
|
const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
|
|
@@ -64881,8 +64917,8 @@ const renderRest = (rest) => succeed$3({
|
|
|
64881
64917
|
* attempt at this very agent rest, and another dispatch would repeat it.
|
|
64882
64918
|
*/
|
|
64883
64919
|
const stalledAt = (rest) => rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
|
|
64884
|
-
/** No process is underway: the default
|
|
64885
|
-
const noProcessUnderway = (rest) => rest.run.
|
|
64920
|
+
/** No process is underway: the default workflow's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
|
|
64921
|
+
const noProcessUnderway = (rest) => rest.run.workflow === void 0 && rest.state === rest.def.initial && rest.step.id.occurrence === 1;
|
|
64886
64922
|
/** `idle` means exactly one thing: no process underway, clean tree. */
|
|
64887
64923
|
const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
|
|
64888
64924
|
const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
|
|
@@ -64921,8 +64957,19 @@ const landingTarget = (outcome, rest) => {
|
|
|
64921
64957
|
message: outcome.message
|
|
64922
64958
|
});
|
|
64923
64959
|
if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$7(new Error(outcome.message));
|
|
64924
|
-
|
|
64960
|
+
if (outcome.kind !== "rest") return succeed$3(rest.def.initial);
|
|
64961
|
+
const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
|
|
64962
|
+
return succeed$3(reserved === void 0 ? outcome.rest.name : reserved);
|
|
64925
64963
|
};
|
|
64964
|
+
/**
|
|
64965
|
+
* A step commit entering the default's first step is what ends an episode, so
|
|
64966
|
+
* a named workflow landing there would read back as finished. Its opening
|
|
64967
|
+
* commit may name that step: a bare opening is never read as a boundary.
|
|
64968
|
+
*/
|
|
64969
|
+
const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && step === initial ? {
|
|
64970
|
+
kind: "refusal",
|
|
64971
|
+
message: `gtd: workflow "${workflow}" reaches step "${step}", the default workflow's first step — that is where a finished process waits, so a started process must never land there; rename the step`
|
|
64972
|
+
} : void 0;
|
|
64926
64973
|
const landingTo = (rest, to) => {
|
|
64927
64974
|
if (rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state) return {
|
|
64928
64975
|
kind: "noop",
|
|
@@ -64984,7 +65031,8 @@ const snapshotFromRest = (rest, verdicts) => gen(function* () {
|
|
|
64984
65031
|
const summaryFor = (run) => gen(function* () {
|
|
64985
65032
|
const config = yield* (yield* ConfigService).load;
|
|
64986
65033
|
const host = yield* Host;
|
|
64987
|
-
const
|
|
65034
|
+
const resolved = yield* workflowOf(config, run);
|
|
65035
|
+
const summary = resolved.def.summary;
|
|
64988
65036
|
if (summary === void 0 || run.trace.length === 0) return void 0;
|
|
64989
65037
|
const entryCommit = run.trace[0].hash;
|
|
64990
65038
|
return summary({
|
|
@@ -64997,7 +65045,7 @@ const summaryFor = (run) => gen(function* () {
|
|
|
64997
65045
|
})),
|
|
64998
65046
|
processCost: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
|
|
64999
65047
|
processCostByModel: costByModel(run.costEntries),
|
|
65000
|
-
...settingsFor(config, run.pinnedVars, host.env)
|
|
65048
|
+
...settingsFor(config, resolved, run.pinnedVars, host.env)
|
|
65001
65049
|
});
|
|
65002
65050
|
});
|
|
65003
65051
|
//#endregion
|
|
@@ -76876,22 +76924,15 @@ f="$(gtd next --json=file)"
|
|
|
76876
76924
|
f="\${f:-.gtd/TODO.md}"
|
|
76877
76925
|
mkdir -p "$(dirname "$f")"
|
|
76878
76926
|
exec "\${EDITOR:-vi}" "$f"`;
|
|
76879
|
-
const
|
|
76927
|
+
const DOOR_COMMAND = `#!/usr/bin/env sh
|
|
76880
76928
|
set -eu
|
|
76881
76929
|
GTD_BUILD=~/.local/bin/gtd-build
|
|
76882
76930
|
if [ -z "\${1:-}" ]; then
|
|
76883
|
-
echo "usage: gtd-
|
|
76931
|
+
echo "usage: gtd-door <name> [args...]" >&2
|
|
76884
76932
|
exit 2
|
|
76885
76933
|
fi
|
|
76886
76934
|
cd "$(git rev-parse --show-toplevel)"
|
|
76887
|
-
script="$(gtd
|
|
76888
|
-
sh -c "$script"
|
|
76889
|
-
exec "$GTD_BUILD"`;
|
|
76890
|
-
const FIX_COMMAND = `#!/usr/bin/env sh
|
|
76891
|
-
set -eu
|
|
76892
|
-
GTD_BUILD=~/.local/bin/gtd-build
|
|
76893
|
-
cd "$(git rev-parse --show-toplevel)"
|
|
76894
|
-
script="$(gtd --entry fix-precheck)"
|
|
76935
|
+
script="$(gtd door "$@")"
|
|
76895
76936
|
sh -c "$script"
|
|
76896
76937
|
exec "$GTD_BUILD"`;
|
|
76897
76938
|
const HEADER = () => `gtd ${GTD_VERSION$1} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, never runs a check script, and never runs a steering mode's own\nformat:/validate: command either — that pair is text in the script gtd next\n--json's validate field (or gtd validate) prints, for the driver to run\nitself ahead of gtd land.\n`;
|
|
@@ -76972,7 +77013,8 @@ exits 0, so read it in a shell variable and guard on emptiness
|
|
|
76972
77013
|
adds the self-validation instruction at a validatable \`prompt\` rest, and a
|
|
76973
77014
|
status-summary header at every OTHER kind; \`content\` itself never carries
|
|
76974
77015
|
either), \`idle\` (\`true\` iff the resolved rest is the workflow's initial
|
|
76975
|
-
state with a clean tree), \`
|
|
77016
|
+
state with a clean tree), \`initial\` (\`true\` iff no process is underway —
|
|
77017
|
+
the default workflow's first step, first visit — whatever the tree holds), \`log\`, \`state\`, \`actor\`, \`changes\` (which
|
|
76976
77018
|
declared \`on\` pattern, if any, each pending change matches), \`next\` (the
|
|
76977
77019
|
first declared \`on\` edge that would fire right now — \`{action?, pattern,
|
|
76978
77020
|
target}\` — or \`null\` on no match). Only at \`kind: "prompt"\` (the DISPATCH
|
|
@@ -77066,7 +77108,7 @@ const DRIVER_OBLIGATIONS = `
|
|
|
77066
77108
|
\`--json=judge\`, pipes that rendered \`{ state, questions }\` document to a
|
|
77067
77109
|
judge model, and pipes ITS verdict (one \`{ id, answer, p }\` per pending
|
|
77068
77110
|
question, as JSON on stdin) to \`gtd judge answer\` — the FIFTH command
|
|
77069
|
-
that changes anything, alongside \`gtd land\`/\`gtd --
|
|
77111
|
+
that changes anything, alongside \`gtd land\`/\`gtd --workflow\`/\`gtd
|
|
77070
77112
|
abandon\`/\`gtd restore\`. It follows the exact same required-half /
|
|
77071
77113
|
optional-half script contract as \`gtd land\` in obligation 8 above:
|
|
77072
77114
|
read \`--json=script\` (and, if you want them, \`--json=settled\`/
|
|
@@ -77108,10 +77150,10 @@ logic beyond "ask gtd again".
|
|
|
77108
77150
|
const commandSuite = () => `
|
|
77109
77151
|
## Building the user's command suite: interview first, then adapt
|
|
77110
77152
|
|
|
77111
|
-
The obligations above are the contract; the
|
|
77153
|
+
The obligations above are the contract; the three command bodies below are one
|
|
77112
77154
|
WORKED EXAMPLE of it. Do not copy them blindly, and do not guess the user's
|
|
77113
77155
|
setup — INVESTIGATE first, then INTERVIEW, then build a suite shaped by both.
|
|
77114
|
-
Hold ONE conversation, not
|
|
77156
|
+
Hold ONE conversation, not three: the setup steps below come first, because a
|
|
77115
77157
|
repo that is not set up yet cannot be driven, and the suite-shape questions
|
|
77116
77158
|
follow in the same numbered list.
|
|
77117
77159
|
|
|
@@ -77167,8 +77209,8 @@ follow in the same numbered list.
|
|
|
77167
77209
|
wrapped in \`# gtd-install: model exports\` / \`# gtd-install: end\`
|
|
77168
77210
|
markers — NOT into \`.gtdrc\`. They stay per machine and are never
|
|
77169
77211
|
committed, so one person's model choice binds nobody else on the repo.
|
|
77170
|
-
Two consequences follow, both worth stating to the user: \`gtd-
|
|
77171
|
-
|
|
77212
|
+
Two consequences follow, both worth stating to the user: \`gtd-door\`
|
|
77213
|
+
inherits these exports only because it \`exec\`s
|
|
77172
77214
|
\`gtd-build\` — anything that drives beats without going through
|
|
77173
77215
|
\`gtd-build\` sees the raw \`smart\`/\`base\` hints and fails; and \`GTD_*\` is
|
|
77174
77216
|
the highest-precedence config layer, so these exports silently win over
|
|
@@ -77183,11 +77225,11 @@ follow in the same numbered list.
|
|
|
77183
77225
|
obligations directly. Pick the runtime to match: bash, their language of
|
|
77184
77226
|
choice, anything that reads \`--json=<path>\` and spawns subprocesses.
|
|
77185
77227
|
9. **Where should the suite live, which editor should \`gtd-edit\` spawn, and
|
|
77186
|
-
does anything need renaming?** Suggest \`~/.local/bin\` for all
|
|
77187
|
-
\`gtd-build\`, \`gtd-edit\`, \`gtd-
|
|
77228
|
+
does anything need renaming?** Suggest \`~/.local/bin\` for all three —
|
|
77229
|
+
\`gtd-build\`, \`gtd-edit\`, \`gtd-door\` — and \`$EDITOR\` (never
|
|
77188
77230
|
a hardcoded editor) for the one that opens files. The interview can
|
|
77189
|
-
rename any of the
|
|
77190
|
-
RESOLVED path baked into \`GTD_BUILD\` in the
|
|
77231
|
+
rename any of the three; whatever name is chosen for \`gtd-build\` is the
|
|
77232
|
+
RESOLVED path baked into \`GTD_BUILD\` in the \`gtd-door\` body below —
|
|
77191
77233
|
never the literal string \`gtd-build\`.
|
|
77192
77234
|
|
|
77193
77235
|
Then build it, and verify safely before the first real drive:
|
|
@@ -77196,9 +77238,9 @@ check your kind dispatch against the table above, call them as often as you
|
|
|
77196
77238
|
like. Nothing happens until you run an emitted script.
|
|
77197
77239
|
|
|
77198
77240
|
The reference rendering in sh (no \`jq\`, no JSON parser at all — each
|
|
77199
|
-
\`--json=<path>\` call prints its one value directly).
|
|
77200
|
-
|
|
77201
|
-
suite's resolved \`gtd-build\` path.
|
|
77241
|
+
\`--json=<path>\` call prints its one value directly). \`gtd-door\` takes the
|
|
77242
|
+
\`gtd-build\` path from one convention: \`GTD_BUILD\` is set once, at the top, to
|
|
77243
|
+
the suite's resolved \`gtd-build\` path.
|
|
77202
77244
|
|
|
77203
77245
|
### \`gtd-build\` — the driver loop
|
|
77204
77246
|
|
|
@@ -77236,41 +77278,33 @@ are ready. Default path: \`~/.local/bin/gtd-edit\`.
|
|
|
77236
77278
|
${EDIT_COMMAND}
|
|
77237
77279
|
\`\`\`
|
|
77238
77280
|
|
|
77239
|
-
### \`gtd-
|
|
77240
|
-
|
|
77241
|
-
Runs \`gtd --entry review-gate.check --var reviewBase=<commitish>\`, captured
|
|
77242
|
-
by command substitution — never a pipe: a pipeline reports only its LAST
|
|
77243
|
-
command's exit status, so \`gtd --entry ... | sh\` under \`set -e\` would sail
|
|
77244
|
-
past a refusal and \`exec\` a loop over a process that was never started. It
|
|
77245
|
-
runs the captured script, then \`exec\`s the suite's RESOLVED \`gtd-build\`
|
|
77246
|
-
path — never the literal string \`gtd-build\`. This is one invocation that
|
|
77247
|
-
carries the review all the way to its next human gate, not just a starter:
|
|
77248
|
-
\`gtd-review\` on a RED baseline hands off to \`gtd-build\`, which halts at the
|
|
77249
|
-
blocked gate and prints it, rather than starting the review. Refuses with
|
|
77250
|
-
\`usage: gtd-review <commitish>\` on stderr and exit \`2\` when the commitish is
|
|
77251
|
-
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-review\`.
|
|
77281
|
+
### \`gtd-door <name> [args...]\` — start a process through a door and drive it
|
|
77252
77282
|
|
|
77253
|
-
|
|
77254
|
-
|
|
77255
|
-
|
|
77256
|
-
|
|
77257
|
-
|
|
77258
|
-
|
|
77259
|
-
|
|
77260
|
-
|
|
77283
|
+
Runs \`gtd door <name> [args...]\`, captured by command substitution — never a
|
|
77284
|
+
pipe: a pipeline reports only its LAST command's exit status, so
|
|
77285
|
+
\`gtd door ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
|
|
77286
|
+
loop over a process that was never started. It runs the captured script, then
|
|
77287
|
+
\`exec\`s the suite's RESOLVED \`gtd-build\` path — never the literal string
|
|
77288
|
+
\`gtd-build\`. This is one invocation that carries the process all the way to
|
|
77289
|
+
its next human gate, not just a starter: \`gtd-door review\` on a RED baseline
|
|
77290
|
+
hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
|
|
77291
|
+
rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
77261
77292
|
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
77262
|
-
|
|
77293
|
+
\`gtd doors\` lists the doors a repository offers (the bundled \`fix\` and
|
|
77294
|
+
\`review [base]\`, plus any its config declares). Refuses with
|
|
77295
|
+
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
77296
|
+
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
77263
77297
|
|
|
77264
77298
|
\`\`\`bash
|
|
77265
|
-
${
|
|
77299
|
+
${DOOR_COMMAND}
|
|
77266
77300
|
\`\`\`
|
|
77267
77301
|
${REINSTALL}`;
|
|
77268
77302
|
const REINSTALL = `
|
|
77269
77303
|
## Re-installing: detect and adapt, don't blindly overwrite
|
|
77270
77304
|
|
|
77271
77305
|
A second install on a machine that already has a suite is not a fresh
|
|
77272
|
-
install — read each of the
|
|
77273
|
-
(\`gtd-build\`, \`gtd-edit\`, \`gtd-
|
|
77306
|
+
install — read each of the three suite paths before writing anything
|
|
77307
|
+
(\`gtd-build\`, \`gtd-edit\`, \`gtd-door\`), and branch per path:
|
|
77274
77308
|
|
|
77275
77309
|
- Absent — install it, as in a fresh install.
|
|
77276
77310
|
- Present and content-equal to what this gtd version would emit — say so,
|
|
@@ -77292,10 +77326,16 @@ markers is unchanged, and re-asks nothing — the resolved model names are
|
|
|
77292
77326
|
per machine, so without this exemption every single re-install would
|
|
77293
77327
|
report drift on the one command everybody has.
|
|
77294
77328
|
|
|
77295
|
-
This check scopes to exactly the
|
|
77329
|
+
This check scopes to exactly the three suite paths above, plus the superseded
|
|
77330
|
+
pair below. \`gtd-loop\` is
|
|
77296
77331
|
outside it entirely — never read, never diffed, never touched by it in any
|
|
77297
77332
|
way — an existing \`gtd-loop\` survives untouched beside the new
|
|
77298
77333
|
\`gtd-build\`, and cleaning it up is the human's own call.
|
|
77334
|
+
|
|
77335
|
+
\`gtd-fix\` and \`gtd-review\` are superseded by \`gtd-door\` (\`gtd-door fix\`,
|
|
77336
|
+
\`gtd-door review [base]\`). When either is still present beside the suite,
|
|
77337
|
+
name it to the user as superseded and ask before removing it — never remove
|
|
77338
|
+
one unasked.
|
|
77299
77339
|
`;
|
|
77300
77340
|
const EDITOR_INTEGRATION = `
|
|
77301
77341
|
## Editor integration: offer it, don't bury it
|
|
@@ -77343,7 +77383,7 @@ const PREREQUISITES = `
|
|
|
77343
77383
|
## Prerequisites and portability
|
|
77344
77384
|
|
|
77345
77385
|
- A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
|
|
77346
|
-
scripts (\`gtd land --json=script\`, \`gtd --
|
|
77386
|
+
scripts (\`gtd land --json=script\`, \`gtd --workflow <name>\`, \`gtd abandon\`,
|
|
77347
77387
|
\`gtd restore\`, \`gtd judge answer --json=script\`) are POSIX sh; captured,
|
|
77348
77388
|
then piped into it (see obligation 8 above). Reading
|
|
77349
77389
|
\`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own output needs
|
|
@@ -77481,7 +77521,7 @@ const normalizeScriptNewline = (script) => script.length === 0 ? script : `${scr
|
|
|
77481
77521
|
/**
|
|
77482
77522
|
* `combinedScript`, narrowed to accept only an already-guarded
|
|
77483
77523
|
* `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
|
|
77484
|
-
* exists to protect: `gtd land`/`gtd --
|
|
77524
|
+
* exists to protect: `gtd land`/`gtd --workflow`'s emitted script. Every OTHER
|
|
77485
77525
|
* `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
|
|
77486
77526
|
* from `emitScripts` directly and keeps the unbranded signature — those
|
|
77487
77527
|
* paths land no guard-gated git write for a brand to protect.
|
|
@@ -77644,29 +77684,77 @@ const runLandCommand = (opts, json, out) => gen(function* () {
|
|
|
77644
77684
|
else out.write(renderLandPlain(built));
|
|
77645
77685
|
});
|
|
77646
77686
|
/**
|
|
77647
|
-
* Starts a brand new process
|
|
77648
|
-
* whatever the working tree carries at
|
|
77649
|
-
* than demanding a clean tree.
|
|
77687
|
+
* Starts a brand new process on `<workflow>`, always as `"human"`. Captures
|
|
77688
|
+
* whatever the working tree carries at the start, like an ordinary land,
|
|
77689
|
+
* rather than demanding a clean tree.
|
|
77650
77690
|
*
|
|
77651
|
-
* Refused when a process is already
|
|
77652
|
-
*
|
|
77653
|
-
* something that is not an ancestor of (and differs from)
|
|
77691
|
+
* An unknown name is a usage error. Refused when a process is already
|
|
77692
|
+
* underway, the workflow reaches no step, a `--var` name is undeclared, or
|
|
77693
|
+
* `base` renders to something that is not an ancestor of (and differs from)
|
|
77694
|
+
* HEAD.
|
|
77654
77695
|
*/
|
|
77655
|
-
const
|
|
77696
|
+
const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => gen(function* () {
|
|
77697
|
+
const config = yield* (yield* ConfigService).load;
|
|
77698
|
+
const resolved = config.workflowNamed(workflow);
|
|
77699
|
+
if (resolved === void 0) return yield* fail$7(new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — startable: ${config.workflowNames.join(", ")}`));
|
|
77656
77700
|
const rest = yield* restAt(void 0, varOverrides);
|
|
77657
|
-
const
|
|
77658
|
-
|
|
77701
|
+
const started = yield* startRefusal(rest, resolved, varOverrides);
|
|
77702
|
+
const plan = yield* planStart({
|
|
77659
77703
|
state: rest.state,
|
|
77660
|
-
idle: noProcessUnderway(rest)
|
|
77661
|
-
entryRefusal: yield* entryRefusal(rest, entryState, varOverrides)
|
|
77704
|
+
idle: noProcessUnderway(rest)
|
|
77662
77705
|
}, actor, {
|
|
77663
|
-
|
|
77706
|
+
workflow: resolved,
|
|
77707
|
+
first: started,
|
|
77664
77708
|
commandLabel,
|
|
77665
77709
|
vars: varOverrides
|
|
77666
77710
|
});
|
|
77667
77711
|
if (plan.kind === "refusal") return yield* fail$7(new Error(plan.message));
|
|
77668
77712
|
out.write(landingScript(ScriptSurface.render(plan.steps)));
|
|
77669
77713
|
});
|
|
77714
|
+
const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
|
|
77715
|
+
/**
|
|
77716
|
+
* `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
|
|
77717
|
+
* process settings its `vars` derive from the positional args. A wrong name or
|
|
77718
|
+
* arity is a usage error naming what would have worked.
|
|
77719
|
+
*/
|
|
77720
|
+
const runDoorCommand = (name, args, out) => gen(function* () {
|
|
77721
|
+
const config = yield* (yield* ConfigService).load;
|
|
77722
|
+
const door = config.doors.get(name);
|
|
77723
|
+
if (door === void 0) {
|
|
77724
|
+
const known = [...config.doors];
|
|
77725
|
+
return yield* fail$7(new GtdUsageError(`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`));
|
|
77726
|
+
}
|
|
77727
|
+
const declared = door.args ?? [];
|
|
77728
|
+
const required = declared.filter((arg) => arg.optional !== true).length;
|
|
77729
|
+
if (args.length < required || args.length > declared.length) return yield* fail$7(new GtdUsageError(`gtd door ${name}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd door ${doorSynopsis(name, door)}`));
|
|
77730
|
+
const named = {};
|
|
77731
|
+
declared.forEach((arg, i) => {
|
|
77732
|
+
named[arg.name] = args[i];
|
|
77733
|
+
});
|
|
77734
|
+
const vars = yield* try_({
|
|
77735
|
+
try: () => door.vars?.(named) ?? {},
|
|
77736
|
+
catch: (e) => /* @__PURE__ */ new Error(`gtd door ${name}: ${e instanceof Error ? e.message : String(e)}`)
|
|
77737
|
+
});
|
|
77738
|
+
yield* runStartCommand("human", door.workflow, { ...vars }, out, `gtd door ${name}`);
|
|
77739
|
+
});
|
|
77740
|
+
/** `gtd doors`: every door and its args, sorted by name — plain lines, or one JSON array. */
|
|
77741
|
+
const runDoorsCommand = (json, out) => gen(function* () {
|
|
77742
|
+
const doors = [...(yield* (yield* ConfigService).load).doors];
|
|
77743
|
+
if (json.kind === "off") {
|
|
77744
|
+
out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""));
|
|
77745
|
+
return;
|
|
77746
|
+
}
|
|
77747
|
+
const document = doors.map(([n, d]) => ({
|
|
77748
|
+
name: n,
|
|
77749
|
+
workflow: d.workflow,
|
|
77750
|
+
args: (d.args ?? []).map((arg) => ({
|
|
77751
|
+
name: arg.name,
|
|
77752
|
+
optional: arg.optional === true
|
|
77753
|
+
}))
|
|
77754
|
+
}));
|
|
77755
|
+
if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
|
|
77756
|
+
else yield* writeSelection(out, document, json.path);
|
|
77757
|
+
});
|
|
77670
77758
|
/**
|
|
77671
77759
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
77672
77760
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
@@ -77923,6 +78011,8 @@ const gatherBeatDocument = (rest, rendered) => gen(function* () {
|
|
|
77923
78011
|
}), statusOf({
|
|
77924
78012
|
rendered,
|
|
77925
78013
|
idle: restIsIdle(rest),
|
|
78014
|
+
initial: noProcessUnderway(rest),
|
|
78015
|
+
workflow: rest.workflowName,
|
|
77926
78016
|
log,
|
|
77927
78017
|
changes: computeStatusChanges(rest.changes),
|
|
77928
78018
|
next: yield* computeNextMatch(rest),
|
|
@@ -78009,7 +78099,9 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
78009
78099
|
...command.cost !== void 0 ? { cost: command.cost } : {},
|
|
78010
78100
|
...command.model !== void 0 ? { model: command.model } : {}
|
|
78011
78101
|
}, json, out);
|
|
78012
|
-
case "
|
|
78102
|
+
case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
|
|
78103
|
+
case "door": return runDoorCommand(command.name, command.args, out);
|
|
78104
|
+
case "doors": return runDoorsCommand(json, out);
|
|
78013
78105
|
case "abandon": return runAbandonCommand(out);
|
|
78014
78106
|
case "restore": return runRestoreCommand(out);
|
|
78015
78107
|
case "next": return runNextCommand(json, out);
|
|
@@ -78073,12 +78165,12 @@ const FLAGS = [
|
|
|
78073
78165
|
name: "--json",
|
|
78074
78166
|
arity: "optional",
|
|
78075
78167
|
repeatable: false,
|
|
78076
|
-
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer",
|
|
78168
|
+
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors",
|
|
78077
78169
|
decode: ([raw]) => right(raw ?? ""),
|
|
78078
|
-
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
78170
|
+
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
78079
78171
|
valueHint: "<path>",
|
|
78080
78172
|
help: [
|
|
78081
|
-
"(gtd next/gtd land/gtd judge/gtd judge answer only) output",
|
|
78173
|
+
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors only) output",
|
|
78082
78174
|
"structured JSON. Bare --json prints the whole document;",
|
|
78083
78175
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
78084
78176
|
"kind, content, session.id) prints just that value: a",
|
|
@@ -78177,7 +78269,7 @@ const FLAGS = [
|
|
|
78177
78269
|
repeatable: false,
|
|
78178
78270
|
scope: (kind) => kind === "land",
|
|
78179
78271
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
78180
|
-
scopeError: "gtd: --cost is only valid for `gtd land` —
|
|
78272
|
+
scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
|
|
78181
78273
|
valueHint: "<n>",
|
|
78182
78274
|
help: ["(gtd land only) record the invocation's token cost"]
|
|
78183
78275
|
},
|
|
@@ -78196,24 +78288,20 @@ const FLAGS = [
|
|
|
78196
78288
|
]
|
|
78197
78289
|
},
|
|
78198
78290
|
{
|
|
78199
|
-
name: "--
|
|
78291
|
+
name: "--workflow",
|
|
78200
78292
|
arity: 1,
|
|
78201
78293
|
repeatable: false,
|
|
78202
|
-
scope: (kind) => kind === "
|
|
78294
|
+
scope: (kind) => kind === "start",
|
|
78203
78295
|
decode: ([raw]) => right(raw ?? ""),
|
|
78204
|
-
scopeError: "gtd: --
|
|
78205
|
-
valueHint: "<
|
|
78206
|
-
help: [
|
|
78207
|
-
"(with no command at all) start a brand new process,",
|
|
78208
|
-
"handing <state> to the workflow as its entry —",
|
|
78209
|
-
"authenticated as human"
|
|
78210
|
-
]
|
|
78296
|
+
scopeError: "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` form; landing and starting are different verbs",
|
|
78297
|
+
valueHint: "<name>",
|
|
78298
|
+
help: ["(with no command at all) start a brand new process on", "the named workflow — authenticated as human"]
|
|
78211
78299
|
},
|
|
78212
78300
|
{
|
|
78213
78301
|
name: "--var",
|
|
78214
78302
|
arity: 1,
|
|
78215
78303
|
repeatable: true,
|
|
78216
|
-
scope: (kind) => kind === "
|
|
78304
|
+
scope: (kind) => kind === "start",
|
|
78217
78305
|
decode: (raws) => {
|
|
78218
78306
|
const vars = {};
|
|
78219
78307
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -78230,10 +78318,10 @@ const FLAGS = [
|
|
|
78230
78318
|
}
|
|
78231
78319
|
return right(vars);
|
|
78232
78320
|
},
|
|
78233
|
-
scopeError: "gtd: --var requires --
|
|
78321
|
+
scopeError: "gtd: --var requires --workflow",
|
|
78234
78322
|
valueHint: "<name>=<value>",
|
|
78235
78323
|
help: [
|
|
78236
|
-
"(with --
|
|
78324
|
+
"(with --workflow; repeatable) pin a process setting for the",
|
|
78237
78325
|
"new process; the name must already be declared by the",
|
|
78238
78326
|
"workflow's defaults or the .gtdrc vars: (environment",
|
|
78239
78327
|
"settings are not pinnable)"
|
|
@@ -78516,6 +78604,34 @@ const COMMAND_ROWS = [
|
|
|
78516
78604
|
"Refuses (exit 1) when no process is underway."
|
|
78517
78605
|
]
|
|
78518
78606
|
},
|
|
78607
|
+
{
|
|
78608
|
+
token: "door",
|
|
78609
|
+
kind: "door",
|
|
78610
|
+
arity: {
|
|
78611
|
+
name: "name",
|
|
78612
|
+
variadic: "args"
|
|
78613
|
+
},
|
|
78614
|
+
details: [
|
|
78615
|
+
"Start a process through a named shortcut, mapping the",
|
|
78616
|
+
"positional args to that workflow's process settings — the",
|
|
78617
|
+
"same start `gtd --workflow` makes, printed the same way.",
|
|
78618
|
+
"Doors come from the config's `doors` export; the bundled",
|
|
78619
|
+
"ones are `fix` and `review [base]`. An unknown door, or too",
|
|
78620
|
+
"few or too many args, is a usage error (exit 2) naming the",
|
|
78621
|
+
"doors or the door's synopsis"
|
|
78622
|
+
]
|
|
78623
|
+
},
|
|
78624
|
+
{
|
|
78625
|
+
token: "doors",
|
|
78626
|
+
kind: "doors",
|
|
78627
|
+
arity: "none",
|
|
78628
|
+
details: [
|
|
78629
|
+
"List every door, sorted: one `name [args] → workflow` line",
|
|
78630
|
+
"each. --json prints one JSON array of { name, workflow,",
|
|
78631
|
+
"args: [{ name, optional }] } instead; --json=<path> reads one",
|
|
78632
|
+
"value off it. Writes nothing"
|
|
78633
|
+
]
|
|
78634
|
+
},
|
|
78519
78635
|
{
|
|
78520
78636
|
token: "judge",
|
|
78521
78637
|
kind: "judge",
|
|
@@ -78571,23 +78687,23 @@ const renderBlock = (header, lines) => {
|
|
|
78571
78687
|
const headerCell = ` ${header}`;
|
|
78572
78688
|
return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
|
|
78573
78689
|
};
|
|
78574
|
-
const
|
|
78690
|
+
const START_SHORT_FORM = renderBlock("(no command) --workflow <name>", ["Starts a new process authenticated as human, e.g.", "'gtd --workflow <name>'"]);
|
|
78575
78691
|
const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
|
|
78576
78692
|
const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
|
|
78577
|
-
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--
|
|
78693
|
+
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--workflow", "--var"]);
|
|
78578
78694
|
const flagHeader = (row) => {
|
|
78579
78695
|
if (row.valueHint === "") return row.name;
|
|
78580
78696
|
return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
|
|
78581
78697
|
};
|
|
78582
78698
|
const renderHelp = () => {
|
|
78583
78699
|
const commandBlocks = COMMAND_ROWS.map((row) => {
|
|
78584
|
-
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : `${row.token} <${row.arity.name}>`;
|
|
78700
|
+
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : `${row.token} <${row.arity.name}>`;
|
|
78585
78701
|
return renderBlock(header, row.details);
|
|
78586
78702
|
});
|
|
78587
78703
|
return `Usage: gtd [command] [options]\n\nCommands:\n${[
|
|
78588
78704
|
commandBlocks[0],
|
|
78589
78705
|
commandBlocks[1],
|
|
78590
|
-
|
|
78706
|
+
START_SHORT_FORM,
|
|
78591
78707
|
...commandBlocks.slice(2),
|
|
78592
78708
|
VERSION_BLOCK,
|
|
78593
78709
|
HELP_BLOCK
|
|
@@ -78692,14 +78808,14 @@ const conflictViolation = (present) => {
|
|
|
78692
78808
|
};
|
|
78693
78809
|
const arityError = (cmd, rest, arity) => {
|
|
78694
78810
|
if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
|
|
78695
|
-
if ("names" in arity)
|
|
78696
|
-
const [first, second] = arity.names;
|
|
78697
|
-
if (rest.length < 2) return rest.length === 0 ? `gtd ${cmd}: missing ${first} and ${second} arguments` : `gtd ${cmd}: missing ${second} argument`;
|
|
78698
|
-
if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
|
|
78699
|
-
return;
|
|
78700
|
-
}
|
|
78811
|
+
if ("names" in arity) return pairArityError(cmd, rest, arity.names);
|
|
78701
78812
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
78702
|
-
if (rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
|
|
78813
|
+
if (!("variadic" in arity) && rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
|
|
78814
|
+
};
|
|
78815
|
+
const pairArityError = (cmd, rest, [first, second]) => {
|
|
78816
|
+
if (rest.length === 0) return `gtd ${cmd}: missing ${first} and ${second} arguments`;
|
|
78817
|
+
if (rest.length === 1) return `gtd ${cmd}: missing ${second} argument`;
|
|
78818
|
+
if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
|
|
78703
78819
|
};
|
|
78704
78820
|
/** Decode every present flag into a bag of values, or the first decode error encountered. */
|
|
78705
78821
|
const decodeFlags = (byFlag) => {
|
|
@@ -78733,8 +78849,8 @@ const parseArgv = (argv) => {
|
|
|
78733
78849
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
|
|
78734
78850
|
const { positionals, byFlag, present, jsonSeen } = tokenized;
|
|
78735
78851
|
const first = positionals[0];
|
|
78736
|
-
const
|
|
78737
|
-
const
|
|
78852
|
+
const workflowRaw = byFlag.get("--workflow")?.[0];
|
|
78853
|
+
const workflowPresent = workflowRaw !== void 0;
|
|
78738
78854
|
if (first === "version") return {
|
|
78739
78855
|
kind: "output",
|
|
78740
78856
|
stdout: `${GTD_VERSION}\n`
|
|
@@ -78745,17 +78861,17 @@ const parseArgv = (argv) => {
|
|
|
78745
78861
|
};
|
|
78746
78862
|
const row = first === void 0 ? void 0 : commandByToken(first);
|
|
78747
78863
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
|
|
78748
|
-
const
|
|
78864
|
+
const selectsStart = workflowPresent && first === void 0;
|
|
78749
78865
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run";
|
|
78750
|
-
const kind =
|
|
78751
|
-
if (row === void 0 && !
|
|
78866
|
+
const kind = selectsStart ? "start" : judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row?.kind;
|
|
78867
|
+
if (row === void 0 && !selectsStart) {
|
|
78752
78868
|
const violation = scopeViolation(void 0, present);
|
|
78753
78869
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
78754
78870
|
if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; run `gtd install`, or copy one from https://github.com/pmelab/gtd/blob/main/docs/driver.md's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
|
|
78755
78871
|
return usagePlan(`unknown command '${first}'`, jsonSeen);
|
|
78756
78872
|
}
|
|
78757
78873
|
const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun ? 2 : 1);
|
|
78758
|
-
if (kind === "
|
|
78874
|
+
if (kind === "start" && first === void 0) {
|
|
78759
78875
|
if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
|
|
78760
78876
|
} else {
|
|
78761
78877
|
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(first, restPositionals, row.arity);
|
|
@@ -78782,14 +78898,14 @@ const parseArgv = (argv) => {
|
|
|
78782
78898
|
verbose
|
|
78783
78899
|
};
|
|
78784
78900
|
}
|
|
78785
|
-
if (kind === "
|
|
78786
|
-
const label = `gtd --
|
|
78901
|
+
if (kind === "start") {
|
|
78902
|
+
const label = `gtd --workflow ${workflowRaw}`;
|
|
78787
78903
|
return {
|
|
78788
78904
|
kind: "command",
|
|
78789
78905
|
command: {
|
|
78790
|
-
kind: "
|
|
78906
|
+
kind: "start",
|
|
78791
78907
|
actor: "human",
|
|
78792
|
-
|
|
78908
|
+
workflow: workflowRaw,
|
|
78793
78909
|
vars: bag["--var"] ?? {},
|
|
78794
78910
|
label
|
|
78795
78911
|
},
|
|
@@ -78797,6 +78913,16 @@ const parseArgv = (argv) => {
|
|
|
78797
78913
|
verbose
|
|
78798
78914
|
};
|
|
78799
78915
|
}
|
|
78916
|
+
if (kind === "door") return {
|
|
78917
|
+
kind: "command",
|
|
78918
|
+
command: {
|
|
78919
|
+
kind: "door",
|
|
78920
|
+
name: restPositionals[0],
|
|
78921
|
+
args: restPositionals.slice(1)
|
|
78922
|
+
},
|
|
78923
|
+
json,
|
|
78924
|
+
verbose
|
|
78925
|
+
};
|
|
78800
78926
|
if (kind === "ui") return {
|
|
78801
78927
|
kind: "command",
|
|
78802
78928
|
command: {
|