@pmelab/gtd 1.8.0 → 1.9.1
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/README.md +121 -51
- package/dist/gtd.bundle.mjs +110 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,14 @@ No config file, no setup subcommand.
|
|
|
50
50
|
|
|
51
51
|
Run `gtd` from your repository's working directory — it prints the next prompt
|
|
52
52
|
to stdout. It takes **no ref argument** — the review base is always
|
|
53
|
-
auto-computed.
|
|
53
|
+
auto-computed. The one exception is `gtd review <target>`: an explicit,
|
|
54
|
+
on-demand human review against a chosen base (see
|
|
55
|
+
[Review subcommand](#review-subcommand) below).
|
|
56
|
+
|
|
57
|
+
On an idle tree outside a process (no steering files, nothing reviewable), `gtd`
|
|
58
|
+
now runs a **health check** instead of stopping immediately — `gtd review` is
|
|
59
|
+
the only way to start an ad-hoc review when the automatic review base yields no
|
|
60
|
+
diff.
|
|
54
61
|
|
|
55
62
|
## JSON output mode
|
|
56
63
|
|
|
@@ -181,7 +188,8 @@ The complete set:
|
|
|
181
188
|
`gtd: new task` · `gtd: grilling` · `gtd: grilled` · `gtd: planning` ·
|
|
182
189
|
`gtd: building` · `gtd: errors` · `gtd: feedback` · `gtd: fixing` ·
|
|
183
190
|
`gtd: package done` · `gtd: awaiting review` · `gtd: done` · `gtd: health-check`
|
|
184
|
-
· `gtd: health-fix` — plus the hand-made `gtd: transport`
|
|
191
|
+
· `gtd: health-fix` · `gtd: reviewing` — plus the hand-made `gtd: transport`
|
|
192
|
+
(see below).
|
|
185
193
|
|
|
186
194
|
The last commit subject is bucketed two ways:
|
|
187
195
|
|
|
@@ -233,8 +241,8 @@ flowchart TD
|
|
|
233
241
|
P2 -->|"empty = approval"| Close["Close package — rm pkg dir, gtd: package done"]:::edge
|
|
234
242
|
P2 -->|"non-empty"| Fixing["Fixing — rm FEEDBACK, fixer agent"]:::agent
|
|
235
243
|
P2 -->|absent| P2b{"HEALTH.md?"}
|
|
236
|
-
P2b -->|"present"| HealthFix["Health Fixing — rm HEALTH.md, gtd: health-
|
|
237
|
-
HealthFix -.->|"re-resolve"| HealthCheck
|
|
244
|
+
P2b -->|"present"| HealthFix["Health Fixing — rm HEALTH.md, gtd: health-check, fixer agent (leaves edits uncommitted)"]:::agent
|
|
245
|
+
HealthFix -.->|"next gtd run: dirty health HEAD → commit gtd: health-fix, re-resolve"| HealthCheck
|
|
238
246
|
P2b -->|absent| P3{".gtd/?"}
|
|
239
247
|
P3 -->|"modified"| Planning["Planning — gtd: planning"]:::agent
|
|
240
248
|
P3 -->|"code dirty / resume / no-op fixer"| Testing["Testing — gtd: building, run tests"]:::edge
|
|
@@ -322,27 +330,27 @@ Each state has a **condition** (when it wins), a deterministic **action**, the
|
|
|
322
330
|
human; **edge-only** states render no prompt at all — the driver performs their
|
|
323
331
|
action and re-resolves silently.
|
|
324
332
|
|
|
325
|
-
| State | Kind | Wins when
|
|
326
|
-
| ------------------ | -------------------------------- |
|
|
327
|
-
| **Transport** | edge-only, auto | HEAD `gtd: transport` (hand-made handoff commit)
|
|
328
|
-
| **Escalate** | STOP | ERRORS.md present
|
|
329
|
-
| **Fixing** | agent, auto | non-empty FEEDBACK.md present
|
|
330
|
-
| **Health Fixing** | agent, auto | HEALTH.md present (no `.gtd`, REVIEW.md, or FEEDBACK.md)
|
|
331
|
-
| **Close package** | edge-only, auto | empty FEEDBACK.md present (clean review); also reached from Agentic Review force-approve
|
|
332
|
-
| **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning`
|
|
333
|
-
| **Testing** | edge-only, auto | `.gtd` present, no FEEDBACK/ERRORS, and a reason to test: code changes, a pending ERRORS.md deletion (human resume), or a clean tree under HEAD `gtd: fixing` (no-op fixer)
|
|
334
|
-
| **Building** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: planning` or `gtd: package done`
|
|
335
|
-
| **Agentic Review** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: building`
|
|
336
|
-
| **Done** | edge-only, auto | REVIEW.md committed + clean tree, **or** committed + checkbox-only edits (only `- [ ]`→`- [x]` flips in REVIEW.md = approval)
|
|
337
|
-
| **Squashing** | agent, auto | no steering files, HEAD `gtd: done` or green Health check with ≥1 `gtd: health-fix`, `squash` enabled, squash base present, no unrelated code dirty (a lone untracked `SQUASH_MSG.md` is allowed)
|
|
338
|
-
| **Accept Review** | edge-only, auto | REVIEW.md committed + pending **non-checkbox** edits (human annotated REVIEW.md with comments / edited code)
|
|
339
|
-
| **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean)
|
|
340
|
-
| **New Feature** | edge-only, auto | boundary HEAD + pending changes (code and/or a new uncommitted TODO.md), **or** HEAD `gtd: new task` + clean tree (lost-seed regen)
|
|
341
|
-
| **Grilling** | agent (iterate) / STOP (answers) | TODO.md present, not New Feature
|
|
342
|
-
| **Grilled** | agent, auto | TODO.md present, no markers, clean tree
|
|
343
|
-
| **Clean** | agent | no steering files, clean tree, boundary or `gtd: package done` HEAD, and the review base yields a **non-empty** diff
|
|
344
|
-
| **Health check** | edge-only, auto | no steering files,
|
|
345
|
-
| **Idle** | STOP | no steering files, clean tree, health check passed with no prior `gtd: health-fix` commits, and nothing to review
|
|
333
|
+
| State | Kind | Wins when | Action & commit | Advances to |
|
|
334
|
+
| ------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
335
|
+
| **Transport** | edge-only, auto | HEAD `gtd: transport` (hand-made handoff commit) | mixed-reset HEAD (`git reset HEAD~1`), keep work in tree; **no commit** | re-derive from the restored tree |
|
|
336
|
+
| **Escalate** | STOP | ERRORS.md present | none | held until the human deletes ERRORS.md |
|
|
337
|
+
| **Fixing** | agent, auto | non-empty FEEDBACK.md present | inline FEEDBACK into the prompt, remove FEEDBACK.md; commit its removal `gtd: fixing` (FEEDBACK was committed by Testing) or `gtd: feedback` (uncommitted, written by Agentic Review) | fixer edits → Testing |
|
|
338
|
+
| **Health Fixing** | agent, auto | HEALTH.md present (no `.gtd`, REVIEW.md, or FEEDBACK.md) | read HEALTH.md into the prompt, commit its removal `gtd: health-check` (HEALTH.md removed so next resolve re-enters `resolveCleanOrIdle`); fixer leaves edits **uncommitted** | fixer edits uncommitted → next `gtd` invocation commits them `gtd: health-fix` → Health check |
|
|
339
|
+
| **Close package** | edge-only, auto | empty FEEDBACK.md present (clean review); also reached from Agentic Review force-approve | rm FEEDBACK.md, rm the first (finished) package dir (+ the now-empty `.gtd/`); commit `gtd: package done` | more packages → Building; `.gtd` gone → Clean |
|
|
340
|
+
| **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning` | commit the `.gtd/` changes `gtd: planning` | continue decomposing, else → Building |
|
|
341
|
+
| **Testing** | edge-only, auto | `.gtd` present, no FEEDBACK/ERRORS, and a reason to test: code changes, a pending ERRORS.md deletion (human resume), or a clean tree under HEAD `gtd: fixing` (no-op fixer) | commit pending tree `gtd: building`, run `testCommand`; green → proceed; red → write FEEDBACK (below cap) or ERRORS (at cap), commit `gtd: errors`; if captured output is empty/whitespace, a sentinel string is written so the file is never empty (empty FEEDBACK remains reserved for agentic-review approval) | green → Agentic Review; FEEDBACK → Fixing; ERRORS → Escalate |
|
|
342
|
+
| **Building** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: planning` or `gtd: package done` | if HEAD `gtd: planning` and TODO.md present, delete TODO.md and commit (prefix unchanged, fires once); select the first package, inline its tasks; agent leaves work **uncommitted** | Testing |
|
|
343
|
+
| **Agentic Review** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: building` | reviewer writes FEEDBACK.md (empty = approval), uncommitted — **unless** force-approved (kill-switch off or review-fix threshold hit), which routes straight to Close package | empty FEEDBACK → Close package; non-empty → Fixing |
|
|
344
|
+
| **Done** | edge-only, auto | REVIEW.md committed + clean tree, **or** committed + checkbox-only edits (only `- [ ]`→`- [x]` flips in REVIEW.md = approval) | rm REVIEW.md, commit `gtd: done` | Squashing (if enabled) or Idle |
|
|
345
|
+
| **Squashing** | agent, auto | no steering files, HEAD `gtd: done` or green Health check with ≥1 `gtd: health-fix`, `squash` enabled, squash base present, no unrelated code dirty (a lone untracked `SQUASH_MSG.md` is allowed) | agent authors a conventional-commits message from the full `<base>..HEAD` diff, then runs `git reset --soft <base>` + `git commit` — collapses all intermediate `gtd: *` commits (including any interleaved non-gtd commits) into one; **gtd then STOPs** — post-squash review fires only on the next manual `gtd` run | Idle (STOP) |
|
|
346
|
+
| **Accept Review** | edge-only, auto | REVIEW.md committed + pending **non-checkbox** edits (human annotated REVIEW.md with comments / edited code) | seed TODO.md from the changeset, `git checkout` to discard the code edits, rm REVIEW.md; **all uncommitted** | Grilling |
|
|
347
|
+
| **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean) | commit REVIEW.md `gtd: awaiting review` | Done (auto, same run) |
|
|
348
|
+
| **New Feature** | edge-only, auto | boundary HEAD + pending changes (code and/or a new uncommitted TODO.md), **or** HEAD `gtd: new task` + clean tree (lost-seed regen) | commit the raw input verbatim `gtd: new task` (unless already there), `git revert --no-commit` it back to a clean baseline, seed TODO.md from that diff — revert + seed left **uncommitted** | Grilling |
|
|
349
|
+
| **Grilling** | agent (iterate) / STOP (answers) | TODO.md present, not New Feature | commit pending edits `gtd: grilling`. Open-question markers present → STOP for the human to answer inline; no markers but dirty → grilling agent iterates | converge (no markers, clean tree) → Grilled |
|
|
350
|
+
| **Grilled** | agent, auto | TODO.md present, no markers, clean tree | commit pending `gtd: grilled` | decompose into `.gtd/` → Planning |
|
|
351
|
+
| **Clean** | agent | no steering files, clean tree, boundary or `gtd: package done` HEAD, and the review base yields a **non-empty** diff | compute the review base (three rules — see below); agent writes REVIEW.md **uncommitted** with `# Review: <short-hash>` heading, `<!-- base: <full-hash> -->` marker, and per-hunk `- [ ]` checkboxes (ticking them signals approval → Done) | Await Review |
|
|
352
|
+
| **Health check** | edge-only, auto | no steering files, outside a process with no reviewable diff (any branch) — the `!reviewable` case from rule 8. Two entry points: (a) clean tree under a boundary or `gtd: package done` HEAD; (b) **dirty tree under a `gtd: health-check` or `gtd: health-fix` HEAD with `!pendingErrorsDeletion`** (the fixer's uncommitted edits — commits them `gtd: health-fix` and re-runs the health check within the same `gtd` invocation; NOT corruption). | run `testCommand` (entry point a); or commit pending edits `gtd: health-fix`, then run `testCommand` (entry point b). green + no prior `gtd: health-fix` → Idle (no commit); red below `fixAttemptCap` → write HEALTH.md, commit `gtd: health-check` → Health Fixing; red at cap → write ERRORS.md, commit `gtd: health-check` → Escalate; green + ≥1 `gtd: health-fix` → Squashing (if `squash`) or Idle | green → Idle or Squashing; red below cap → Health Fixing; red at cap → Escalate |
|
|
353
|
+
| **Idle** | STOP | no steering files, clean tree, health check passed with no prior `gtd: health-fix` commits, and nothing to review | none (no commit — the health check edge terminates the driver loop directly) | — |
|
|
346
354
|
|
|
347
355
|
Every prompt also embeds the current `git diff HEAD` (untracked files included)
|
|
348
356
|
inline, plus the last commit subject and working-tree status, so the agent has
|
|
@@ -412,17 +420,25 @@ files, `gtd` runs `testCommand` instead of stopping. This reuses `fixAttemptCap`
|
|
|
412
420
|
(default 3) and `squash` — no new config keys are introduced.
|
|
413
421
|
|
|
414
422
|
```
|
|
415
|
-
Idle path → Health check(red) → Health Fixing →
|
|
416
|
-
│
|
|
417
|
-
|
|
423
|
+
Idle path → Health check(red) → Health Fixing → [fixer edits, uncommitted]
|
|
424
|
+
│ │
|
|
425
|
+
│ next gtd run: commit gtd: health-fix
|
|
426
|
+
│ │
|
|
427
|
+
│ Health check(red) → …
|
|
428
|
+
│ │
|
|
429
|
+
│ Health check(green)
|
|
430
|
+
│ │
|
|
431
|
+
└── below cap: HEALTH.md, gtd: health-check └── green + ≥1 health-fix → Squashing (if squash enabled) → Idle
|
|
418
432
|
└── at/over the cap: ERRORS.md, gtd: health-check → Escalate
|
|
419
|
-
green + ≥1 health-fix → Squashing (if squash enabled) → Idle
|
|
420
433
|
```
|
|
421
434
|
|
|
422
435
|
- **green, no prior `gtd: health-fix`** → Idle immediately (no commit).
|
|
423
436
|
- **red, below `fixAttemptCap`** → write test output to HEALTH.md, commit
|
|
424
|
-
`gtd: health-check` → **Health Fixing** agent fixes
|
|
425
|
-
`gtd
|
|
437
|
+
`gtd: health-check` → **Health Fixing** agent fixes the code and leaves its
|
|
438
|
+
edits **uncommitted**. The next `gtd` invocation detects the dirty tree under
|
|
439
|
+
the `gtd: health-check` HEAD, commits the fixer's edits as `gtd: health-fix`
|
|
440
|
+
(an edge-only `health-check` state with a `commitPending` action), and
|
|
441
|
+
immediately re-runs the health check within that same invocation.
|
|
426
442
|
- **red, at or over `fixAttemptCap`** → write test output to ERRORS.md, commit
|
|
427
443
|
`gtd: health-check` → **Escalate** (human gate). Delete ERRORS.md to reset the
|
|
428
444
|
budget and resume.
|
|
@@ -494,9 +510,9 @@ loop before the next one starts.
|
|
|
494
510
|
(`gtd: done`) → **Squashing** → **Idle**. The Squashing agent authors a
|
|
495
511
|
conventional-commits message from the full process diff and squashes all
|
|
496
512
|
intermediate `gtd: *` commits into one with `git reset --soft <base>` +
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
513
|
+
`git commit`, then **gtd STOPs**. The base is the parent of the current
|
|
514
|
+
cycle's start marker **nearest to HEAD** (the last `gtd: new task`; for
|
|
515
|
+
legacy cycles the last contiguous `gtd: grilling` run), and on a feature
|
|
500
516
|
branch it never reaches below the merge-base with the default branch — stray
|
|
501
517
|
markers left behind by older squashes can never drag the squash into
|
|
502
518
|
previously shipped features. Post-squash review does not fire automatically —
|
|
@@ -505,20 +521,13 @@ loop before the next one starts.
|
|
|
505
521
|
has no unrelated code dirty — a lone untracked `SQUASH_MSG.md` is tolerated
|
|
506
522
|
and deleted before the squash commit. If unrelated code is dirty at
|
|
507
523
|
`gtd: done`, gtd routes to **New Feature** instead. Set `squash: false` in
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
> > > > > > > straight to Idle. Checking off REVIEW.md checkboxes (`- [ ]` →
|
|
516
|
-
> > > > > > > `- [x]`) also counts as approval and routes to **Done** — they
|
|
517
|
-
> > > > > > > are navigation aids, not feedback. Only **non-checkbox** edits
|
|
518
|
-
> > > > > > > (code changes, inline comments, textual annotations in
|
|
519
|
-
> > > > > > > REVIEW.md) trigger **Accept Review**, which seeds a fresh
|
|
520
|
-
> > > > > > > `TODO.md` from your feedback, discards your code edits, removes
|
|
521
|
-
> > > > > > > `REVIEW.md`, and re-enters Grilling — the loop starts over.
|
|
524
|
+
`.gtdrc` to skip squashing and go straight to Idle. Checking off REVIEW.md
|
|
525
|
+
checkboxes (`- [ ]` → `- [x]`) also counts as approval and routes to **Done**
|
|
526
|
+
— they are navigation aids, not feedback. Only **non-checkbox** edits (code
|
|
527
|
+
changes, inline comments, textual annotations in REVIEW.md) trigger **Accept
|
|
528
|
+
Review**, which seeds a fresh `TODO.md` from your feedback, discards your
|
|
529
|
+
code edits, removes `REVIEW.md`, and re-enters Grilling — the loop starts
|
|
530
|
+
over.
|
|
522
531
|
|
|
523
532
|
## Configuration
|
|
524
533
|
|
|
@@ -738,10 +747,71 @@ When there are genuinely no open questions left, the agent writes the sentinel
|
|
|
738
747
|
line `no open questions — run gtd to plan` and leaves **no** markers — a clean
|
|
739
748
|
tree with no markers is what advances the plan to **Grilled** and decomposition.
|
|
740
749
|
|
|
741
|
-
##
|
|
750
|
+
## CLI flags
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
Usage: gtd [command] [options]
|
|
754
|
+
|
|
755
|
+
Commands:
|
|
756
|
+
(default) Run the gtd driver loop — detect state, emit next prompt
|
|
757
|
+
format <file> Format a markdown file in place
|
|
758
|
+
review <target> Ad-hoc human review against a git ref or branch
|
|
759
|
+
|
|
760
|
+
Options:
|
|
761
|
+
--json Output structured JSON instead of plain text
|
|
762
|
+
--verbose Show verbose output (thinking deltas, tool events)
|
|
763
|
+
--debug Show debug-level internal information
|
|
764
|
+
--version, -v Print version and exit
|
|
765
|
+
--help, -h Print this help and exit
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`--version` (`-v`) and `--help` (`-h`) short-circuit before any git or
|
|
769
|
+
repository-state work — they run outside a repo and in any repo state.
|
|
770
|
+
|
|
771
|
+
## Subcommands
|
|
772
|
+
|
|
773
|
+
gtd ships two subcommands: `format` and `review`.
|
|
774
|
+
|
|
775
|
+
## Review subcommand
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
gtd review <target>
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
Starts an explicit, on-demand human review of the diff between HEAD and
|
|
782
|
+
`merge-base(<target>, HEAD)`. Use this when the automatic review base yields no
|
|
783
|
+
diff (idle tree outside a process) or when you want to review against a specific
|
|
784
|
+
base regardless of workflow state.
|
|
785
|
+
|
|
786
|
+
### Flow
|
|
787
|
+
|
|
788
|
+
1. `gtd review <target>` computes the diff HEAD adds over
|
|
789
|
+
`merge-base(<target>, HEAD)`.
|
|
790
|
+
2. The edge writes an empty anchor commit `gtd: reviewing` (no content — just
|
|
791
|
+
the marker).
|
|
792
|
+
3. The **Clean** state writes `REVIEW.md` with the computed diff and emits the
|
|
793
|
+
normal review prompt. `--json` is accepted and enables auto-advance mode
|
|
794
|
+
(same behavior as the default command).
|
|
795
|
+
4. The normal loop then drives: **Await Review** → **Done** → **Squashing**,
|
|
796
|
+
collapsing back to the `gtd: reviewing` anchor commit.
|
|
797
|
+
|
|
798
|
+
### Error handling
|
|
799
|
+
|
|
800
|
+
All errors exit with **code 1** and write a message to **stderr**:
|
|
801
|
+
|
|
802
|
+
- **Missing target** — `gtd review` with no argument:
|
|
803
|
+
`gtd review: missing target argument`
|
|
804
|
+
- **Extra arguments** — `gtd review main extra`:
|
|
805
|
+
`gtd review: too many arguments — expected one target, got: …`
|
|
806
|
+
- **Unresolvable ref** — the target cannot be resolved by git:
|
|
807
|
+
`gtd review: cannot resolve ref '<target>': <error message>`
|
|
808
|
+
- **Empty diff** — the merge-base diff between `<target>` and HEAD is empty
|
|
809
|
+
(nothing to review):
|
|
810
|
+
`gtd review: nothing to review (<target> diff is empty after filtering)`
|
|
811
|
+
|
|
812
|
+
## Format subcommand
|
|
742
813
|
|
|
743
|
-
gtd
|
|
744
|
-
markdown file in place:
|
|
814
|
+
`gtd format` formats a markdown file in place:
|
|
745
815
|
|
|
746
816
|
```bash
|
|
747
817
|
gtd format <file>
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -367980,6 +367980,9 @@ var TestRunner = class _TestRunner extends Context_exports.Tag("TestRunner")() {
|
|
|
367980
367980
|
);
|
|
367981
367981
|
};
|
|
367982
367982
|
|
|
367983
|
+
// src/program.ts
|
|
367984
|
+
import { createRequire as createRequire2 } from "module";
|
|
367985
|
+
|
|
367983
367986
|
// src/Events.ts
|
|
367984
367987
|
import { join as join11 } from "path";
|
|
367985
367988
|
|
|
@@ -368477,8 +368480,8 @@ var fixing_default = '<%~ include("@header") %>\n\n<%~ include("@context", { con
|
|
|
368477
368480
|
// src/prompts/agentic-review.md
|
|
368478
368481
|
var agentic_review_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nSpawn a **reviewing subagent** using model `<%= it.model %>`. The package\'s task spec\nfiles and its cumulative diff are found below. The subagent must:\n\n1. **Read the task spec files** \u2014 they define the acceptance criteria. Every\n requirement must be checked.\n2. **Read the package diff** \u2014 examine every hunk to determine whether the\n implementation satisfies the spec.\n3. **Always write `FEEDBACK.md`** in the repo root:\n - **Fully satisfies the spec** \u2192 write an **empty** `FEEDBACK.md` (zero bytes\n or whitespace only). This empty file is the **approval** signal.\n - **Does not fully satisfy** \u2192 write concrete, actionable findings anchored\n to specific file and symbol names, grouped under short headings, so the fix\n agent can act without re-reading the diff.\n4. **Do not edit source files and do not commit** \u2014 the reviewer only writes\n `FEEDBACK.md`, left uncommitted.\n<% if (it.context.packages[0]) { %>\n<%~ include("@package", { pkg: it.context.packages[0], fenceFor: it.fenceFor }) %>\n<% } %><% if (it.context.refDiff && it.context.refDiff.trim()) { %>\n<%~ include("@diff", { heading: "Package diff", diff: it.context.refDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
368479
368482
|
|
|
368480
|
-
// src/prompts/
|
|
368481
|
-
var
|
|
368483
|
+
// src/prompts/review.md
|
|
368484
|
+
var review_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nSpawn a **planning-model subagent** using model `<%= it.model %>` to author a\n`REVIEW.md` file that will help a human to review the changes. It must:\n\n1. **Read the diff inlined below** \u2014 extract the changed hunks with their file\n paths.\n2. **Group hunks semantically** \u2014 cluster hunks that serve the same logical\n concern (the same feature, refactor, or fix), even across files. Aim for the\n fewest chunks that keep the review navigable.\n3. **Write `REVIEW.md`** in the repo root in this format:\n\n ```markdown\n # Review: <short-hash>\n\n <!-- base: <full-hash> -->\n\n ## <Chunk Title>\n\n <What this chunk changes and why>\n\n - [ ] ./path/to/file.ts#42\n - [ ] ./path/to/file.ts#99\n\n ## <Another Chunk Title>\n\n <Explanation>\n\n - [ ] ./path/to/another.ts#1\n ```\n\n - `<short-hash>` is the first 7 characters of the review base SHA;\n `<full-hash>` is the full SHA. Both are read from the `Review base:` line /\n diff label in the prompt context.\n - Chunk titles are short imperative phrases (\u2264 6 words).\n - Explanations describe _what_ changed and _why_, not just where.\n - File pointers are relative, prefixed with `./`; the line numbers (`#42`)\n are creation-time hints that will drift \u2014 not authoritative.\n - Checkboxes (`- [ ]`) signal approval \u2014 ticking them (with no other edits)\n counts as approving the review (`gtd: done`). Only non-checkbox edits to\n `REVIEW.md` (or any code edits) are treated as a change-request.\n - The user checks off or edits items in place as they work through the\n review; there is no separate Resolved section.\n\n4. Leave `REVIEW.md` **uncommitted**, the outer loop will process it.\n<% if (it.context.refDiff && it.context.refDiff.trim()) { %>\n<%= it.context.reviewBase !== undefined ? "\\nReview base: " + it.context.reviewBase + "\\n" : "" %>\n<%~ include("@diff", { heading: "Changes to review (`git diff " + (it.context.reviewBase !== undefined ? it.context.reviewBase : "<base>") + " HEAD`)", diff: it.context.refDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
368482
368485
|
|
|
368483
368486
|
// src/prompts/squashing.md
|
|
368484
368487
|
var squashing_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nThe process is **approved and done**. Your job is to author a clean\nconventional-commits squash message and hand it off to the harness.\n\n### Step 1 \u2014 Extract decisions from grilling rounds\n\nScan the git history of recent `gtd: ...` commits. Look for changes to `TODO.md`\n\u2014 specifically the `## Captured input (grilling)` sections and any edits to plan\ntext. Extract **key decisions, trade-offs, and design choices** made during\ngrilling rounds. These will appear in the commit body so the history is\nself-documenting.\n\n### Step 2 \u2014 Draft the commit message\n\nDraft ONE conventional-commits message:\n\n```\ntype(scope): subject\n\nbody (explain the why \u2014 motivation, trade-offs, key decisions from grilling)\n```\n\n- **type**: `feat` / `fix` / `refactor` / `chore` / `docs` / `test`\n- **subject**: imperative mood, \u2264 72 characters, lowercase after the colon\n- **body**: include the important decisions / trade-offs from grilling sessions.\n Omit if there were no meaningful decisions to capture.\n\n### Step 3 \u2014 Write SQUASH_MSG.md and hand off\n\nWrite the commit message (plain text, no markdown wrapper) to `SQUASH_MSG.md` in\nthe repo root, then leave it uncommitted \u2014 the harness handles the squash commit\non the next cycle once it sees `SQUASH_MSG.md`.\n\n**Do not run `git reset --soft` or `git commit` yourself** \u2014 the harness handles\nthe squash commit.\n<% if (it.context.squashDiff && it.context.squashDiff.trim()) { %>\n<%= it.context.squashBase !== undefined ? "\\nSquash base: " + it.context.squashBase + "\\n" : "" %>\n<%~ include("@diff", { heading: "Full-process diff (`git diff " + (it.context.squashBase !== undefined ? it.context.squashBase : "<squashBase>") + " HEAD`)", diff: it.context.squashDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<%~ include(it.tail) %>\n';
|
|
@@ -368532,7 +368535,7 @@ eta.loadTemplate("@decompose", decompose_default);
|
|
|
368532
368535
|
eta.loadTemplate("@building", building_default);
|
|
368533
368536
|
eta.loadTemplate("@fixing", fixing_default);
|
|
368534
368537
|
eta.loadTemplate("@agentic-review", agentic_review_default);
|
|
368535
|
-
eta.loadTemplate("@
|
|
368538
|
+
eta.loadTemplate("@review", review_default);
|
|
368536
368539
|
eta.loadTemplate("@squashing", squashing_default);
|
|
368537
368540
|
eta.loadTemplate("@escalate", escalate_default);
|
|
368538
368541
|
eta.loadTemplate("@idle", idle_default);
|
|
@@ -368544,7 +368547,7 @@ var STATE_TEMPLATE = {
|
|
|
368544
368547
|
building: "@building",
|
|
368545
368548
|
fixing: "@fixing",
|
|
368546
368549
|
"agentic-review": "@agentic-review",
|
|
368547
|
-
clean: "@
|
|
368550
|
+
clean: "@review",
|
|
368548
368551
|
squashing: "@squashing",
|
|
368549
368552
|
escalate: "@escalate",
|
|
368550
368553
|
idle: "@idle",
|
|
@@ -385730,7 +385733,7 @@ function clean(original, cloned) {
|
|
|
385730
385733
|
}
|
|
385731
385734
|
return cloned;
|
|
385732
385735
|
}
|
|
385733
|
-
var
|
|
385736
|
+
var clean_default = clean;
|
|
385734
385737
|
function printFrontMatter({ node }) {
|
|
385735
385738
|
return node.raw;
|
|
385736
385739
|
}
|
|
@@ -385835,7 +385838,7 @@ function normalizePrinter(printer2) {
|
|
|
385835
385838
|
if (originalCleanFunction && frontMatterSupport.massageAstNode) {
|
|
385836
385839
|
massageAstNode = new Proxy(originalCleanFunction, {
|
|
385837
385840
|
apply(target, thisArgument, argumentsList) {
|
|
385838
|
-
|
|
385841
|
+
clean_default(...argumentsList);
|
|
385839
385842
|
return Reflect.apply(target, thisArgument, argumentsList);
|
|
385840
385843
|
}
|
|
385841
385844
|
});
|
|
@@ -388279,6 +388282,7 @@ var PACKAGE_DONE_SUBJECT = "gtd: package done";
|
|
|
388279
388282
|
var AWAITING_REVIEW_SUBJECT = "gtd: awaiting review";
|
|
388280
388283
|
var DONE_SUBJECT = "gtd: done";
|
|
388281
388284
|
var REVIEW_FEEDBACK_SUBJECT = "gtd: review feedback";
|
|
388285
|
+
var REVIEWING_SUBJECT = "gtd: reviewing";
|
|
388282
388286
|
var SQUASH_MSG_FILE = "SQUASH_MSG.md";
|
|
388283
388287
|
var STEERING_FILES = [
|
|
388284
388288
|
TODO_FILE,
|
|
@@ -388577,7 +388581,7 @@ var gatherEvents = () => (
|
|
|
388577
388581
|
const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
|
|
388578
388582
|
let startIdx = -1;
|
|
388579
388583
|
for (let i = squashCycle.length - 1; i >= 0; i--) {
|
|
388580
|
-
if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT) {
|
|
388584
|
+
if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT || subjectOf(squashCycle[i]) === REVIEWING_SUBJECT) {
|
|
388581
388585
|
startIdx = i;
|
|
388582
388586
|
break;
|
|
388583
388587
|
}
|
|
@@ -388684,6 +388688,15 @@ var captureAndRevert = (git, subject, onRegen) => Effect_exports.gen(function* (
|
|
|
388684
388688
|
yield* git.revertNoCommit("HEAD");
|
|
388685
388689
|
return captured;
|
|
388686
388690
|
});
|
|
388691
|
+
var reviewAgainst = (target) => Effect_exports.gen(function* () {
|
|
388692
|
+
const git = yield* GitService;
|
|
388693
|
+
const targetHash = yield* git.resolveRef(target);
|
|
388694
|
+
const mergeBase = yield* git.mergeBase(target, "HEAD");
|
|
388695
|
+
const mergeBaseHash = Option_exports.isNone(mergeBase) || mergeBase.value === targetHash ? targetHash : mergeBase.value;
|
|
388696
|
+
const refDiff = yield* git.diffRef(mergeBaseHash, WORKFLOW_FILE_EXCLUDES);
|
|
388697
|
+
if (refDiff.trim().length === 0) return void 0;
|
|
388698
|
+
return { reviewBase: mergeBaseHash, refDiff };
|
|
388699
|
+
});
|
|
388687
388700
|
var perform = (action) => (
|
|
388688
388701
|
// fallow-ignore-next-line complexity
|
|
388689
388702
|
Effect_exports.gen(function* () {
|
|
@@ -389097,6 +389110,14 @@ var resolveGrilling = (p4, counters, head7) => {
|
|
|
389097
389110
|
};
|
|
389098
389111
|
var resolveCleanOrIdle = (p4, counters, head7) => {
|
|
389099
389112
|
const isHealthHead = head7 === "gtd: health-check" || head7 === "gtd: health-fix";
|
|
389113
|
+
if (!p4.workingTreeClean && !p4.pendingErrorsDeletion && isHealthHead) {
|
|
389114
|
+
return {
|
|
389115
|
+
state: "health-check",
|
|
389116
|
+
autoAdvance: true,
|
|
389117
|
+
edgeAction: { kind: "commitPending", prefix: "gtd: health-fix" },
|
|
389118
|
+
context: buildContext(p4, counters)
|
|
389119
|
+
};
|
|
389120
|
+
}
|
|
389100
389121
|
if (!p4.workingTreeClean && !p4.pendingErrorsDeletion) return null;
|
|
389101
389122
|
if (!p4.workingTreeClean && p4.pendingErrorsDeletion && p4.gtdDirExists) return null;
|
|
389102
389123
|
if (!p4.workingTreeClean && p4.pendingErrorsDeletion && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
|
|
@@ -389198,6 +389219,20 @@ var resolve4 = (events) => {
|
|
|
389198
389219
|
if (p4.todoExists) return resolveGrilling(p4, counters, head7);
|
|
389199
389220
|
return resolveCleanOrIdle(p4, counters, head7) ?? corrupt();
|
|
389200
389221
|
};
|
|
389222
|
+
var cleanResult = (args2) => ({
|
|
389223
|
+
state: "clean",
|
|
389224
|
+
autoAdvance: args2.autoAdvance,
|
|
389225
|
+
context: {
|
|
389226
|
+
...buildContext(
|
|
389227
|
+
{
|
|
389228
|
+
...DEFAULT_PAYLOAD,
|
|
389229
|
+
reviewBase: args2.reviewBase,
|
|
389230
|
+
refDiff: args2.refDiff
|
|
389231
|
+
},
|
|
389232
|
+
{ testFixCount: 0, reviewFixCount: 0, healthFixCount: 0 }
|
|
389233
|
+
)
|
|
389234
|
+
}
|
|
389235
|
+
});
|
|
389201
389236
|
|
|
389202
389237
|
// src/State.ts
|
|
389203
389238
|
var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
|
|
@@ -389220,6 +389255,22 @@ var detect = () => Effect_exports.gen(function* () {
|
|
|
389220
389255
|
});
|
|
389221
389256
|
|
|
389222
389257
|
// src/program.ts
|
|
389258
|
+
var _require = createRequire2(import.meta.url);
|
|
389259
|
+
var GTD_VERSION = _require("../package.json").version;
|
|
389260
|
+
var HELP_TEXT = `Usage: gtd [command] [options]
|
|
389261
|
+
|
|
389262
|
+
Commands:
|
|
389263
|
+
(default) Run the gtd driver loop \u2014 detect state, emit next prompt
|
|
389264
|
+
format <file> Format a markdown file in place
|
|
389265
|
+
review <target> Ad-hoc human review against a git ref or branch
|
|
389266
|
+
|
|
389267
|
+
Options:
|
|
389268
|
+
--json Output structured JSON instead of plain text
|
|
389269
|
+
--verbose Show verbose output (thinking deltas, tool events)
|
|
389270
|
+
--debug Show debug-level internal information
|
|
389271
|
+
--version, -v Print version and exit
|
|
389272
|
+
--help, -h Print this help and exit
|
|
389273
|
+
`;
|
|
389223
389274
|
var IDLE_RESULT = {
|
|
389224
389275
|
state: "idle",
|
|
389225
389276
|
autoAdvance: false,
|
|
@@ -389240,6 +389291,14 @@ function makeProgram(opts = {}) {
|
|
|
389240
389291
|
const json2 = argv.includes("--json");
|
|
389241
389292
|
const positional = argv.slice(2).find((a5) => !a5.startsWith("--"));
|
|
389242
389293
|
return Effect_exports.gen(function* () {
|
|
389294
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
389295
|
+
write4(GTD_VERSION + "\n");
|
|
389296
|
+
return;
|
|
389297
|
+
}
|
|
389298
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
389299
|
+
write4(HELP_TEXT);
|
|
389300
|
+
return;
|
|
389301
|
+
}
|
|
389243
389302
|
const sub = positional;
|
|
389244
389303
|
if (sub === "format") {
|
|
389245
389304
|
if (json2) {
|
|
@@ -389257,10 +389316,6 @@ function makeProgram(opts = {}) {
|
|
|
389257
389316
|
yield* formatFile(args2[0]);
|
|
389258
389317
|
return;
|
|
389259
389318
|
}
|
|
389260
|
-
if (sub !== void 0) {
|
|
389261
|
-
return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
|
|
389262
|
-
}
|
|
389263
|
-
const config2 = yield* ConfigService;
|
|
389264
389319
|
const git = yield* GitService;
|
|
389265
389320
|
const fs9 = yield* FileSystem_exports.FileSystem;
|
|
389266
389321
|
const topLevel = yield* git.topLevel();
|
|
@@ -389273,6 +389328,50 @@ function makeProgram(opts = {}) {
|
|
|
389273
389328
|
)
|
|
389274
389329
|
);
|
|
389275
389330
|
}
|
|
389331
|
+
const config2 = yield* ConfigService;
|
|
389332
|
+
if (sub === "review") {
|
|
389333
|
+
const args2 = argv.slice(3).filter((a5) => a5.length > 0 && !a5.startsWith("--"));
|
|
389334
|
+
if (args2.length === 0) {
|
|
389335
|
+
return yield* Effect_exports.fail(new Error("gtd review: missing target argument"));
|
|
389336
|
+
}
|
|
389337
|
+
if (args2.length > 1) {
|
|
389338
|
+
return yield* Effect_exports.fail(
|
|
389339
|
+
new Error(
|
|
389340
|
+
`gtd review: too many arguments \u2014 expected one target, got: ${args2.join(", ")}`
|
|
389341
|
+
)
|
|
389342
|
+
);
|
|
389343
|
+
}
|
|
389344
|
+
const target = args2[0];
|
|
389345
|
+
const reviewResult = yield* reviewAgainst(target).pipe(
|
|
389346
|
+
Effect_exports.catchAll(
|
|
389347
|
+
(error) => Effect_exports.fail(new Error(`gtd review: cannot resolve ref '${target}': ${error.message}`))
|
|
389348
|
+
)
|
|
389349
|
+
);
|
|
389350
|
+
if (reviewResult === void 0) {
|
|
389351
|
+
return yield* Effect_exports.fail(
|
|
389352
|
+
new Error(`gtd review: nothing to review (${target} diff is empty after filtering)`)
|
|
389353
|
+
);
|
|
389354
|
+
}
|
|
389355
|
+
const { reviewBase, refDiff } = reviewResult;
|
|
389356
|
+
yield* git.commitAllWithPrefix(REVIEWING_SUBJECT);
|
|
389357
|
+
const result = cleanResult({ reviewBase, refDiff, autoAdvance: json2 });
|
|
389358
|
+
const builtPrompt = buildPrompt(result, config2.resolveModel, json2 ? "json" : "plain");
|
|
389359
|
+
if (json2) {
|
|
389360
|
+
write4(
|
|
389361
|
+
JSON.stringify({
|
|
389362
|
+
state: result.state,
|
|
389363
|
+
autoAdvance: result.autoAdvance,
|
|
389364
|
+
prompt: builtPrompt
|
|
389365
|
+
}) + "\n"
|
|
389366
|
+
);
|
|
389367
|
+
} else {
|
|
389368
|
+
write4(builtPrompt);
|
|
389369
|
+
}
|
|
389370
|
+
return;
|
|
389371
|
+
}
|
|
389372
|
+
if (sub !== void 0) {
|
|
389373
|
+
return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
|
|
389374
|
+
}
|
|
389276
389375
|
let hops = 0;
|
|
389277
389376
|
while (true) {
|
|
389278
389377
|
hops += 1;
|