@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.
Files changed (3) hide show
  1. package/README.md +121 -51
  2. package/dist/gtd.bundle.mjs +110 -11
  3. 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` (see below).
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-fix, fixer agent"]:::agent
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 | Action & commit | Advances to |
326
- | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
327
- | **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 |
328
- | **Escalate** | STOP | ERRORS.md present | none | held until the human deletes ERRORS.md |
329
- | **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 |
330
- | **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-fix` | fixer edits → Health check |
331
- | **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 |
332
- | **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning` | commit the `.gtd/` changes `gtd: planning` | continue decomposing, else → Building |
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) | 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 |
334
- | **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 |
335
- | **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 |
336
- | **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 |
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) | 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) |
338
- | **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 |
339
- | **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean) | commit REVIEW.md `gtd: awaiting review` | Done (auto, same run) |
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) | 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 |
341
- | **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 |
342
- | **Grilled** | agent, auto | TODO.md present, no markers, clean tree | commit pending `gtd: grilled` | decompose into `.gtd/` → Planning |
343
- | **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 |
344
- | **Health check** | edge-only, auto | no steering files, clean tree, outside a process with no reviewable diff (any branch) — the `!reviewable` case from rule 8 | run `testCommand`: 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 |
345
- | **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) | — |
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 → Health check(red) → … → Health check(green)
416
-
417
- └── below cap: HEALTH.md, gtd: health-check ─────────────────┘
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, commits removal as
425
- `gtd: health-fix`, loops back to Health check.
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
- <<<<<<< HEAD `git commit`, then **gtd STOPs**. The base is the parent of the
498
- current cycle's start marker **nearest to HEAD** (the last `gtd: new task`;
499
- for legacy cycles the last contiguous `gtd: grilling` run), and on a feature
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
- ======= `git commit`, then **gtd STOPs**. Post-squash review does not fire
509
- automatically it fires only on the next manual `gtd` run (when the squash
510
- commit is the boundary HEAD and a reviewable diff exists). Squashing fires
511
- when the tree has no unrelated code dirty — a lone untracked `SQUASH_MSG.md`
512
- is tolerated and deleted before the squash commit. If unrelated code is dirty
513
- at `gtd: done`, gtd routes to **New Feature** instead. Set `squash: false` in
514
- > > > > > > > origin/46-config-schema `.gtdrc` to skip squashing and go
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
- ## Formatting
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 ships a `format` subcommand the **only** subcommand — that formats a
744
- markdown file in place:
814
+ `gtd format` formats a markdown file in place:
745
815
 
746
816
  ```bash
747
817
  gtd format <file>
@@ -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/clean.md
368481
- var clean_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';
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("@clean", clean_default);
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: "@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 clean_default2 = clean;
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
- clean_default2(...argumentsList);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {