@pmelab/gtd 1.7.0 → 1.9.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.
Files changed (3) hide show
  1. package/README.md +186 -58
  2. package/dist/gtd.bundle.mjs +265 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,7 +17,7 @@ merge-base with the default branch (whole-history fallback when there is no
17
17
  default branch, when HEAD equals the merge-base, or when there is no merge-base
18
18
  — i.e. budgets engage on the default branch too) plus the working tree, turns
19
19
  them into a `COMMIT[]` + single terminal `RESOLVE` event stream, and folds them
20
- through the machine. The fold lands on exactly **one** of 18 states, which
20
+ through the machine. The fold lands on exactly **one** of 19 states, which
21
21
  selects the prompt. A single run resolves to a single state.
22
22
 
23
23
  `resolve()` returns that state plus an optional **`EdgeAction`** (a commit,
@@ -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
 
@@ -138,6 +145,11 @@ across runs:
138
145
  - **ERRORS.md** — the escalation gate: persistent test-failure output that stops
139
146
  the loop for a human (written instead of FEEDBACK.md once the fix-attempt cap
140
147
  is hit; never auto-consumed).
148
+ - **HEALTH.md** — idle health-check failure output: written when `testCommand`
149
+ fails on a bare idle tree (no `.gtd`, no REVIEW.md, no FEEDBACK.md). Carries
150
+ the failure while the dedicated health-fix loop repairs it. Analogous to
151
+ FEEDBACK.md but lives on the default-branch idle path rather than inside a
152
+ build process. Never written while any other steering file is present.
141
153
  - **.gtd/** — ordered work packages (one numbered directory each) of
142
154
  parallelizable subtasks.
143
155
 
@@ -158,9 +170,11 @@ Every run derives the state in **three layers**:
158
170
  1. **Transport pre-pass** — if HEAD is `gtd: transport`, short-circuit to the
159
171
  Transport state (mixed-reset) before anything else is considered.
160
172
  2. **Steering-file precedence** — the presence of `ERRORS.md` / `FEEDBACK.md` /
161
- `.gtd/` / `REVIEW.md` drives the decision, authoritative regardless of HEAD.
173
+ `HEALTH.md` / `.gtd/` / `REVIEW.md` drives the decision, authoritative
174
+ regardless of HEAD.
162
175
  3. **HEAD bucket** — with no steering files in play, the last-commit bucket plus
163
- working-tree cleanliness selects New Feature / Grilling / Clean / Idle.
176
+ working-tree cleanliness selects New Feature / Grilling / Clean / Health
177
+ check / Idle.
164
178
 
165
179
  Within layers 2 and 3 the HEAD subject further disambiguates states the
166
180
  filesystem alone cannot separate (e.g. inside the `.gtd/` lifecycle, HEAD
@@ -173,8 +187,9 @@ The complete set:
173
187
 
174
188
  `gtd: new task` · `gtd: grilling` · `gtd: grilled` · `gtd: planning` ·
175
189
  `gtd: building` · `gtd: errors` · `gtd: feedback` · `gtd: fixing` ·
176
- `gtd: package done` · `gtd: awaiting review` · `gtd: done` plus the hand-made
177
- `gtd: transport` (see below).
190
+ `gtd: package done` · `gtd: awaiting review` · `gtd: done` · `gtd: health-check`
191
+ · `gtd: health-fix` · `gtd: reviewing` — plus the hand-made `gtd: transport`
192
+ (see below).
178
193
 
179
194
  The last commit subject is bucketed two ways:
180
195
 
@@ -189,24 +204,28 @@ The last commit subject is bucketed two ways:
189
204
  1. **ERRORS.md present** → Escalate (human gate; STOP).
190
205
  2. **FEEDBACK.md present** → non-empty → Fixing; **empty** (clean agentic review
191
206
  = approval) → Close package.
192
- 3. **.gtd present** → build lifecycle, routed by tree + HEAD:
207
+ 3. **HEALTH.md present** → Health Fixing (idle health-fix loop; no `.gtd`,
208
+ REVIEW.md, or FEEDBACK.md).
209
+ 4. **.gtd present** → build lifecycle, routed by tree + HEAD:
193
210
  - `.gtd` modified (package files added/edited) → Planning
194
211
  - code changes present → Testing
195
212
  - clean tree + HEAD `gtd: fixing` (no-op fixer) → Testing (re-test)
196
213
  - else clean, by HEAD: `gtd: planning` / `gtd: package done` → Building;
197
214
  `gtd: building` → Agentic Review (or Close package, if force-approved)
198
- 4. **REVIEW.md present** → review lifecycle, routed by committed-ness + tree:
215
+ 5. **REVIEW.md present** → review lifecycle, routed by committed-ness + tree:
199
216
  committed + clean → Done; committed + checkbox-only edits (only `[ ]`↔`[x]`
200
217
  flips in REVIEW.md) → Done; committed + non-checkbox pending edits → Accept
201
218
  Review; uncommitted → Await Review (commits REVIEW.md and auto-advances to
202
- Done). 4a. **HEAD `gtd: done` + `squash` enabled + squash base present + no
219
+ Done). 5a. **HEAD `gtd: done` + `squash` enabled + squash base present + no
203
220
  unrelated code dirty** (a lone untracked `SQUASH_MSG.md` is allowed) →
204
221
  Squashing; unrelated code dirty → New Feature.
205
- 5. **Boundary HEAD + pending changes** (and no `.gtd`/REVIEW/FEEDBACK), or HEAD
222
+ 6. **Boundary HEAD + pending changes** (and no `.gtd`/REVIEW/FEEDBACK), or HEAD
206
223
  `gtd: new task` + clean tree (regenerate a lost seed) → New Feature.
207
- 6. **TODO.md present** → Grilling / Grilled.
208
- 7. **Boundary or `gtd: package done` HEAD + clean tree** → Clean (review the
209
- work) or Idle (nothing to review).
224
+ 7. **TODO.md present** → Grilling / Grilled.
225
+ 8. **Boundary or `gtd: package done` HEAD + clean tree** → Clean (review the
226
+ work), **Health check** (run `testCommand` when there is nothing to review
227
+ on any branch outside a process), or Idle (health check green, nothing to
228
+ do).
210
229
 
211
230
  Anything matching no rule is corruption — `gtd` **hard-errors** rather than
212
231
  guess.
@@ -221,7 +240,10 @@ flowchart TD
221
240
  P1 -->|no| P2{"FEEDBACK.md?"}
222
241
  P2 -->|"empty = approval"| Close["Close package — rm pkg dir, gtd: package done"]:::edge
223
242
  P2 -->|"non-empty"| Fixing["Fixing — rm FEEDBACK, fixer agent"]:::agent
224
- P2 -->|absent| P3{".gtd/?"}
243
+ P2 -->|absent| P2b{"HEALTH.md?"}
244
+ P2b -->|"present"| HealthFix["Health Fixing — rm HEALTH.md, gtd: health-fix, fixer agent"]:::agent
245
+ HealthFix -.->|"re-resolve"| HealthCheck
246
+ P2b -->|absent| P3{".gtd/?"}
225
247
  P3 -->|"modified"| Planning["Planning — gtd: planning"]:::agent
226
248
  P3 -->|"code dirty / resume / no-op fixer"| Testing["Testing — gtd: building, run tests"]:::edge
227
249
  P3 -->|"clean, HEAD planning/package done"| Building["Building — pick & build one package"]:::agent
@@ -234,17 +256,21 @@ flowchart TD
234
256
  P4 -->|absent| P5{"boundary HEAD + dirty,<br/>or gtd: new task + clean?"}
235
257
  Done -->|"squash enabled"| Squashing["Squashing — reset --soft base, squash commit"]:::agent
236
258
  Done -->|"squash disabled"| Idle
237
- Squashing --> Idle["Idle — nothing to do (STOP; review on next manual gtd run)"]:::gate
259
+ Squashing --> Idle["Idle — nothing to do (STOP)"]:::gate
238
260
  P5 -->|yes| NewFeature["New Feature — gtd: new task, revert, seed TODO"]:::edge
239
261
  P5 -->|no| P6{"TODO.md?"}
240
262
  P6 -->|"open markers"| GrillStop["Grilling — gtd: grilling, STOP for answers"]:::gate
241
263
  P6 -->|"dirty, no markers"| GrillIter["Grilling — gtd: grilling, agent iterates"]:::agent
242
- P6 -->|"clean, no markers"| GrilledReview["Grilled review — gtd: grilled, human review"]:::gate
243
- GrilledReview -->|"re-run gtd clean"| Grilled["Grilled — gtd: grilled, decompose"]:::agent
244
- GrilledReview -->|"edits present"| GrillIter
264
+ P6 -->|"clean, no markers"| Grilled["Grilled — gtd: grilled, decompose"]:::agent
245
265
  P6 -->|absent| P7{"clean + boundary/package-done HEAD,<br/>reviewable diff?"}
246
266
  P7 -->|yes| CleanState["Clean — write REVIEW.md"]:::agent
247
- P7 -->|no| Idle
267
+ P7 -->|"no (idle, outside a process)"| HealthCheck["Health check — run testCommand"]:::edge
268
+ HealthCheck -->|"green, no health-fix"| Idle
269
+ HealthCheck -->|"green + ≥1 health-fix, squash enabled"| Squashing
270
+ HealthCheck -->|"red, below cap"| HealthMd["write HEALTH.md, gtd: health-check"]:::edge
271
+ HealthMd -.->|"re-resolve"| HealthFix
272
+ HealthCheck -->|"red, at cap"| ErrorsMd["write ERRORS.md, gtd: health-check"]:::edge
273
+ ErrorsMd -.->|"re-resolve"| Escalate
248
274
  classDef edge fill:#1a4a6b,color:#fff
249
275
  classDef agent fill:#2d6a4f,color:#fff
250
276
  classDef gate fill:#7a3b1d,color:#fff
@@ -265,6 +291,10 @@ guessing:
265
291
  - FEEDBACK.md without .gtd
266
292
  - ERRORS.md + FEEDBACK.md
267
293
  - ERRORS.md without .gtd
294
+ - HEALTH.md + .gtd (health check only runs from a bare idle tree)
295
+ - HEALTH.md + REVIEW.md (same)
296
+ - HEALTH.md + FEEDBACK.md (same)
297
+ - HEALTH.md + ERRORS.md (escalation wins; HEALTH.md must not coexist)
268
298
 
269
299
  Legal coexistence: `.gtd` + TODO.md (plan kept alongside packages during
270
300
  **Planning** only — TODO.md is deleted at the first Building turn);
@@ -292,7 +322,7 @@ back into the working tree uncommitted, and re-derives state from scratch. If
292
322
  the transport commit is the repository's root commit (no parent), `gtd` fails
293
323
  immediately with a clear error instead of looping.
294
324
 
295
- ## The 18 states
325
+ ## The 19 states
296
326
 
297
327
  Each state has a **condition** (when it wins), a deterministic **action**, the
298
328
  **commit(s)** it produces, and where it **advances**. States marked
@@ -300,35 +330,36 @@ Each state has a **condition** (when it wins), a deterministic **action**, the
300
330
  human; **edge-only** states render no prompt at all — the driver performs their
301
331
  action and re-resolves silently.
302
332
 
303
- | State | Kind | Wins when | Action & commit | Advances to |
304
- | ------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
305
- | **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 |
306
- | **Escalate** | STOP | ERRORS.md present | none | held until the human deletes ERRORS.md |
307
- | **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 |
308
- | **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 packagesBuilding; `.gtd` gone → Clean |
309
- | **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning` | commit the `.gtd/` changes `gtd: planning` | continue decomposing, elseBuilding |
310
- | **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 |
311
- | **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 |
312
- | **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 |
313
- | **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 |
314
- | **Squashing** | agent, auto | no steering files, HEAD `gtd: done`, `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) |
315
- | **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 |
316
- | **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean) | commit REVIEW.md `gtd: awaiting review` | Done (auto, same run) |
317
- | **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 |
318
- | **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 review |
319
- | **Grilled review** | STOP | TODO.md present, no markers, clean tree, HEAD `gtd: grilled` (converged not yet decomposed) | none | edits Grilling; clean re-run → Grilled |
320
- | **Grilled** | agent, auto | TODO.md present, no markers, clean tree, HEAD `gtd: grilled` (returning from Grilled review with no new edits) | commit pending `gtd: grilled` | decompose into `.gtd/` → Planning |
321
- | **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 (four 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 |
322
- | **Idle** | STOP | no steering files, clean tree, and nothing to review (HEAD `gtd: done` with `squash` disabled or after Squashing, or no reviewable diff) | none | |
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-fix` | fixer editsHealth 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, elseBuilding |
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, 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 |
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) | — |
323
354
 
324
355
  Every prompt also embeds the current `git diff HEAD` (untracked files included)
325
356
  inline, plus the last commit subject and working-tree status, so the agent has
326
357
  full context.
327
358
 
328
- ### Review base — four rules
359
+ ### Review base — three rules
329
360
 
330
361
  The review base (the commit whose diff to HEAD forms the REVIEW.md) is chosen by
331
- four rules evaluated in priority order:
362
+ three rules evaluated in priority order:
332
363
 
333
364
  1. **Within a process, first review** — a `gtd: grilling` commit exists after
334
365
  the last `gtd: done` (or task start), but no `gtd: awaiting review` yet →
@@ -337,18 +368,16 @@ four rules evaluated in priority order:
337
368
  2. **Within a process, incremental** — `gtd: awaiting review` also present in
338
369
  the current cycle (takes precedence over rule 1) → base = last
339
370
  `gtd: awaiting review`; `refDiff` spans only the post-review changes.
340
- 3. **Outside a process, feature branch** — no `gtd: grilling` after the last
341
- `gtd: done`, and HEAD is not on the default branch base = merge-base with
342
- the default branch; `refDiff` spans the whole branch.
343
- 4. **Outside a process, default branch** — no process active and HEAD is on the
344
- default branch → skip review; `reviewBase`/`refDiff` unset → Idle.
371
+ 3. **Outside a process (any branch)** — no `gtd: grilling` after the last
372
+ `gtd: done` skip review; `reviewBase`/`refDiff` unsetIdle (the health
373
+ check runs instead).
345
374
 
346
375
  In all cases, if the diff from the chosen base to HEAD is empty,
347
376
  `reviewBase`/`refDiff` are left unset and the machine settles in Idle.
348
377
 
349
378
  ## The fix loops & counter folds
350
379
 
351
- Two derived counters drive the budgeted loops. Both are **folded in the
380
+ Three derived counters drive the budgeted loops. All are **folded in the
352
381
  machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
353
382
 
354
383
  - **`testFixCount`** — `gtd: errors` commits (test-fix attempts) since the
@@ -358,6 +387,9 @@ machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
358
387
  round, and every human resume starts a **fresh budget**.
359
388
  - **`reviewFixCount`** — `gtd: feedback` commits (review-fix rounds) since the
360
389
  most recent package start.
390
+ - **`healthFixCount`** — `gtd: health-check` commits since the most recent
391
+ commit that removed HEALTH.md (or the start of branch history if none). Reuses
392
+ `fixAttemptCap` — no separate config key.
361
393
 
362
394
  ### Test-fix loop (`fixAttemptCap`, default 3)
363
395
 
@@ -381,6 +413,31 @@ ERRORS.md — which **resets the fix-attempt budget** (the next run re-tests and
381
413
  grants a fresh `cap` attempts before escalating again). While ERRORS.md exists,
382
414
  every run resolves straight back to Escalate.
383
415
 
416
+ ### Health-fix loop (`fixAttemptCap`, `squash` — no new config)
417
+
418
+ Outside a process (any branch), when there is no reviewable diff and no steering
419
+ files, `gtd` runs `testCommand` instead of stopping. This reuses `fixAttemptCap`
420
+ (default 3) and `squash` — no new config keys are introduced.
421
+
422
+ ```
423
+ Idle path → Health check(red) → Health Fixing → Health check(red) → … → Health check(green)
424
+ │ │
425
+ └── below cap: HEALTH.md, gtd: health-check ─────────────────┘
426
+ └── at/over the cap: ERRORS.md, gtd: health-check → Escalate
427
+ green + ≥1 health-fix → Squashing (if squash enabled) → Idle
428
+ ```
429
+
430
+ - **green, no prior `gtd: health-fix`** → Idle immediately (no commit).
431
+ - **red, below `fixAttemptCap`** → write test output to HEALTH.md, commit
432
+ `gtd: health-check` → **Health Fixing** agent fixes, commits removal as
433
+ `gtd: health-fix`, loops back to Health check.
434
+ - **red, at or over `fixAttemptCap`** → write test output to ERRORS.md, commit
435
+ `gtd: health-check` → **Escalate** (human gate). Delete ERRORS.md to reset the
436
+ budget and resume.
437
+ - **green, ≥1 `gtd: health-fix` present** → health-fix cycle converged: if
438
+ `squash` is enabled → **Squashing** (squash base = parent of the first
439
+ `gtd: health-check` of the current run); otherwise → Idle (no commit).
440
+
384
441
  ### Review-fix loop & agentic review (`reviewThreshold`, default 3)
385
442
 
386
443
  After a green test run, **Agentic Review** reviews the package's accumulated
@@ -425,11 +482,9 @@ loop before the next one starts.
425
482
  your answer, and run `gtd` again. The agent integrates answers, moves them to
426
483
  `## Resolved`, and raises fresh questions — repeat until none remain (it
427
484
  writes `no open questions — run gtd to plan` with no markers).
428
- 4. **Converge.** A clean tree with no markers resolves to **Grilled review**
429
- (`gtd: grilled`) a human STOP. Review the plan; edit and re-run `gtd` to
430
- re-enter Grilling, or re-run `gtd` with no changes to advance. A clean re-run
431
- triggers **Grilled** (auto-advance), which then **Planning** decomposes
432
- `TODO.md` into ordered `.gtd/` work packages (`gtd: planning`).
485
+ 4. **Converge.** A clean tree with no markers resolves to **Grilled**
486
+ (`gtd: grilled`), then **Planning** decomposes `TODO.md` into ordered `.gtd/`
487
+ work packages (`gtd: planning`).
433
488
  5. **Build.** Run `gtd` — **Building** first deletes `TODO.md` (when HEAD is
434
489
  `gtd: planning` and it is still present, committed under the same
435
490
  `gtd: planning` prefix — fires once). It then names the single next package
@@ -482,11 +537,13 @@ built-in defaults apply. Supported filenames (searched in this order):
482
537
  ### Schema
483
538
 
484
539
  - **`testCommand`** (string, default `npm run test`) — the command the edge runs
485
- in the Testing state to verify a built package.
540
+ in the Testing state to verify a built package, and in the Health check state
541
+ on the default-branch idle path.
486
542
  - **`fixAttemptCap`** (non-negative integer, default `3`) — the test-fix budget:
487
543
  how many `gtd: errors` attempts are allowed per sub-loop before the failure is
488
544
  escalated to ERRORS.md (Escalate). `0` disables the cap (escalates immediately
489
- on the first red run).
545
+ on the first red run). Also reused as the health-fix budget (no separate
546
+ config key).
490
547
  - **`reviewThreshold`** (integer ≥ 1, default `3`) — the review-fix budget: how
491
548
  many `gtd: feedback` rounds are allowed per package before Agentic Review
492
549
  force-approves.
@@ -682,10 +739,50 @@ When there are genuinely no open questions left, the agent writes the sentinel
682
739
  line `no open questions — run gtd to plan` and leaves **no** markers — a clean
683
740
  tree with no markers is what advances the plan to **Grilled** and decomposition.
684
741
 
685
- ## Formatting
742
+ ## Subcommands
743
+
744
+ gtd ships two subcommands: `format` and `review`.
745
+
746
+ ## Review subcommand
747
+
748
+ ```bash
749
+ gtd review <target>
750
+ ```
751
+
752
+ Starts an explicit, on-demand human review of the diff between HEAD and
753
+ `merge-base(<target>, HEAD)`. Use this when the automatic review base yields no
754
+ diff (idle tree outside a process) or when you want to review against a specific
755
+ base regardless of workflow state.
686
756
 
687
- gtd ships a `format` subcommand — the **only** subcommand — that formats a
688
- markdown file in place:
757
+ ### Flow
758
+
759
+ 1. `gtd review <target>` computes the diff HEAD adds over
760
+ `merge-base(<target>, HEAD)`.
761
+ 2. The edge writes an empty anchor commit `gtd: reviewing` (no content — just
762
+ the marker).
763
+ 3. The **Clean** state writes `REVIEW.md` with the computed diff and emits the
764
+ normal review prompt. `--json` is accepted and enables auto-advance mode
765
+ (same behavior as the default command).
766
+ 4. The normal loop then drives: **Await Review** → **Done** → **Squashing**,
767
+ collapsing back to the `gtd: reviewing` anchor commit.
768
+
769
+ ### Error handling
770
+
771
+ All errors exit with **code 1** and write a message to **stderr**:
772
+
773
+ - **Missing target** — `gtd review` with no argument:
774
+ `gtd review: missing target argument`
775
+ - **Extra arguments** — `gtd review main extra`:
776
+ `gtd review: too many arguments — expected one target, got: …`
777
+ - **Unresolvable ref** — the target cannot be resolved by git:
778
+ `gtd review: cannot resolve ref '<target>': <error message>`
779
+ - **Empty diff** — the merge-base diff between `<target>` and HEAD is empty
780
+ (nothing to review):
781
+ `gtd review: nothing to review (<target> diff is empty after filtering)`
782
+
783
+ ## Format subcommand
784
+
785
+ `gtd format` formats a markdown file in place:
689
786
 
690
787
  ```bash
691
788
  gtd format <file>
@@ -812,6 +909,37 @@ Run `npm run test:mutation` after making changes to the mutated files to check
812
909
  whether surviving mutants increased. The HTML report lands in
813
910
  `reports/mutation/mutation.html` (git-ignored).
814
911
 
912
+ ### Mutation testing
913
+
914
+ Run mutation testing on-demand with `npm run test:mutation` (StrykerJS, ~2 min).
915
+ The single `stryker.config.json` mutates six core files:
916
+
917
+ ```
918
+ src/Machine.ts src/Prompt.ts src/Config.ts
919
+ src/Format.ts src/State.ts src/Events.ts
920
+ ```
921
+
922
+ `src/Git.ts` is excluded: the Cucumber harness stubs git at the Effect boundary,
923
+ so Git.ts mutants have zero in-memory coverage. Measuring its post-refactor
924
+ Live-tier kill rate is a follow-up before re-including it.
925
+
926
+ **`process.chdir()` gotcha (resolved).** `@stryker-mutator/vitest-runner`
927
+ hardcodes `pool: 'threads'` internally, and `process.chdir()` is unsupported in
928
+ worker threads. Before the cwd refactor (package 01), four test files
929
+ (`Events.test.ts`, `Git.test.ts`, `Config.test.ts`, `TestRunner.test.ts`) had to
930
+ be excluded from all Stryker runs. The refactor eliminated those calls, letting
931
+ all four files rejoin the run.
932
+
933
+ Two additional notes: `vitest.related` is disabled for feature-file runs because
934
+ feature files don't import source files directly (Stryker's coverage-based
935
+ filtering would assign zero tests to every mutant). Compile-error mutants are
936
+ counted as kills by the TypeScript checker — they represent real signal, not a
937
+ configuration problem.
938
+
939
+ Run `npm run test:mutation` after making changes to the mutated files to check
940
+ whether surviving mutants increased. The HTML report lands in
941
+ `reports/mutation/mutation.html` (git-ignored).
942
+
815
943
  ## Releasing
816
944
 
817
945
  Releases are automatic. Push releasable Conventional Commits (`fix:`, `feat:`,
@@ -368477,8 +368477,8 @@ var fixing_default = '<%~ include("@header") %>\n\n<%~ include("@context", { con
368477
368477
  // src/prompts/agentic-review.md
368478
368478
  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
368479
 
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';
368480
+ // src/prompts/review.md
368481
+ 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
368482
 
368483
368483
  // src/prompts/squashing.md
368484
368484
  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';
@@ -368486,9 +368486,6 @@ var squashing_default = '<%~ include("@header") %>\n\n<%~ include("@context", {
368486
368486
  // src/prompts/escalate.md
368487
368487
  var escalate_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nThe agent was not able to fix all errors on its own. The last error report is\nstored in `ERRORS.md` for a human to investigate.\n\nNext steps for the human developer:\n\n1. Investigate and fix errors reported in `ERRORS.md`\n2. Delete `ERRORS.md`\n3. Continue the process when you are ready\n\n<%~ include(it.tail) %>\n';
368488
368488
 
368489
- // src/prompts/grilled-review.md
368490
- var grilled_review_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %> The\nplan in `TODO.md` has converged \u2014 no open questions remain \u2014 and has been\ncommitted as `gtd: grilled`.\n\n### Human review gate\n\nThis is a human review point. Tell the user to open `TODO.md` and review the\nfinalized plan:\n\n- To **proceed**, continue with a clean working tree \u2014 the plan will be\n decomposed into `.gtd/` work packages.\n- To **revise**, edit `TODO.md` (or sketch code) and continue \u2014 the changes\n re-enter the grilling loop for another convergence round.\n\n<%~ include(it.tail) %>\n';
368491
-
368492
368489
  // src/prompts/idle.md
368493
368490
  var idle_default = '<%~ include("@header") %>\n\n<%~ include("@context", { context: it.context, fenceFor: it.fenceFor }) %>\nReport that the repository is idle \u2014 nothing to do.\n\n<%~ include(it.tail) %>\n';
368494
368491
 
@@ -368500,7 +368497,8 @@ var EDGE_ONLY_STATES = /* @__PURE__ */ new Set([
368500
368497
  "accept-review",
368501
368498
  "close-package",
368502
368499
  "done",
368503
- "await-review"
368500
+ "await-review",
368501
+ "health-check"
368504
368502
  ]);
368505
368503
  var MODEL_STATE = {
368506
368504
  grilling: "grilling",
@@ -368508,6 +368506,7 @@ var MODEL_STATE = {
368508
368506
  planning: "decompose",
368509
368507
  building: "building",
368510
368508
  fixing: "fixing",
368509
+ "health-fixing": "fixing",
368511
368510
  "agentic-review": "agentic-review",
368512
368511
  clean: "clean",
368513
368512
  squashing: "clean"
@@ -368533,24 +368532,23 @@ eta.loadTemplate("@decompose", decompose_default);
368533
368532
  eta.loadTemplate("@building", building_default);
368534
368533
  eta.loadTemplate("@fixing", fixing_default);
368535
368534
  eta.loadTemplate("@agentic-review", agentic_review_default);
368536
- eta.loadTemplate("@clean", clean_default);
368535
+ eta.loadTemplate("@review", review_default);
368537
368536
  eta.loadTemplate("@squashing", squashing_default);
368538
368537
  eta.loadTemplate("@escalate", escalate_default);
368539
- eta.loadTemplate("@grilled-review", grilled_review_default);
368540
368538
  eta.loadTemplate("@idle", idle_default);
368541
368539
  eta.readFile = null;
368542
368540
  eta.resolvePath = null;
368543
368541
  var STATE_TEMPLATE = {
368544
368542
  grilled: "@decompose",
368545
- "grilled-review": "@grilled-review",
368546
368543
  planning: "@decompose",
368547
368544
  building: "@building",
368548
368545
  fixing: "@fixing",
368549
368546
  "agentic-review": "@agentic-review",
368550
- clean: "@clean",
368547
+ clean: "@review",
368551
368548
  squashing: "@squashing",
368552
368549
  escalate: "@escalate",
368553
- idle: "@idle"
368550
+ idle: "@idle",
368551
+ "health-fixing": "@fixing"
368554
368552
  };
368555
368553
  var buildPrompt = (result, resolveModel = builtinResolveModel, output = "plain") => {
368556
368554
  const { state, context: context10 } = result;
@@ -385732,7 +385730,7 @@ function clean(original, cloned) {
385732
385730
  }
385733
385731
  return cloned;
385734
385732
  }
385735
- var clean_default2 = clean;
385733
+ var clean_default = clean;
385736
385734
  function printFrontMatter({ node }) {
385737
385735
  return node.raw;
385738
385736
  }
@@ -385837,7 +385835,7 @@ function normalizePrinter(printer2) {
385837
385835
  if (originalCleanFunction && frontMatterSupport.massageAstNode) {
385838
385836
  massageAstNode = new Proxy(originalCleanFunction, {
385839
385837
  apply(target, thisArgument, argumentsList) {
385840
- clean_default2(...argumentsList);
385838
+ clean_default(...argumentsList);
385841
385839
  return Reflect.apply(target, thisArgument, argumentsList);
385842
385840
  }
385843
385841
  });
@@ -388266,8 +388264,11 @@ var GTD_DIR = ".gtd";
388266
388264
  var REVIEW_FILE = "REVIEW.md";
388267
388265
  var FEEDBACK_FILE = "FEEDBACK.md";
388268
388266
  var ERRORS_FILE = "ERRORS.md";
388267
+ var HEALTH_FILE = "HEALTH.md";
388269
388268
  var EMPTY_FAILURE_SENTINEL = "Test command failed with no output (exit code non-zero).";
388270
388269
  var UNANSWERED_MARKER = "<!-- user answers here -->";
388270
+ var HEALTH_CHECK_SUBJECT = "gtd: health-check";
388271
+ var HEALTH_FIX_SUBJECT = "gtd: health-fix";
388271
388272
  var NEW_TASK_SUBJECT = "gtd: new task";
388272
388273
  var GRILLING_SUBJECT = "gtd: grilling";
388273
388274
  var PLANNING_SUBJECT = "gtd: planning";
@@ -388278,12 +388279,14 @@ var PACKAGE_DONE_SUBJECT = "gtd: package done";
388278
388279
  var AWAITING_REVIEW_SUBJECT = "gtd: awaiting review";
388279
388280
  var DONE_SUBJECT = "gtd: done";
388280
388281
  var REVIEW_FEEDBACK_SUBJECT = "gtd: review feedback";
388282
+ var REVIEWING_SUBJECT = "gtd: reviewing";
388281
388283
  var SQUASH_MSG_FILE = "SQUASH_MSG.md";
388282
388284
  var STEERING_FILES = [
388283
388285
  TODO_FILE,
388284
388286
  REVIEW_FILE,
388285
388287
  FEEDBACK_FILE,
388286
388288
  ERRORS_FILE,
388289
+ HEALTH_FILE,
388287
388290
  SQUASH_MSG_FILE
388288
388291
  ];
388289
388292
  var WORKFLOW_FILE_EXCLUDES = [...STEERING_FILES, GTD_DIR];
@@ -388473,7 +388476,9 @@ var gatherEvents = () => (
388473
388476
  isFeedback: subject === FEEDBACK_SUBJECT,
388474
388477
  isPackageStart: subject === PLANNING_SUBJECT || subject === PACKAGE_DONE_SUBJECT,
388475
388478
  isWorkflowCommit: subject.startsWith("gtd: "),
388476
- removedErrors
388479
+ removedErrors,
388480
+ isHealthCheck: subject === HEALTH_CHECK_SUBJECT,
388481
+ isHealthFix: subject === HEALTH_FIX_SUBJECT
388477
388482
  };
388478
388483
  }
388479
388484
  );
@@ -388547,10 +388552,6 @@ var gatherEvents = () => (
388547
388552
  candidate = firstGrilling.hash ?? EMPTY_TREE;
388548
388553
  }
388549
388554
  } else {
388550
- const mergeBaseCandidate = Option_exports.isSome(base) && base.value !== headHash ? base.value : void 0;
388551
- if (mergeBaseCandidate !== void 0) {
388552
- candidate = mergeBaseCandidate;
388553
- }
388554
388555
  }
388555
388556
  if (candidate !== void 0 && hasCommitsAfterLastDone) {
388556
388557
  const candidateDiff = yield* git.diffRef(candidate, WORKFLOW_FILE_EXCLUDES).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
@@ -388577,7 +388578,7 @@ var gatherEvents = () => (
388577
388578
  const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
388578
388579
  let startIdx = -1;
388579
388580
  for (let i = squashCycle.length - 1; i >= 0; i--) {
388580
- if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT) {
388581
+ if (subjectOf(squashCycle[i]) === NEW_TASK_SUBJECT || subjectOf(squashCycle[i]) === REVIEWING_SUBJECT) {
388581
388582
  startIdx = i;
388582
388583
  break;
388583
388584
  }
@@ -388606,6 +388607,31 @@ var gatherEvents = () => (
388606
388607
  }
388607
388608
  const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388608
388609
  const squashMsgContent = squashMsgPresent ? yield* fs9.readFileString(resolve5(SQUASH_MSG_FILE)) : "";
388610
+ const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388611
+ const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388612
+ const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388613
+ let healthFixBase;
388614
+ if (config2.squash) {
388615
+ const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
388616
+ let firstHealthCheckHash;
388617
+ let healthCheckCount = 0;
388618
+ for (const commit3 of history) {
388619
+ const s5 = subjectOf(commit3);
388620
+ const isPackageStart = s5 === PLANNING_SUBJECT || s5 === PACKAGE_DONE_SUBJECT;
388621
+ if (isPackageStart || commit3.removedErrors) {
388622
+ firstHealthCheckHash = void 0;
388623
+ healthCheckCount = 0;
388624
+ }
388625
+ if (s5 === HEALTH_CHECK_SUBJECT) {
388626
+ healthCheckCount++;
388627
+ if (healthCheckCount === 1) firstHealthCheckHash = commit3.hash;
388628
+ }
388629
+ }
388630
+ if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
388631
+ const base2 = yield* git.resolveRef(`${firstHealthCheckHash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388632
+ healthFixBase = base2;
388633
+ }
388634
+ }
388609
388635
  const payload = {
388610
388636
  todoExists,
388611
388637
  todoCommitted,
@@ -388637,7 +388663,11 @@ var gatherEvents = () => (
388637
388663
  ...squashBase !== void 0 ? { squashBase } : {},
388638
388664
  ...squashDiff !== void 0 ? { squashDiff } : {},
388639
388665
  squashMsgPresent,
388640
- squashMsgContent
388666
+ squashMsgContent,
388667
+ healthPresent,
388668
+ healthContent,
388669
+ healthCommitted,
388670
+ ...healthFixBase !== void 0 ? { healthFixBase } : {}
388641
388671
  };
388642
388672
  const resolveEvent = { type: "RESOLVE", payload };
388643
388673
  return [...commitEvents, resolveEvent];
@@ -388655,6 +388685,15 @@ var captureAndRevert = (git, subject, onRegen) => Effect_exports.gen(function* (
388655
388685
  yield* git.revertNoCommit("HEAD");
388656
388686
  return captured;
388657
388687
  });
388688
+ var reviewAgainst = (target) => Effect_exports.gen(function* () {
388689
+ const git = yield* GitService;
388690
+ const targetHash = yield* git.resolveRef(target);
388691
+ const mergeBase = yield* git.mergeBase(target, "HEAD");
388692
+ const mergeBaseHash = Option_exports.isNone(mergeBase) || mergeBase.value === targetHash ? targetHash : mergeBase.value;
388693
+ const refDiff = yield* git.diffRef(mergeBaseHash, WORKFLOW_FILE_EXCLUDES);
388694
+ if (refDiff.trim().length === 0) return void 0;
388695
+ return { reviewBase: mergeBaseHash, refDiff };
388696
+ });
388658
388697
  var perform = (action) => (
388659
388698
  // fallow-ignore-next-line complexity
388660
388699
  Effect_exports.gen(function* () {
@@ -388667,7 +388706,7 @@ var perform = (action) => (
388667
388706
  // work in the tree, then re-derive. (No producer command — consume only.)
388668
388707
  case "transportReset": {
388669
388708
  yield* git.mixedResetHead();
388670
- return;
388709
+ return { stop: false };
388671
388710
  }
388672
388711
  // New Feature: capture the raw input as `gtd: new task` (unless HEAD is
388673
388712
  // already there — the lost-seed regenerate case), revert it back to a
@@ -388675,7 +388714,7 @@ var perform = (action) => (
388675
388714
  case "seedNewFeature": {
388676
388715
  const captured = yield* captureAndRevert(git, NEW_TASK_SUBJECT, Effect_exports.void);
388677
388716
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388678
- return;
388717
+ return { stop: false };
388679
388718
  }
388680
388719
  // Accept Review: capture the human's pending changeset durably as
388681
388720
  // `gtd: review feedback` (annotations, code edits, and new files alike),
@@ -388687,7 +388726,7 @@ var perform = (action) => (
388687
388726
  const captured = yield* captureAndRevert(git, REVIEW_FEEDBACK_SUBJECT, git.resetHard());
388688
388727
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388689
388728
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388690
- return;
388729
+ return { stop: false };
388691
388730
  }
388692
388731
  // Grilling capture: the plan is already committed and the user sketched
388693
388732
  // code during the round. Fold the code diff (untracked included, steering
@@ -388710,7 +388749,7 @@ var perform = (action) => (
388710
388749
  }
388711
388750
  yield* fs9.writeFileString(resolve5(TODO_FILE), appendCapturedInput(todoNow, captured));
388712
388751
  yield* git.commitAllWithPrefix(GRILLING_SUBJECT);
388713
- return;
388752
+ return { stop: false };
388714
388753
  }
388715
388754
  // Testing: commit the pending tree `gtd: building` (nothing pending in the
388716
388755
  // no-op-fixer case), run tests; on red write FEEDBACK (below cap) or ERRORS
@@ -388727,13 +388766,46 @@ var perform = (action) => (
388727
388766
  if (!committedBuilding) {
388728
388767
  yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
388729
388768
  }
388730
- return;
388769
+ return { stop: false };
388731
388770
  }
388732
388771
  const target = action.capReached ? ERRORS_FILE : FEEDBACK_FILE;
388733
388772
  const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388734
388773
  yield* fs9.writeFileString(resolve5(target), body);
388735
388774
  yield* git.commitAllWithPrefix(ERRORS_SUBJECT);
388736
- return;
388775
+ return { stop: false };
388776
+ }
388777
+ // Health check: run tests on an idle/clean tree.
388778
+ // Green, no prior fixes → stop immediately (no commit).
388779
+ // Green, fixes exist → stop immediately (squash handled via healthFixBase
388780
+ // when gatherEvents sets it; the machine resolves squashing state).
388781
+ // Red below cap → write HEALTH.md (uncommitted); resolveHealth will
388782
+ // commit it as `gtd: health-check` via `commitPending removeHealth`.
388783
+ // Red at cap → write ERRORS.md and commit `gtd: health-check` immediately
388784
+ // so the Escalate state fires on the next resolve.
388785
+ case "runHealthCheck": {
388786
+ if (action.commitErrorsReset === true) {
388787
+ yield* git.commitAllWithPrefix(HEALTH_FIX_SUBJECT);
388788
+ }
388789
+ const runner = yield* TestRunner;
388790
+ const result = yield* runner.run();
388791
+ if (result.exitCode === 0) {
388792
+ if (action.healthFixBase !== void 0 && action.commitErrorsReset !== true) {
388793
+ yield* fs9.writeFileString(
388794
+ resolve5(SQUASH_MSG_FILE),
388795
+ "chore: health-check cycle squash\n"
388796
+ );
388797
+ return { stop: false };
388798
+ }
388799
+ return { stop: true };
388800
+ }
388801
+ const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388802
+ if (action.capReached) {
388803
+ yield* fs9.writeFileString(resolve5(ERRORS_FILE), body);
388804
+ yield* git.commitAllWithPrefix(HEALTH_CHECK_SUBJECT);
388805
+ } else {
388806
+ yield* fs9.writeFileString(resolve5(HEALTH_FILE), body);
388807
+ }
388808
+ return { stop: false };
388737
388809
  }
388738
388810
  // Grilling / Grilled / Planning / Fixing: commit the pending tree under a
388739
388811
  // fixed phase prefix. Fixing sets `removeFeedback` so FEEDBACK.md's removal
@@ -388747,11 +388819,14 @@ var perform = (action) => (
388747
388819
  if (action.removeTodo === true) {
388748
388820
  yield* fs9.remove(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388749
388821
  }
388822
+ if (action.removeHealth === true) {
388823
+ yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388824
+ }
388750
388825
  if (action.removeTodo !== true) {
388751
388826
  yield* formatFile(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388752
388827
  }
388753
388828
  yield* git.commitAllWithPrefix(action.prefix);
388754
- return;
388829
+ return { stop: false };
388755
388830
  }
388756
388831
  // Close package: remove the (maybe-empty / maybe-absent) FEEDBACK.md, rm
388757
388832
  // the first (finished) package dir (+ the now-empty `.gtd/`), commit
@@ -388764,19 +388839,19 @@ var perform = (action) => (
388764
388839
  yield* git.removePackageDir(`${GTD_DIR}/${first2.name}`);
388765
388840
  }
388766
388841
  yield* git.commitAllWithPrefix(PACKAGE_DONE_SUBJECT);
388767
- return;
388842
+ return { stop: false };
388768
388843
  }
388769
388844
  // Await Review: commit REVIEW.md as `gtd: awaiting review`.
388770
388845
  case "commitReview": {
388771
388846
  yield* formatFile(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388772
388847
  yield* git.commitAllWithPrefix(AWAITING_REVIEW_SUBJECT);
388773
- return;
388848
+ return { stop: false };
388774
388849
  }
388775
388850
  // Done: rm REVIEW.md, commit `gtd: done`.
388776
388851
  case "done": {
388777
388852
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388778
388853
  yield* git.commitAllWithPrefix(DONE_SUBJECT);
388779
- return;
388854
+ return { stop: false };
388780
388855
  }
388781
388856
  // Squash: rm SQUASH_MSG.md, soft-reset to squashBase, commit everything
388782
388857
  // under the provided commit message.
@@ -388784,7 +388859,7 @@ var perform = (action) => (
388784
388859
  yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388785
388860
  yield* git.softResetTo(action.squashBase);
388786
388861
  yield* git.commitAllWithPrefix(action.commitMessage);
388787
- return;
388862
+ return { stop: false };
388788
388863
  }
388789
388864
  }
388790
388865
  }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
@@ -388802,14 +388877,17 @@ var GtdStateError = class extends Error {
388802
388877
  var foldCounters = (events) => {
388803
388878
  let testFixCount = 0;
388804
388879
  let reviewFixCount = 0;
388880
+ let healthFixCount = 0;
388805
388881
  for (const event of events) {
388806
388882
  if (event.type !== "COMMIT") continue;
388807
388883
  if (event.isPackageStart || event.isFeedback || event.removedErrors) testFixCount = 0;
388808
388884
  if (event.isErrors) testFixCount += 1;
388809
388885
  if (event.isPackageStart) reviewFixCount = 0;
388810
388886
  if (event.isFeedback) reviewFixCount += 1;
388887
+ if (event.isPackageStart || event.removedErrors) healthFixCount = 0;
388888
+ if (event.isHealthCheck) healthFixCount += 1;
388811
388889
  }
388812
- return { testFixCount, reviewFixCount };
388890
+ return { testFixCount, reviewFixCount, healthFixCount };
388813
388891
  };
388814
388892
  var isBoundary = (subject) => !subject.startsWith("gtd: ") || subject === "gtd: done";
388815
388893
  var DEFAULT_PAYLOAD = {
@@ -388839,7 +388917,10 @@ var DEFAULT_PAYLOAD = {
388839
388917
  reviewThreshold: 3,
388840
388918
  squashEnabled: false,
388841
388919
  squashMsgPresent: false,
388842
- squashMsgContent: ""
388920
+ squashMsgContent: "",
388921
+ healthPresent: false,
388922
+ healthContent: "",
388923
+ healthCommitted: false
388843
388924
  };
388844
388925
  var buildContext = (p4, counters, grillingCase) => ({
388845
388926
  testFixCount: counters.testFixCount,
@@ -388852,7 +388933,7 @@ var buildContext = (p4, counters, grillingCase) => ({
388852
388933
  ...p4.squashDiff !== void 0 ? { squashDiff: p4.squashDiff } : {},
388853
388934
  lastCommitSubject: p4.lastCommitSubject,
388854
388935
  workingTreeClean: p4.workingTreeClean,
388855
- feedbackContent: p4.feedbackContent,
388936
+ feedbackContent: p4.feedbackContent !== "" ? p4.feedbackContent : p4.healthContent,
388856
388937
  ...grillingCase !== void 0 ? { grillingCase } : {}
388857
388938
  });
388858
388939
  var assertLegal = (p4) => {
@@ -388866,7 +388947,32 @@ var assertLegal = (p4) => {
388866
388947
  if (p4.feedbackPresent && p4.reviewPresent) fail20("illegal combination: FEEDBACK.md + REVIEW.md");
388867
388948
  if (p4.feedbackPresent && !p4.gtdDirExists) fail20("illegal combination: FEEDBACK.md without .gtd");
388868
388949
  if (p4.errorsPresent && p4.feedbackPresent) fail20("illegal combination: ERRORS.md + FEEDBACK.md");
388869
- if (p4.errorsPresent && !p4.gtdDirExists) fail20("illegal combination: ERRORS.md without .gtd");
388950
+ const isHealthCapEscalation = p4.lastCommitSubject === "gtd: health-check" || p4.lastCommitSubject === "gtd: health-fix";
388951
+ if (p4.errorsPresent && !p4.gtdDirExists && !isHealthCapEscalation)
388952
+ fail20("illegal combination: ERRORS.md without .gtd");
388953
+ if (p4.healthPresent && p4.gtdDirExists) fail20("illegal combination: HEALTH.md + .gtd");
388954
+ if (p4.healthPresent && p4.reviewPresent) fail20("illegal combination: HEALTH.md + REVIEW.md");
388955
+ if (p4.healthPresent && p4.feedbackPresent) fail20("illegal combination: HEALTH.md + FEEDBACK.md");
388956
+ if (p4.healthPresent && p4.errorsPresent) fail20("illegal combination: HEALTH.md + ERRORS.md");
388957
+ };
388958
+ var resolveHealth = (p4, counters) => {
388959
+ const hasPendingWork = !p4.healthCommitted || !p4.workingTreeClean;
388960
+ const prefix = p4.healthCommitted ? "gtd: health-fix" : "gtd: health-check";
388961
+ return {
388962
+ state: "health-fixing",
388963
+ autoAdvance: true,
388964
+ ...hasPendingWork ? {
388965
+ edgeAction: {
388966
+ kind: "commitPending",
388967
+ prefix,
388968
+ // Always remove HEALTH.md: on gtd: health-check this clears it so
388969
+ // the next run re-enters resolveCleanOrIdle; on gtd: health-fix it
388970
+ // clears the marker left by a prior health-check run.
388971
+ removeHealth: true
388972
+ }
388973
+ } : {},
388974
+ context: buildContext(p4, counters)
388975
+ };
388870
388976
  };
388871
388977
  var resolveFeedback = (p4, counters) => {
388872
388978
  if (p4.feedbackEmpty) {
@@ -388992,22 +389098,21 @@ var resolveGrilling = (p4, counters, head7) => {
388992
389098
  context: buildContext(p4, counters, "iterate")
388993
389099
  };
388994
389100
  }
388995
- if (head7 === "gtd: grilled") {
388996
- return {
388997
- state: "grilled",
388998
- autoAdvance: true,
388999
- context: buildContext(p4, counters)
389000
- };
389001
- }
389002
389101
  return {
389003
- state: "grilled-review",
389004
- autoAdvance: false,
389102
+ state: "grilled",
389103
+ autoAdvance: true,
389005
389104
  edgeAction: { kind: "commitPending", prefix: "gtd: grilled" },
389006
389105
  context: buildContext(p4, counters)
389007
389106
  };
389008
389107
  };
389009
389108
  var resolveCleanOrIdle = (p4, counters, head7) => {
389010
- if (!p4.workingTreeClean || !isBoundary(head7) && head7 !== "gtd: package done") return null;
389109
+ const isHealthHead = head7 === "gtd: health-check" || head7 === "gtd: health-fix";
389110
+ if (!p4.workingTreeClean && !p4.pendingErrorsDeletion) return null;
389111
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && p4.gtdDirExists) return null;
389112
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389113
+ return null;
389114
+ if (p4.workingTreeClean && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389115
+ return null;
389011
389116
  if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0) {
389012
389117
  if (p4.squashMsgPresent) {
389013
389118
  return {
@@ -389024,9 +389129,20 @@ var resolveCleanOrIdle = (p4, counters, head7) => {
389024
389129
  return { state: "squashing", autoAdvance: true, context: buildContext(p4, counters) };
389025
389130
  }
389026
389131
  const reviewable = p4.hasCommitsAfterLastDone && p4.reviewBase !== void 0 && (p4.refDiff ?? "").trim().length > 0;
389132
+ if (reviewable) {
389133
+ return { state: "clean", autoAdvance: false, context: buildContext(p4, counters) };
389134
+ }
389135
+ const squashHealth = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
389027
389136
  return {
389028
- state: reviewable ? "clean" : "idle",
389029
- autoAdvance: false,
389137
+ state: "health-check",
389138
+ autoAdvance: true,
389139
+ edgeAction: {
389140
+ kind: "runHealthCheck",
389141
+ errorCount: counters.healthFixCount,
389142
+ capReached: counters.healthFixCount >= p4.fixAttemptCap,
389143
+ ...squashHealth ? { healthFixBase: p4.healthFixBase } : {},
389144
+ ...p4.pendingErrorsDeletion ? { commitErrorsReset: true } : {}
389145
+ },
389030
389146
  context: buildContext(p4, counters)
389031
389147
  };
389032
389148
  };
@@ -389053,6 +389169,7 @@ var resolve4 = (events) => {
389053
389169
  }
389054
389170
  if (p4.errorsPresent)
389055
389171
  return { state: "escalate", autoAdvance: false, context: buildContext(p4, counters) };
389172
+ if (p4.healthPresent) return resolveHealth(p4, counters);
389056
389173
  if (p4.feedbackPresent) return resolveFeedback(p4, counters);
389057
389174
  if (p4.gtdDirExists) return resolveGtdLifecycle(p4, counters, head7, corrupt);
389058
389175
  if (p4.reviewPresent) return resolveReviewLifecycle(p4, counters, head7);
@@ -389068,6 +389185,18 @@ var resolve4 = (events) => {
389068
389185
  context: buildContext(p4, counters)
389069
389186
  };
389070
389187
  }
389188
+ if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && !p4.codeDirty) {
389189
+ return {
389190
+ state: "squashing",
389191
+ autoAdvance: false,
389192
+ edgeAction: {
389193
+ kind: "squashCommit",
389194
+ squashBase: p4.healthFixBase,
389195
+ commitMessage: p4.squashMsgContent
389196
+ },
389197
+ context: buildContext(p4, counters)
389198
+ };
389199
+ }
389071
389200
  if (isBoundary(head7) && !p4.workingTreeClean && !p4.todoCommitted || head7 === "gtd: new task" && p4.workingTreeClean) {
389072
389201
  return {
389073
389202
  state: "new-feature",
@@ -389079,6 +389208,20 @@ var resolve4 = (events) => {
389079
389208
  if (p4.todoExists) return resolveGrilling(p4, counters, head7);
389080
389209
  return resolveCleanOrIdle(p4, counters, head7) ?? corrupt();
389081
389210
  };
389211
+ var cleanResult = (args2) => ({
389212
+ state: "clean",
389213
+ autoAdvance: args2.autoAdvance,
389214
+ context: {
389215
+ ...buildContext(
389216
+ {
389217
+ ...DEFAULT_PAYLOAD,
389218
+ reviewBase: args2.reviewBase,
389219
+ refDiff: args2.refDiff
389220
+ },
389221
+ { testFixCount: 0, reviewFixCount: 0, healthFixCount: 0 }
389222
+ )
389223
+ }
389224
+ });
389082
389225
 
389083
389226
  // src/State.ts
389084
389227
  var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
@@ -389088,7 +389231,8 @@ var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
389088
389231
  "await-review",
389089
389232
  "accept-review",
389090
389233
  "close-package",
389091
- "done"
389234
+ "done",
389235
+ "health-check"
389092
389236
  ]);
389093
389237
  var isEdgeOnly = (state) => EDGE_ONLY_STATES2.has(state);
389094
389238
  var detect = () => Effect_exports.gen(function* () {
@@ -389100,6 +389244,19 @@ var detect = () => Effect_exports.gen(function* () {
389100
389244
  });
389101
389245
 
389102
389246
  // src/program.ts
389247
+ var IDLE_RESULT = {
389248
+ state: "idle",
389249
+ autoAdvance: false,
389250
+ context: {
389251
+ testFixCount: 0,
389252
+ reviewFixCount: 0,
389253
+ packages: DEFAULT_PAYLOAD.packages,
389254
+ diff: DEFAULT_PAYLOAD.diff,
389255
+ lastCommitSubject: DEFAULT_PAYLOAD.lastCommitSubject,
389256
+ workingTreeClean: DEFAULT_PAYLOAD.workingTreeClean,
389257
+ feedbackContent: DEFAULT_PAYLOAD.feedbackContent
389258
+ }
389259
+ };
389103
389260
  var MAX_EDGE_HOPS = 100;
389104
389261
  function makeProgram(opts = {}) {
389105
389262
  const argv = opts.argv ?? process.argv;
@@ -389124,10 +389281,6 @@ function makeProgram(opts = {}) {
389124
389281
  yield* formatFile(args2[0]);
389125
389282
  return;
389126
389283
  }
389127
- if (sub !== void 0) {
389128
- return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
389129
- }
389130
- const config2 = yield* ConfigService;
389131
389284
  const git = yield* GitService;
389132
389285
  const fs9 = yield* FileSystem_exports.FileSystem;
389133
389286
  const topLevel = yield* git.topLevel();
@@ -389140,6 +389293,50 @@ function makeProgram(opts = {}) {
389140
389293
  )
389141
389294
  );
389142
389295
  }
389296
+ const config2 = yield* ConfigService;
389297
+ if (sub === "review") {
389298
+ const args2 = argv.slice(3).filter((a5) => a5.length > 0 && !a5.startsWith("--"));
389299
+ if (args2.length === 0) {
389300
+ return yield* Effect_exports.fail(new Error("gtd review: missing target argument"));
389301
+ }
389302
+ if (args2.length > 1) {
389303
+ return yield* Effect_exports.fail(
389304
+ new Error(
389305
+ `gtd review: too many arguments \u2014 expected one target, got: ${args2.join(", ")}`
389306
+ )
389307
+ );
389308
+ }
389309
+ const target = args2[0];
389310
+ const reviewResult = yield* reviewAgainst(target).pipe(
389311
+ Effect_exports.catchAll(
389312
+ (error) => Effect_exports.fail(new Error(`gtd review: cannot resolve ref '${target}': ${error.message}`))
389313
+ )
389314
+ );
389315
+ if (reviewResult === void 0) {
389316
+ return yield* Effect_exports.fail(
389317
+ new Error(`gtd review: nothing to review (${target} diff is empty after filtering)`)
389318
+ );
389319
+ }
389320
+ const { reviewBase, refDiff } = reviewResult;
389321
+ yield* git.commitAllWithPrefix(REVIEWING_SUBJECT);
389322
+ const result = cleanResult({ reviewBase, refDiff, autoAdvance: json2 });
389323
+ const builtPrompt = buildPrompt(result, config2.resolveModel, json2 ? "json" : "plain");
389324
+ if (json2) {
389325
+ write4(
389326
+ JSON.stringify({
389327
+ state: result.state,
389328
+ autoAdvance: result.autoAdvance,
389329
+ prompt: builtPrompt
389330
+ }) + "\n"
389331
+ );
389332
+ } else {
389333
+ write4(builtPrompt);
389334
+ }
389335
+ return;
389336
+ }
389337
+ if (sub !== void 0) {
389338
+ return yield* Effect_exports.fail(new Error(`unknown command '${sub}'`));
389339
+ }
389143
389340
  let hops = 0;
389144
389341
  while (true) {
389145
389342
  hops += 1;
@@ -389150,7 +389347,22 @@ function makeProgram(opts = {}) {
389150
389347
  }
389151
389348
  const result = yield* detect();
389152
389349
  if (result.edgeAction !== void 0) {
389153
- yield* perform(result.edgeAction);
389350
+ const { stop: stop2 } = yield* perform(result.edgeAction);
389351
+ if (stop2) {
389352
+ const builtPrompt2 = buildPrompt(IDLE_RESULT, config2.resolveModel, json2 ? "json" : "plain");
389353
+ if (json2) {
389354
+ write4(
389355
+ JSON.stringify({
389356
+ state: "idle",
389357
+ autoAdvance: false,
389358
+ prompt: builtPrompt2
389359
+ }) + "\n"
389360
+ );
389361
+ } else {
389362
+ write4(builtPrompt2);
389363
+ }
389364
+ return;
389365
+ }
389154
389366
  }
389155
389367
  if (isEdgeOnly(result.state)) {
389156
389368
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
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": {