@pmelab/gtd 1.7.0 → 1.8.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 +151 -64
  2. package/dist/gtd.bundle.mjs +190 -42
  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,
@@ -138,6 +138,11 @@ across runs:
138
138
  - **ERRORS.md** — the escalation gate: persistent test-failure output that stops
139
139
  the loop for a human (written instead of FEEDBACK.md once the fix-attempt cap
140
140
  is hit; never auto-consumed).
141
+ - **HEALTH.md** — idle health-check failure output: written when `testCommand`
142
+ fails on a bare idle tree (no `.gtd`, no REVIEW.md, no FEEDBACK.md). Carries
143
+ the failure while the dedicated health-fix loop repairs it. Analogous to
144
+ FEEDBACK.md but lives on the default-branch idle path rather than inside a
145
+ build process. Never written while any other steering file is present.
141
146
  - **.gtd/** — ordered work packages (one numbered directory each) of
142
147
  parallelizable subtasks.
143
148
 
@@ -158,9 +163,11 @@ Every run derives the state in **three layers**:
158
163
  1. **Transport pre-pass** — if HEAD is `gtd: transport`, short-circuit to the
159
164
  Transport state (mixed-reset) before anything else is considered.
160
165
  2. **Steering-file precedence** — the presence of `ERRORS.md` / `FEEDBACK.md` /
161
- `.gtd/` / `REVIEW.md` drives the decision, authoritative regardless of HEAD.
166
+ `HEALTH.md` / `.gtd/` / `REVIEW.md` drives the decision, authoritative
167
+ regardless of HEAD.
162
168
  3. **HEAD bucket** — with no steering files in play, the last-commit bucket plus
163
- working-tree cleanliness selects New Feature / Grilling / Clean / Idle.
169
+ working-tree cleanliness selects New Feature / Grilling / Clean / Health
170
+ check / Idle.
164
171
 
165
172
  Within layers 2 and 3 the HEAD subject further disambiguates states the
166
173
  filesystem alone cannot separate (e.g. inside the `.gtd/` lifecycle, HEAD
@@ -173,8 +180,8 @@ The complete set:
173
180
 
174
181
  `gtd: new task` · `gtd: grilling` · `gtd: grilled` · `gtd: planning` ·
175
182
  `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).
183
+ `gtd: package done` · `gtd: awaiting review` · `gtd: done` · `gtd: health-check`
184
+ · `gtd: health-fix` — plus the hand-made `gtd: transport` (see below).
178
185
 
179
186
  The last commit subject is bucketed two ways:
180
187
 
@@ -189,24 +196,28 @@ The last commit subject is bucketed two ways:
189
196
  1. **ERRORS.md present** → Escalate (human gate; STOP).
190
197
  2. **FEEDBACK.md present** → non-empty → Fixing; **empty** (clean agentic review
191
198
  = approval) → Close package.
192
- 3. **.gtd present** → build lifecycle, routed by tree + HEAD:
199
+ 3. **HEALTH.md present** → Health Fixing (idle health-fix loop; no `.gtd`,
200
+ REVIEW.md, or FEEDBACK.md).
201
+ 4. **.gtd present** → build lifecycle, routed by tree + HEAD:
193
202
  - `.gtd` modified (package files added/edited) → Planning
194
203
  - code changes present → Testing
195
204
  - clean tree + HEAD `gtd: fixing` (no-op fixer) → Testing (re-test)
196
205
  - else clean, by HEAD: `gtd: planning` / `gtd: package done` → Building;
197
206
  `gtd: building` → Agentic Review (or Close package, if force-approved)
198
- 4. **REVIEW.md present** → review lifecycle, routed by committed-ness + tree:
207
+ 5. **REVIEW.md present** → review lifecycle, routed by committed-ness + tree:
199
208
  committed + clean → Done; committed + checkbox-only edits (only `[ ]`↔`[x]`
200
209
  flips in REVIEW.md) → Done; committed + non-checkbox pending edits → Accept
201
210
  Review; uncommitted → Await Review (commits REVIEW.md and auto-advances to
202
- Done). 4a. **HEAD `gtd: done` + `squash` enabled + squash base present + no
211
+ Done). 5a. **HEAD `gtd: done` + `squash` enabled + squash base present + no
203
212
  unrelated code dirty** (a lone untracked `SQUASH_MSG.md` is allowed) →
204
213
  Squashing; unrelated code dirty → New Feature.
205
- 5. **Boundary HEAD + pending changes** (and no `.gtd`/REVIEW/FEEDBACK), or HEAD
214
+ 6. **Boundary HEAD + pending changes** (and no `.gtd`/REVIEW/FEEDBACK), or HEAD
206
215
  `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).
216
+ 7. **TODO.md present** → Grilling / Grilled.
217
+ 8. **Boundary or `gtd: package done` HEAD + clean tree** → Clean (review the
218
+ work), **Health check** (run `testCommand` when there is nothing to review
219
+ on any branch outside a process), or Idle (health check green, nothing to
220
+ do).
210
221
 
211
222
  Anything matching no rule is corruption — `gtd` **hard-errors** rather than
212
223
  guess.
@@ -221,7 +232,10 @@ flowchart TD
221
232
  P1 -->|no| P2{"FEEDBACK.md?"}
222
233
  P2 -->|"empty = approval"| Close["Close package — rm pkg dir, gtd: package done"]:::edge
223
234
  P2 -->|"non-empty"| Fixing["Fixing — rm FEEDBACK, fixer agent"]:::agent
224
- P2 -->|absent| P3{".gtd/?"}
235
+ 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
238
+ P2b -->|absent| P3{".gtd/?"}
225
239
  P3 -->|"modified"| Planning["Planning — gtd: planning"]:::agent
226
240
  P3 -->|"code dirty / resume / no-op fixer"| Testing["Testing — gtd: building, run tests"]:::edge
227
241
  P3 -->|"clean, HEAD planning/package done"| Building["Building — pick & build one package"]:::agent
@@ -234,17 +248,21 @@ flowchart TD
234
248
  P4 -->|absent| P5{"boundary HEAD + dirty,<br/>or gtd: new task + clean?"}
235
249
  Done -->|"squash enabled"| Squashing["Squashing — reset --soft base, squash commit"]:::agent
236
250
  Done -->|"squash disabled"| Idle
237
- Squashing --> Idle["Idle — nothing to do (STOP; review on next manual gtd run)"]:::gate
251
+ Squashing --> Idle["Idle — nothing to do (STOP)"]:::gate
238
252
  P5 -->|yes| NewFeature["New Feature — gtd: new task, revert, seed TODO"]:::edge
239
253
  P5 -->|no| P6{"TODO.md?"}
240
254
  P6 -->|"open markers"| GrillStop["Grilling — gtd: grilling, STOP for answers"]:::gate
241
255
  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
256
+ P6 -->|"clean, no markers"| Grilled["Grilled — gtd: grilled, decompose"]:::agent
245
257
  P6 -->|absent| P7{"clean + boundary/package-done HEAD,<br/>reviewable diff?"}
246
258
  P7 -->|yes| CleanState["Clean — write REVIEW.md"]:::agent
247
- P7 -->|no| Idle
259
+ P7 -->|"no (idle, outside a process)"| HealthCheck["Health check — run testCommand"]:::edge
260
+ HealthCheck -->|"green, no health-fix"| Idle
261
+ HealthCheck -->|"green + ≥1 health-fix, squash enabled"| Squashing
262
+ HealthCheck -->|"red, below cap"| HealthMd["write HEALTH.md, gtd: health-check"]:::edge
263
+ HealthMd -.->|"re-resolve"| HealthFix
264
+ HealthCheck -->|"red, at cap"| ErrorsMd["write ERRORS.md, gtd: health-check"]:::edge
265
+ ErrorsMd -.->|"re-resolve"| Escalate
248
266
  classDef edge fill:#1a4a6b,color:#fff
249
267
  classDef agent fill:#2d6a4f,color:#fff
250
268
  classDef gate fill:#7a3b1d,color:#fff
@@ -265,6 +283,10 @@ guessing:
265
283
  - FEEDBACK.md without .gtd
266
284
  - ERRORS.md + FEEDBACK.md
267
285
  - ERRORS.md without .gtd
286
+ - HEALTH.md + .gtd (health check only runs from a bare idle tree)
287
+ - HEALTH.md + REVIEW.md (same)
288
+ - HEALTH.md + FEEDBACK.md (same)
289
+ - HEALTH.md + ERRORS.md (escalation wins; HEALTH.md must not coexist)
268
290
 
269
291
  Legal coexistence: `.gtd` + TODO.md (plan kept alongside packages during
270
292
  **Planning** only — TODO.md is deleted at the first Building turn);
@@ -292,7 +314,7 @@ back into the working tree uncommitted, and re-derives state from scratch. If
292
314
  the transport commit is the repository's root commit (no parent), `gtd` fails
293
315
  immediately with a clear error instead of looping.
294
316
 
295
- ## The 18 states
317
+ ## The 19 states
296
318
 
297
319
  Each state has a **condition** (when it wins), a deterministic **action**, the
298
320
  **commit(s)** it produces, and where it **advances**. States marked
@@ -300,35 +322,36 @@ Each state has a **condition** (when it wins), a deterministic **action**, the
300
322
  human; **edge-only** states render no prompt at all — the driver performs their
301
323
  action and re-resolves silently.
302
324
 
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 | |
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 editsHealth 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, elseBuilding |
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) | — |
323
346
 
324
347
  Every prompt also embeds the current `git diff HEAD` (untracked files included)
325
348
  inline, plus the last commit subject and working-tree status, so the agent has
326
349
  full context.
327
350
 
328
- ### Review base — four rules
351
+ ### Review base — three rules
329
352
 
330
353
  The review base (the commit whose diff to HEAD forms the REVIEW.md) is chosen by
331
- four rules evaluated in priority order:
354
+ three rules evaluated in priority order:
332
355
 
333
356
  1. **Within a process, first review** — a `gtd: grilling` commit exists after
334
357
  the last `gtd: done` (or task start), but no `gtd: awaiting review` yet →
@@ -337,18 +360,16 @@ four rules evaluated in priority order:
337
360
  2. **Within a process, incremental** — `gtd: awaiting review` also present in
338
361
  the current cycle (takes precedence over rule 1) → base = last
339
362
  `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.
363
+ 3. **Outside a process (any branch)** — no `gtd: grilling` after the last
364
+ `gtd: done` skip review; `reviewBase`/`refDiff` unsetIdle (the health
365
+ check runs instead).
345
366
 
346
367
  In all cases, if the diff from the chosen base to HEAD is empty,
347
368
  `reviewBase`/`refDiff` are left unset and the machine settles in Idle.
348
369
 
349
370
  ## The fix loops & counter folds
350
371
 
351
- Two derived counters drive the budgeted loops. Both are **folded in the
372
+ Three derived counters drive the budgeted loops. All are **folded in the
352
373
  machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
353
374
 
354
375
  - **`testFixCount`** — `gtd: errors` commits (test-fix attempts) since the
@@ -358,6 +379,9 @@ machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
358
379
  round, and every human resume starts a **fresh budget**.
359
380
  - **`reviewFixCount`** — `gtd: feedback` commits (review-fix rounds) since the
360
381
  most recent package start.
382
+ - **`healthFixCount`** — `gtd: health-check` commits since the most recent
383
+ commit that removed HEALTH.md (or the start of branch history if none). Reuses
384
+ `fixAttemptCap` — no separate config key.
361
385
 
362
386
  ### Test-fix loop (`fixAttemptCap`, default 3)
363
387
 
@@ -381,6 +405,31 @@ ERRORS.md — which **resets the fix-attempt budget** (the next run re-tests and
381
405
  grants a fresh `cap` attempts before escalating again). While ERRORS.md exists,
382
406
  every run resolves straight back to Escalate.
383
407
 
408
+ ### Health-fix loop (`fixAttemptCap`, `squash` — no new config)
409
+
410
+ Outside a process (any branch), when there is no reviewable diff and no steering
411
+ files, `gtd` runs `testCommand` instead of stopping. This reuses `fixAttemptCap`
412
+ (default 3) and `squash` — no new config keys are introduced.
413
+
414
+ ```
415
+ Idle path → Health check(red) → Health Fixing → Health check(red) → … → Health check(green)
416
+ │ │
417
+ └── below cap: HEALTH.md, gtd: health-check ─────────────────┘
418
+ └── at/over the cap: ERRORS.md, gtd: health-check → Escalate
419
+ green + ≥1 health-fix → Squashing (if squash enabled) → Idle
420
+ ```
421
+
422
+ - **green, no prior `gtd: health-fix`** → Idle immediately (no commit).
423
+ - **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.
426
+ - **red, at or over `fixAttemptCap`** → write test output to ERRORS.md, commit
427
+ `gtd: health-check` → **Escalate** (human gate). Delete ERRORS.md to reset the
428
+ budget and resume.
429
+ - **green, ≥1 `gtd: health-fix` present** → health-fix cycle converged: if
430
+ `squash` is enabled → **Squashing** (squash base = parent of the first
431
+ `gtd: health-check` of the current run); otherwise → Idle (no commit).
432
+
384
433
  ### Review-fix loop & agentic review (`reviewThreshold`, default 3)
385
434
 
386
435
  After a green test run, **Agentic Review** reviews the package's accumulated
@@ -425,11 +474,9 @@ loop before the next one starts.
425
474
  your answer, and run `gtd` again. The agent integrates answers, moves them to
426
475
  `## Resolved`, and raises fresh questions — repeat until none remain (it
427
476
  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`).
477
+ 4. **Converge.** A clean tree with no markers resolves to **Grilled**
478
+ (`gtd: grilled`), then **Planning** decomposes `TODO.md` into ordered `.gtd/`
479
+ work packages (`gtd: planning`).
433
480
  5. **Build.** Run `gtd` — **Building** first deletes `TODO.md` (when HEAD is
434
481
  `gtd: planning` and it is still present, committed under the same
435
482
  `gtd: planning` prefix — fires once). It then names the single next package
@@ -447,9 +494,9 @@ loop before the next one starts.
447
494
  (`gtd: done`) → **Squashing** → **Idle**. The Squashing agent authors a
448
495
  conventional-commits message from the full process diff and squashes all
449
496
  intermediate `gtd: *` commits into one with `git reset --soft <base>` +
450
- `git commit`, then **gtd STOPs**. The base is the parent of the current
451
- cycle's start marker **nearest to HEAD** (the last `gtd: new task`; for
452
- legacy cycles the last contiguous `gtd: grilling` run), and on a feature
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
453
500
  branch it never reaches below the merge-base with the default branch — stray
454
501
  markers left behind by older squashes can never drag the squash into
455
502
  previously shipped features. Post-squash review does not fire automatically —
@@ -458,13 +505,20 @@ loop before the next one starts.
458
505
  has no unrelated code dirty — a lone untracked `SQUASH_MSG.md` is tolerated
459
506
  and deleted before the squash commit. If unrelated code is dirty at
460
507
  `gtd: done`, gtd routes to **New Feature** instead. Set `squash: false` in
461
- `.gtdrc` to skip squashing and go straight to Idle. Checking off REVIEW.md
462
- checkboxes (`- [ ]` `- [x]`) also counts as approval and routes to **Done**
463
- they are navigation aids, not feedback. Only **non-checkbox** edits (code
464
- changes, inline comments, textual annotations in REVIEW.md) trigger **Accept
465
- Review**, which seeds a fresh `TODO.md` from your feedback, discards your
466
- code edits, removes `REVIEW.md`, and re-enters Grilling the loop starts
467
- over.
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.
468
522
 
469
523
  ## Configuration
470
524
 
@@ -482,11 +536,13 @@ built-in defaults apply. Supported filenames (searched in this order):
482
536
  ### Schema
483
537
 
484
538
  - **`testCommand`** (string, default `npm run test`) — the command the edge runs
485
- in the Testing state to verify a built package.
539
+ in the Testing state to verify a built package, and in the Health check state
540
+ on the default-branch idle path.
486
541
  - **`fixAttemptCap`** (non-negative integer, default `3`) — the test-fix budget:
487
542
  how many `gtd: errors` attempts are allowed per sub-loop before the failure is
488
543
  escalated to ERRORS.md (Escalate). `0` disables the cap (escalates immediately
489
- on the first red run).
544
+ on the first red run). Also reused as the health-fix budget (no separate
545
+ config key).
490
546
  - **`reviewThreshold`** (integer ≥ 1, default `3`) — the review-fix budget: how
491
547
  many `gtd: feedback` rounds are allowed per package before Agentic Review
492
548
  force-approves.
@@ -812,6 +868,37 @@ Run `npm run test:mutation` after making changes to the mutated files to check
812
868
  whether surviving mutants increased. The HTML report lands in
813
869
  `reports/mutation/mutation.html` (git-ignored).
814
870
 
871
+ ### Mutation testing
872
+
873
+ Run mutation testing on-demand with `npm run test:mutation` (StrykerJS, ~2 min).
874
+ The single `stryker.config.json` mutates six core files:
875
+
876
+ ```
877
+ src/Machine.ts src/Prompt.ts src/Config.ts
878
+ src/Format.ts src/State.ts src/Events.ts
879
+ ```
880
+
881
+ `src/Git.ts` is excluded: the Cucumber harness stubs git at the Effect boundary,
882
+ so Git.ts mutants have zero in-memory coverage. Measuring its post-refactor
883
+ Live-tier kill rate is a follow-up before re-including it.
884
+
885
+ **`process.chdir()` gotcha (resolved).** `@stryker-mutator/vitest-runner`
886
+ hardcodes `pool: 'threads'` internally, and `process.chdir()` is unsupported in
887
+ worker threads. Before the cwd refactor (package 01), four test files
888
+ (`Events.test.ts`, `Git.test.ts`, `Config.test.ts`, `TestRunner.test.ts`) had to
889
+ be excluded from all Stryker runs. The refactor eliminated those calls, letting
890
+ all four files rejoin the run.
891
+
892
+ Two additional notes: `vitest.related` is disabled for feature-file runs because
893
+ feature files don't import source files directly (Stryker's coverage-based
894
+ filtering would assign zero tests to every mutant). Compile-error mutants are
895
+ counted as kills by the TypeScript checker — they represent real signal, not a
896
+ configuration problem.
897
+
898
+ Run `npm run test:mutation` after making changes to the mutated files to check
899
+ whether surviving mutants increased. The HTML report lands in
900
+ `reports/mutation/mutation.html` (git-ignored).
901
+
815
902
  ## Releasing
816
903
 
817
904
  Releases are automatic. Push releasable Conventional Commits (`fix:`, `feat:`,
@@ -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"
@@ -368536,13 +368535,11 @@ eta.loadTemplate("@agentic-review", agentic_review_default);
368536
368535
  eta.loadTemplate("@clean", clean_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",
@@ -368550,7 +368547,8 @@ var STATE_TEMPLATE = {
368550
368547
  clean: "@clean",
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;
@@ -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";
@@ -388284,6 +388285,7 @@ var STEERING_FILES = [
388284
388285
  REVIEW_FILE,
388285
388286
  FEEDBACK_FILE,
388286
388287
  ERRORS_FILE,
388288
+ HEALTH_FILE,
388287
388289
  SQUASH_MSG_FILE
388288
388290
  ];
388289
388291
  var WORKFLOW_FILE_EXCLUDES = [...STEERING_FILES, GTD_DIR];
@@ -388473,7 +388475,9 @@ var gatherEvents = () => (
388473
388475
  isFeedback: subject === FEEDBACK_SUBJECT,
388474
388476
  isPackageStart: subject === PLANNING_SUBJECT || subject === PACKAGE_DONE_SUBJECT,
388475
388477
  isWorkflowCommit: subject.startsWith("gtd: "),
388476
- removedErrors
388478
+ removedErrors,
388479
+ isHealthCheck: subject === HEALTH_CHECK_SUBJECT,
388480
+ isHealthFix: subject === HEALTH_FIX_SUBJECT
388477
388481
  };
388478
388482
  }
388479
388483
  );
@@ -388547,10 +388551,6 @@ var gatherEvents = () => (
388547
388551
  candidate = firstGrilling.hash ?? EMPTY_TREE;
388548
388552
  }
388549
388553
  } 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
388554
  }
388555
388555
  if (candidate !== void 0 && hasCommitsAfterLastDone) {
388556
388556
  const candidateDiff = yield* git.diffRef(candidate, WORKFLOW_FILE_EXCLUDES).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
@@ -388606,6 +388606,31 @@ var gatherEvents = () => (
388606
388606
  }
388607
388607
  const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388608
388608
  const squashMsgContent = squashMsgPresent ? yield* fs9.readFileString(resolve5(SQUASH_MSG_FILE)) : "";
388609
+ const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388610
+ const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388611
+ const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388612
+ let healthFixBase;
388613
+ if (config2.squash) {
388614
+ const subjectOf = (c7) => (c7.message.split("\n")[0] ?? "").trim();
388615
+ let firstHealthCheckHash;
388616
+ let healthCheckCount = 0;
388617
+ for (const commit3 of history) {
388618
+ const s5 = subjectOf(commit3);
388619
+ const isPackageStart = s5 === PLANNING_SUBJECT || s5 === PACKAGE_DONE_SUBJECT;
388620
+ if (isPackageStart || commit3.removedErrors) {
388621
+ firstHealthCheckHash = void 0;
388622
+ healthCheckCount = 0;
388623
+ }
388624
+ if (s5 === HEALTH_CHECK_SUBJECT) {
388625
+ healthCheckCount++;
388626
+ if (healthCheckCount === 1) firstHealthCheckHash = commit3.hash;
388627
+ }
388628
+ }
388629
+ if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
388630
+ const base2 = yield* git.resolveRef(`${firstHealthCheckHash}~1`).pipe(Effect_exports.catchAll(() => Effect_exports.succeed(EMPTY_TREE)));
388631
+ healthFixBase = base2;
388632
+ }
388633
+ }
388609
388634
  const payload = {
388610
388635
  todoExists,
388611
388636
  todoCommitted,
@@ -388637,7 +388662,11 @@ var gatherEvents = () => (
388637
388662
  ...squashBase !== void 0 ? { squashBase } : {},
388638
388663
  ...squashDiff !== void 0 ? { squashDiff } : {},
388639
388664
  squashMsgPresent,
388640
- squashMsgContent
388665
+ squashMsgContent,
388666
+ healthPresent,
388667
+ healthContent,
388668
+ healthCommitted,
388669
+ ...healthFixBase !== void 0 ? { healthFixBase } : {}
388641
388670
  };
388642
388671
  const resolveEvent = { type: "RESOLVE", payload };
388643
388672
  return [...commitEvents, resolveEvent];
@@ -388667,7 +388696,7 @@ var perform = (action) => (
388667
388696
  // work in the tree, then re-derive. (No producer command — consume only.)
388668
388697
  case "transportReset": {
388669
388698
  yield* git.mixedResetHead();
388670
- return;
388699
+ return { stop: false };
388671
388700
  }
388672
388701
  // New Feature: capture the raw input as `gtd: new task` (unless HEAD is
388673
388702
  // already there — the lost-seed regenerate case), revert it back to a
@@ -388675,7 +388704,7 @@ var perform = (action) => (
388675
388704
  case "seedNewFeature": {
388676
388705
  const captured = yield* captureAndRevert(git, NEW_TASK_SUBJECT, Effect_exports.void);
388677
388706
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388678
- return;
388707
+ return { stop: false };
388679
388708
  }
388680
388709
  // Accept Review: capture the human's pending changeset durably as
388681
388710
  // `gtd: review feedback` (annotations, code edits, and new files alike),
@@ -388687,7 +388716,7 @@ var perform = (action) => (
388687
388716
  const captured = yield* captureAndRevert(git, REVIEW_FEEDBACK_SUBJECT, git.resetHard());
388688
388717
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388689
388718
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388690
- return;
388719
+ return { stop: false };
388691
388720
  }
388692
388721
  // Grilling capture: the plan is already committed and the user sketched
388693
388722
  // code during the round. Fold the code diff (untracked included, steering
@@ -388710,7 +388739,7 @@ var perform = (action) => (
388710
388739
  }
388711
388740
  yield* fs9.writeFileString(resolve5(TODO_FILE), appendCapturedInput(todoNow, captured));
388712
388741
  yield* git.commitAllWithPrefix(GRILLING_SUBJECT);
388713
- return;
388742
+ return { stop: false };
388714
388743
  }
388715
388744
  // Testing: commit the pending tree `gtd: building` (nothing pending in the
388716
388745
  // no-op-fixer case), run tests; on red write FEEDBACK (below cap) or ERRORS
@@ -388727,13 +388756,46 @@ var perform = (action) => (
388727
388756
  if (!committedBuilding) {
388728
388757
  yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
388729
388758
  }
388730
- return;
388759
+ return { stop: false };
388731
388760
  }
388732
388761
  const target = action.capReached ? ERRORS_FILE : FEEDBACK_FILE;
388733
388762
  const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388734
388763
  yield* fs9.writeFileString(resolve5(target), body);
388735
388764
  yield* git.commitAllWithPrefix(ERRORS_SUBJECT);
388736
- return;
388765
+ return { stop: false };
388766
+ }
388767
+ // Health check: run tests on an idle/clean tree.
388768
+ // Green, no prior fixes → stop immediately (no commit).
388769
+ // Green, fixes exist → stop immediately (squash handled via healthFixBase
388770
+ // when gatherEvents sets it; the machine resolves squashing state).
388771
+ // Red below cap → write HEALTH.md (uncommitted); resolveHealth will
388772
+ // commit it as `gtd: health-check` via `commitPending removeHealth`.
388773
+ // Red at cap → write ERRORS.md and commit `gtd: health-check` immediately
388774
+ // so the Escalate state fires on the next resolve.
388775
+ case "runHealthCheck": {
388776
+ if (action.commitErrorsReset === true) {
388777
+ yield* git.commitAllWithPrefix(HEALTH_FIX_SUBJECT);
388778
+ }
388779
+ const runner = yield* TestRunner;
388780
+ const result = yield* runner.run();
388781
+ if (result.exitCode === 0) {
388782
+ if (action.healthFixBase !== void 0 && action.commitErrorsReset !== true) {
388783
+ yield* fs9.writeFileString(
388784
+ resolve5(SQUASH_MSG_FILE),
388785
+ "chore: health-check cycle squash\n"
388786
+ );
388787
+ return { stop: false };
388788
+ }
388789
+ return { stop: true };
388790
+ }
388791
+ const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388792
+ if (action.capReached) {
388793
+ yield* fs9.writeFileString(resolve5(ERRORS_FILE), body);
388794
+ yield* git.commitAllWithPrefix(HEALTH_CHECK_SUBJECT);
388795
+ } else {
388796
+ yield* fs9.writeFileString(resolve5(HEALTH_FILE), body);
388797
+ }
388798
+ return { stop: false };
388737
388799
  }
388738
388800
  // Grilling / Grilled / Planning / Fixing: commit the pending tree under a
388739
388801
  // fixed phase prefix. Fixing sets `removeFeedback` so FEEDBACK.md's removal
@@ -388747,11 +388809,14 @@ var perform = (action) => (
388747
388809
  if (action.removeTodo === true) {
388748
388810
  yield* fs9.remove(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388749
388811
  }
388812
+ if (action.removeHealth === true) {
388813
+ yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388814
+ }
388750
388815
  if (action.removeTodo !== true) {
388751
388816
  yield* formatFile(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388752
388817
  }
388753
388818
  yield* git.commitAllWithPrefix(action.prefix);
388754
- return;
388819
+ return { stop: false };
388755
388820
  }
388756
388821
  // Close package: remove the (maybe-empty / maybe-absent) FEEDBACK.md, rm
388757
388822
  // the first (finished) package dir (+ the now-empty `.gtd/`), commit
@@ -388764,19 +388829,19 @@ var perform = (action) => (
388764
388829
  yield* git.removePackageDir(`${GTD_DIR}/${first2.name}`);
388765
388830
  }
388766
388831
  yield* git.commitAllWithPrefix(PACKAGE_DONE_SUBJECT);
388767
- return;
388832
+ return { stop: false };
388768
388833
  }
388769
388834
  // Await Review: commit REVIEW.md as `gtd: awaiting review`.
388770
388835
  case "commitReview": {
388771
388836
  yield* formatFile(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388772
388837
  yield* git.commitAllWithPrefix(AWAITING_REVIEW_SUBJECT);
388773
- return;
388838
+ return { stop: false };
388774
388839
  }
388775
388840
  // Done: rm REVIEW.md, commit `gtd: done`.
388776
388841
  case "done": {
388777
388842
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388778
388843
  yield* git.commitAllWithPrefix(DONE_SUBJECT);
388779
- return;
388844
+ return { stop: false };
388780
388845
  }
388781
388846
  // Squash: rm SQUASH_MSG.md, soft-reset to squashBase, commit everything
388782
388847
  // under the provided commit message.
@@ -388784,7 +388849,7 @@ var perform = (action) => (
388784
388849
  yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388785
388850
  yield* git.softResetTo(action.squashBase);
388786
388851
  yield* git.commitAllWithPrefix(action.commitMessage);
388787
- return;
388852
+ return { stop: false };
388788
388853
  }
388789
388854
  }
388790
388855
  }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
@@ -388802,14 +388867,17 @@ var GtdStateError = class extends Error {
388802
388867
  var foldCounters = (events) => {
388803
388868
  let testFixCount = 0;
388804
388869
  let reviewFixCount = 0;
388870
+ let healthFixCount = 0;
388805
388871
  for (const event of events) {
388806
388872
  if (event.type !== "COMMIT") continue;
388807
388873
  if (event.isPackageStart || event.isFeedback || event.removedErrors) testFixCount = 0;
388808
388874
  if (event.isErrors) testFixCount += 1;
388809
388875
  if (event.isPackageStart) reviewFixCount = 0;
388810
388876
  if (event.isFeedback) reviewFixCount += 1;
388877
+ if (event.isPackageStart || event.removedErrors) healthFixCount = 0;
388878
+ if (event.isHealthCheck) healthFixCount += 1;
388811
388879
  }
388812
- return { testFixCount, reviewFixCount };
388880
+ return { testFixCount, reviewFixCount, healthFixCount };
388813
388881
  };
388814
388882
  var isBoundary = (subject) => !subject.startsWith("gtd: ") || subject === "gtd: done";
388815
388883
  var DEFAULT_PAYLOAD = {
@@ -388839,7 +388907,10 @@ var DEFAULT_PAYLOAD = {
388839
388907
  reviewThreshold: 3,
388840
388908
  squashEnabled: false,
388841
388909
  squashMsgPresent: false,
388842
- squashMsgContent: ""
388910
+ squashMsgContent: "",
388911
+ healthPresent: false,
388912
+ healthContent: "",
388913
+ healthCommitted: false
388843
388914
  };
388844
388915
  var buildContext = (p4, counters, grillingCase) => ({
388845
388916
  testFixCount: counters.testFixCount,
@@ -388852,7 +388923,7 @@ var buildContext = (p4, counters, grillingCase) => ({
388852
388923
  ...p4.squashDiff !== void 0 ? { squashDiff: p4.squashDiff } : {},
388853
388924
  lastCommitSubject: p4.lastCommitSubject,
388854
388925
  workingTreeClean: p4.workingTreeClean,
388855
- feedbackContent: p4.feedbackContent,
388926
+ feedbackContent: p4.feedbackContent !== "" ? p4.feedbackContent : p4.healthContent,
388856
388927
  ...grillingCase !== void 0 ? { grillingCase } : {}
388857
388928
  });
388858
388929
  var assertLegal = (p4) => {
@@ -388866,7 +388937,32 @@ var assertLegal = (p4) => {
388866
388937
  if (p4.feedbackPresent && p4.reviewPresent) fail20("illegal combination: FEEDBACK.md + REVIEW.md");
388867
388938
  if (p4.feedbackPresent && !p4.gtdDirExists) fail20("illegal combination: FEEDBACK.md without .gtd");
388868
388939
  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");
388940
+ const isHealthCapEscalation = p4.lastCommitSubject === "gtd: health-check" || p4.lastCommitSubject === "gtd: health-fix";
388941
+ if (p4.errorsPresent && !p4.gtdDirExists && !isHealthCapEscalation)
388942
+ fail20("illegal combination: ERRORS.md without .gtd");
388943
+ if (p4.healthPresent && p4.gtdDirExists) fail20("illegal combination: HEALTH.md + .gtd");
388944
+ if (p4.healthPresent && p4.reviewPresent) fail20("illegal combination: HEALTH.md + REVIEW.md");
388945
+ if (p4.healthPresent && p4.feedbackPresent) fail20("illegal combination: HEALTH.md + FEEDBACK.md");
388946
+ if (p4.healthPresent && p4.errorsPresent) fail20("illegal combination: HEALTH.md + ERRORS.md");
388947
+ };
388948
+ var resolveHealth = (p4, counters) => {
388949
+ const hasPendingWork = !p4.healthCommitted || !p4.workingTreeClean;
388950
+ const prefix = p4.healthCommitted ? "gtd: health-fix" : "gtd: health-check";
388951
+ return {
388952
+ state: "health-fixing",
388953
+ autoAdvance: true,
388954
+ ...hasPendingWork ? {
388955
+ edgeAction: {
388956
+ kind: "commitPending",
388957
+ prefix,
388958
+ // Always remove HEALTH.md: on gtd: health-check this clears it so
388959
+ // the next run re-enters resolveCleanOrIdle; on gtd: health-fix it
388960
+ // clears the marker left by a prior health-check run.
388961
+ removeHealth: true
388962
+ }
388963
+ } : {},
388964
+ context: buildContext(p4, counters)
388965
+ };
388870
388966
  };
388871
388967
  var resolveFeedback = (p4, counters) => {
388872
388968
  if (p4.feedbackEmpty) {
@@ -388992,22 +389088,21 @@ var resolveGrilling = (p4, counters, head7) => {
388992
389088
  context: buildContext(p4, counters, "iterate")
388993
389089
  };
388994
389090
  }
388995
- if (head7 === "gtd: grilled") {
388996
- return {
388997
- state: "grilled",
388998
- autoAdvance: true,
388999
- context: buildContext(p4, counters)
389000
- };
389001
- }
389002
389091
  return {
389003
- state: "grilled-review",
389004
- autoAdvance: false,
389092
+ state: "grilled",
389093
+ autoAdvance: true,
389005
389094
  edgeAction: { kind: "commitPending", prefix: "gtd: grilled" },
389006
389095
  context: buildContext(p4, counters)
389007
389096
  };
389008
389097
  };
389009
389098
  var resolveCleanOrIdle = (p4, counters, head7) => {
389010
- if (!p4.workingTreeClean || !isBoundary(head7) && head7 !== "gtd: package done") return null;
389099
+ const isHealthHead = head7 === "gtd: health-check" || head7 === "gtd: health-fix";
389100
+ if (!p4.workingTreeClean && !p4.pendingErrorsDeletion) return null;
389101
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && p4.gtdDirExists) return null;
389102
+ if (!p4.workingTreeClean && p4.pendingErrorsDeletion && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389103
+ return null;
389104
+ if (p4.workingTreeClean && !isBoundary(head7) && head7 !== "gtd: package done" && !isHealthHead)
389105
+ return null;
389011
389106
  if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0) {
389012
389107
  if (p4.squashMsgPresent) {
389013
389108
  return {
@@ -389024,9 +389119,20 @@ var resolveCleanOrIdle = (p4, counters, head7) => {
389024
389119
  return { state: "squashing", autoAdvance: true, context: buildContext(p4, counters) };
389025
389120
  }
389026
389121
  const reviewable = p4.hasCommitsAfterLastDone && p4.reviewBase !== void 0 && (p4.refDiff ?? "").trim().length > 0;
389122
+ if (reviewable) {
389123
+ return { state: "clean", autoAdvance: false, context: buildContext(p4, counters) };
389124
+ }
389125
+ const squashHealth = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
389027
389126
  return {
389028
- state: reviewable ? "clean" : "idle",
389029
- autoAdvance: false,
389127
+ state: "health-check",
389128
+ autoAdvance: true,
389129
+ edgeAction: {
389130
+ kind: "runHealthCheck",
389131
+ errorCount: counters.healthFixCount,
389132
+ capReached: counters.healthFixCount >= p4.fixAttemptCap,
389133
+ ...squashHealth ? { healthFixBase: p4.healthFixBase } : {},
389134
+ ...p4.pendingErrorsDeletion ? { commitErrorsReset: true } : {}
389135
+ },
389030
389136
  context: buildContext(p4, counters)
389031
389137
  };
389032
389138
  };
@@ -389053,6 +389159,7 @@ var resolve4 = (events) => {
389053
389159
  }
389054
389160
  if (p4.errorsPresent)
389055
389161
  return { state: "escalate", autoAdvance: false, context: buildContext(p4, counters) };
389162
+ if (p4.healthPresent) return resolveHealth(p4, counters);
389056
389163
  if (p4.feedbackPresent) return resolveFeedback(p4, counters);
389057
389164
  if (p4.gtdDirExists) return resolveGtdLifecycle(p4, counters, head7, corrupt);
389058
389165
  if (p4.reviewPresent) return resolveReviewLifecycle(p4, counters, head7);
@@ -389068,6 +389175,18 @@ var resolve4 = (events) => {
389068
389175
  context: buildContext(p4, counters)
389069
389176
  };
389070
389177
  }
389178
+ if (p4.squashEnabled && p4.healthFixBase !== void 0 && p4.squashMsgPresent && !p4.codeDirty) {
389179
+ return {
389180
+ state: "squashing",
389181
+ autoAdvance: false,
389182
+ edgeAction: {
389183
+ kind: "squashCommit",
389184
+ squashBase: p4.healthFixBase,
389185
+ commitMessage: p4.squashMsgContent
389186
+ },
389187
+ context: buildContext(p4, counters)
389188
+ };
389189
+ }
389071
389190
  if (isBoundary(head7) && !p4.workingTreeClean && !p4.todoCommitted || head7 === "gtd: new task" && p4.workingTreeClean) {
389072
389191
  return {
389073
389192
  state: "new-feature",
@@ -389088,7 +389207,8 @@ var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
389088
389207
  "await-review",
389089
389208
  "accept-review",
389090
389209
  "close-package",
389091
- "done"
389210
+ "done",
389211
+ "health-check"
389092
389212
  ]);
389093
389213
  var isEdgeOnly = (state) => EDGE_ONLY_STATES2.has(state);
389094
389214
  var detect = () => Effect_exports.gen(function* () {
@@ -389100,6 +389220,19 @@ var detect = () => Effect_exports.gen(function* () {
389100
389220
  });
389101
389221
 
389102
389222
  // src/program.ts
389223
+ var IDLE_RESULT = {
389224
+ state: "idle",
389225
+ autoAdvance: false,
389226
+ context: {
389227
+ testFixCount: 0,
389228
+ reviewFixCount: 0,
389229
+ packages: DEFAULT_PAYLOAD.packages,
389230
+ diff: DEFAULT_PAYLOAD.diff,
389231
+ lastCommitSubject: DEFAULT_PAYLOAD.lastCommitSubject,
389232
+ workingTreeClean: DEFAULT_PAYLOAD.workingTreeClean,
389233
+ feedbackContent: DEFAULT_PAYLOAD.feedbackContent
389234
+ }
389235
+ };
389103
389236
  var MAX_EDGE_HOPS = 100;
389104
389237
  function makeProgram(opts = {}) {
389105
389238
  const argv = opts.argv ?? process.argv;
@@ -389150,7 +389283,22 @@ function makeProgram(opts = {}) {
389150
389283
  }
389151
389284
  const result = yield* detect();
389152
389285
  if (result.edgeAction !== void 0) {
389153
- yield* perform(result.edgeAction);
389286
+ const { stop: stop2 } = yield* perform(result.edgeAction);
389287
+ if (stop2) {
389288
+ const builtPrompt2 = buildPrompt(IDLE_RESULT, config2.resolveModel, json2 ? "json" : "plain");
389289
+ if (json2) {
389290
+ write4(
389291
+ JSON.stringify({
389292
+ state: "idle",
389293
+ autoAdvance: false,
389294
+ prompt: builtPrompt2
389295
+ }) + "\n"
389296
+ );
389297
+ } else {
389298
+ write4(builtPrompt2);
389299
+ }
389300
+ return;
389301
+ }
389154
389302
  }
389155
389303
  if (isEdgeOnly(result.state)) {
389156
389304
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "1.7.0",
3
+ "version": "1.8.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": {