@pmelab/gtd 1.6.1 → 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.
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 17 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,7 +248,7 @@ 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
@@ -242,7 +256,13 @@ flowchart TD
242
256
  P6 -->|"clean, no markers"| Grilled["Grilled — gtd: grilled, decompose"]:::agent
243
257
  P6 -->|absent| P7{"clean + boundary/package-done HEAD,<br/>reviewable diff?"}
244
258
  P7 -->|yes| CleanState["Clean — write REVIEW.md"]:::agent
245
- 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
246
266
  classDef edge fill:#1a4a6b,color:#fff
247
267
  classDef agent fill:#2d6a4f,color:#fff
248
268
  classDef gate fill:#7a3b1d,color:#fff
@@ -263,6 +283,10 @@ guessing:
263
283
  - FEEDBACK.md without .gtd
264
284
  - ERRORS.md + FEEDBACK.md
265
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)
266
290
 
267
291
  Legal coexistence: `.gtd` + TODO.md (plan kept alongside packages during
268
292
  **Planning** only — TODO.md is deleted at the first Building turn);
@@ -290,7 +314,7 @@ back into the working tree uncommitted, and re-derives state from scratch. If
290
314
  the transport commit is the repository's root commit (no parent), `gtd` fails
291
315
  immediately with a clear error instead of looping.
292
316
 
293
- ## The 17 states
317
+ ## The 19 states
294
318
 
295
319
  Each state has a **condition** (when it wins), a deterministic **action**, the
296
320
  **commit(s)** it produces, and where it **advances**. States marked
@@ -298,34 +322,36 @@ Each state has a **condition** (when it wins), a deterministic **action**, the
298
322
  human; **edge-only** states render no prompt at all — the driver performs their
299
323
  action and re-resolves silently.
300
324
 
301
- | State | Kind | Wins when | Action & commit | Advances to |
302
- | ------------------ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
303
- | **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 |
304
- | **Escalate** | STOP | ERRORS.md present | none | held until the human deletes ERRORS.md |
305
- | **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 |
306
- | **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 |
307
- | **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning` | commit the `.gtd/` changes `gtd: planning` | continue decomposing, elseBuilding |
308
- | **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 |
309
- | **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 |
310
- | **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 |
311
- | **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 |
312
- | **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) |
313
- | **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 |
314
- | **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean) | commit REVIEW.md `gtd: awaiting review` | Done (auto, same run) |
315
- | **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 |
316
- | **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 |
317
- | **Grilled** | agent, auto | TODO.md present, no markers, clean tree | commit pending `gtd: grilled` | decompose into `.gtd/`Planning |
318
- | **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 |
319
- | **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) | — |
320
346
 
321
347
  Every prompt also embeds the current `git diff HEAD` (untracked files included)
322
348
  inline, plus the last commit subject and working-tree status, so the agent has
323
349
  full context.
324
350
 
325
- ### Review base — four rules
351
+ ### Review base — three rules
326
352
 
327
353
  The review base (the commit whose diff to HEAD forms the REVIEW.md) is chosen by
328
- four rules evaluated in priority order:
354
+ three rules evaluated in priority order:
329
355
 
330
356
  1. **Within a process, first review** — a `gtd: grilling` commit exists after
331
357
  the last `gtd: done` (or task start), but no `gtd: awaiting review` yet →
@@ -334,18 +360,16 @@ four rules evaluated in priority order:
334
360
  2. **Within a process, incremental** — `gtd: awaiting review` also present in
335
361
  the current cycle (takes precedence over rule 1) → base = last
336
362
  `gtd: awaiting review`; `refDiff` spans only the post-review changes.
337
- 3. **Outside a process, feature branch** — no `gtd: grilling` after the last
338
- `gtd: done`, and HEAD is not on the default branch base = merge-base with
339
- the default branch; `refDiff` spans the whole branch.
340
- 4. **Outside a process, default branch** — no process active and HEAD is on the
341
- 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).
342
366
 
343
367
  In all cases, if the diff from the chosen base to HEAD is empty,
344
368
  `reviewBase`/`refDiff` are left unset and the machine settles in Idle.
345
369
 
346
370
  ## The fix loops & counter folds
347
371
 
348
- 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
349
373
  machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
350
374
 
351
375
  - **`testFixCount`** — `gtd: errors` commits (test-fix attempts) since the
@@ -355,6 +379,9 @@ machine** from flags on the `COMMIT[]` stream — never recomputed at the edge:
355
379
  round, and every human resume starts a **fresh budget**.
356
380
  - **`reviewFixCount`** — `gtd: feedback` commits (review-fix rounds) since the
357
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.
358
385
 
359
386
  ### Test-fix loop (`fixAttemptCap`, default 3)
360
387
 
@@ -378,6 +405,31 @@ ERRORS.md — which **resets the fix-attempt budget** (the next run re-tests and
378
405
  grants a fresh `cap` attempts before escalating again). While ERRORS.md exists,
379
406
  every run resolves straight back to Escalate.
380
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
+
381
433
  ### Review-fix loop & agentic review (`reviewThreshold`, default 3)
382
434
 
383
435
  After a green test run, **Agentic Review** reviews the package's accumulated
@@ -484,11 +536,13 @@ built-in defaults apply. Supported filenames (searched in this order):
484
536
  ### Schema
485
537
 
486
538
  - **`testCommand`** (string, default `npm run test`) — the command the edge runs
487
- 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.
488
541
  - **`fixAttemptCap`** (non-negative integer, default `3`) — the test-fix budget:
489
542
  how many `gtd: errors` attempts are allowed per sub-loop before the failure is
490
543
  escalated to ERRORS.md (Escalate). `0` disables the cap (escalates immediately
491
- on the first red run).
544
+ on the first red run). Also reused as the health-fix budget (no separate
545
+ config key).
492
546
  - **`reviewThreshold`** (integer ≥ 1, default `3`) — the review-fix budget: how
493
547
  many `gtd: feedback` rounds are allowed per package before Agentic Review
494
548
  force-approves.
@@ -368497,7 +368497,8 @@ var EDGE_ONLY_STATES = /* @__PURE__ */ new Set([
368497
368497
  "accept-review",
368498
368498
  "close-package",
368499
368499
  "done",
368500
- "await-review"
368500
+ "await-review",
368501
+ "health-check"
368501
368502
  ]);
368502
368503
  var MODEL_STATE = {
368503
368504
  grilling: "grilling",
@@ -368505,6 +368506,7 @@ var MODEL_STATE = {
368505
368506
  planning: "decompose",
368506
368507
  building: "building",
368507
368508
  fixing: "fixing",
368509
+ "health-fixing": "fixing",
368508
368510
  "agentic-review": "agentic-review",
368509
368511
  clean: "clean",
368510
368512
  squashing: "clean"
@@ -368545,7 +368547,8 @@ var STATE_TEMPLATE = {
368545
368547
  clean: "@clean",
368546
368548
  squashing: "@squashing",
368547
368549
  escalate: "@escalate",
368548
- idle: "@idle"
368550
+ idle: "@idle",
368551
+ "health-fixing": "@fixing"
368549
368552
  };
368550
368553
  var buildPrompt = (result, resolveModel = builtinResolveModel, output = "plain") => {
368551
368554
  const { state, context: context10 } = result;
@@ -388261,8 +388264,11 @@ var GTD_DIR = ".gtd";
388261
388264
  var REVIEW_FILE = "REVIEW.md";
388262
388265
  var FEEDBACK_FILE = "FEEDBACK.md";
388263
388266
  var ERRORS_FILE = "ERRORS.md";
388267
+ var HEALTH_FILE = "HEALTH.md";
388264
388268
  var EMPTY_FAILURE_SENTINEL = "Test command failed with no output (exit code non-zero).";
388265
388269
  var UNANSWERED_MARKER = "<!-- user answers here -->";
388270
+ var HEALTH_CHECK_SUBJECT = "gtd: health-check";
388271
+ var HEALTH_FIX_SUBJECT = "gtd: health-fix";
388266
388272
  var NEW_TASK_SUBJECT = "gtd: new task";
388267
388273
  var GRILLING_SUBJECT = "gtd: grilling";
388268
388274
  var PLANNING_SUBJECT = "gtd: planning";
@@ -388279,6 +388285,7 @@ var STEERING_FILES = [
388279
388285
  REVIEW_FILE,
388280
388286
  FEEDBACK_FILE,
388281
388287
  ERRORS_FILE,
388288
+ HEALTH_FILE,
388282
388289
  SQUASH_MSG_FILE
388283
388290
  ];
388284
388291
  var WORKFLOW_FILE_EXCLUDES = [...STEERING_FILES, GTD_DIR];
@@ -388468,7 +388475,9 @@ var gatherEvents = () => (
388468
388475
  isFeedback: subject === FEEDBACK_SUBJECT,
388469
388476
  isPackageStart: subject === PLANNING_SUBJECT || subject === PACKAGE_DONE_SUBJECT,
388470
388477
  isWorkflowCommit: subject.startsWith("gtd: "),
388471
- removedErrors
388478
+ removedErrors,
388479
+ isHealthCheck: subject === HEALTH_CHECK_SUBJECT,
388480
+ isHealthFix: subject === HEALTH_FIX_SUBJECT
388472
388481
  };
388473
388482
  }
388474
388483
  );
@@ -388542,10 +388551,6 @@ var gatherEvents = () => (
388542
388551
  candidate = firstGrilling.hash ?? EMPTY_TREE;
388543
388552
  }
388544
388553
  } else {
388545
- const mergeBaseCandidate = Option_exports.isSome(base) && base.value !== headHash ? base.value : void 0;
388546
- if (mergeBaseCandidate !== void 0) {
388547
- candidate = mergeBaseCandidate;
388548
- }
388549
388554
  }
388550
388555
  if (candidate !== void 0 && hasCommitsAfterLastDone) {
388551
388556
  const candidateDiff = yield* git.diffRef(candidate, WORKFLOW_FILE_EXCLUDES).pipe(Effect_exports.catchAll(() => Effect_exports.succeed("")));
@@ -388601,6 +388606,31 @@ var gatherEvents = () => (
388601
388606
  }
388602
388607
  const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388603
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
+ }
388604
388634
  const payload = {
388605
388635
  todoExists,
388606
388636
  todoCommitted,
@@ -388632,7 +388662,11 @@ var gatherEvents = () => (
388632
388662
  ...squashBase !== void 0 ? { squashBase } : {},
388633
388663
  ...squashDiff !== void 0 ? { squashDiff } : {},
388634
388664
  squashMsgPresent,
388635
- squashMsgContent
388665
+ squashMsgContent,
388666
+ healthPresent,
388667
+ healthContent,
388668
+ healthCommitted,
388669
+ ...healthFixBase !== void 0 ? { healthFixBase } : {}
388636
388670
  };
388637
388671
  const resolveEvent = { type: "RESOLVE", payload };
388638
388672
  return [...commitEvents, resolveEvent];
@@ -388662,7 +388696,7 @@ var perform = (action) => (
388662
388696
  // work in the tree, then re-derive. (No producer command — consume only.)
388663
388697
  case "transportReset": {
388664
388698
  yield* git.mixedResetHead();
388665
- return;
388699
+ return { stop: false };
388666
388700
  }
388667
388701
  // New Feature: capture the raw input as `gtd: new task` (unless HEAD is
388668
388702
  // already there — the lost-seed regenerate case), revert it back to a
@@ -388670,7 +388704,7 @@ var perform = (action) => (
388670
388704
  case "seedNewFeature": {
388671
388705
  const captured = yield* captureAndRevert(git, NEW_TASK_SUBJECT, Effect_exports.void);
388672
388706
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388673
- return;
388707
+ return { stop: false };
388674
388708
  }
388675
388709
  // Accept Review: capture the human's pending changeset durably as
388676
388710
  // `gtd: review feedback` (annotations, code edits, and new files alike),
@@ -388682,7 +388716,7 @@ var perform = (action) => (
388682
388716
  const captured = yield* captureAndRevert(git, REVIEW_FEEDBACK_SUBJECT, git.resetHard());
388683
388717
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388684
388718
  yield* fs9.writeFileString(resolve5(TODO_FILE), seedTodo(captured));
388685
- return;
388719
+ return { stop: false };
388686
388720
  }
388687
388721
  // Grilling capture: the plan is already committed and the user sketched
388688
388722
  // code during the round. Fold the code diff (untracked included, steering
@@ -388705,7 +388739,7 @@ var perform = (action) => (
388705
388739
  }
388706
388740
  yield* fs9.writeFileString(resolve5(TODO_FILE), appendCapturedInput(todoNow, captured));
388707
388741
  yield* git.commitAllWithPrefix(GRILLING_SUBJECT);
388708
- return;
388742
+ return { stop: false };
388709
388743
  }
388710
388744
  // Testing: commit the pending tree `gtd: building` (nothing pending in the
388711
388745
  // no-op-fixer case), run tests; on red write FEEDBACK (below cap) or ERRORS
@@ -388722,13 +388756,46 @@ var perform = (action) => (
388722
388756
  if (!committedBuilding) {
388723
388757
  yield* git.commitAllWithPrefix(BUILDING_SUBJECT);
388724
388758
  }
388725
- return;
388759
+ return { stop: false };
388726
388760
  }
388727
388761
  const target = action.capReached ? ERRORS_FILE : FEEDBACK_FILE;
388728
388762
  const body = /\S/.test(result.output) ? result.output : EMPTY_FAILURE_SENTINEL;
388729
388763
  yield* fs9.writeFileString(resolve5(target), body);
388730
388764
  yield* git.commitAllWithPrefix(ERRORS_SUBJECT);
388731
- 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 };
388732
388799
  }
388733
388800
  // Grilling / Grilled / Planning / Fixing: commit the pending tree under a
388734
388801
  // fixed phase prefix. Fixing sets `removeFeedback` so FEEDBACK.md's removal
@@ -388742,11 +388809,14 @@ var perform = (action) => (
388742
388809
  if (action.removeTodo === true) {
388743
388810
  yield* fs9.remove(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388744
388811
  }
388812
+ if (action.removeHealth === true) {
388813
+ yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388814
+ }
388745
388815
  if (action.removeTodo !== true) {
388746
388816
  yield* formatFile(resolve5(TODO_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388747
388817
  }
388748
388818
  yield* git.commitAllWithPrefix(action.prefix);
388749
- return;
388819
+ return { stop: false };
388750
388820
  }
388751
388821
  // Close package: remove the (maybe-empty / maybe-absent) FEEDBACK.md, rm
388752
388822
  // the first (finished) package dir (+ the now-empty `.gtd/`), commit
@@ -388759,19 +388829,19 @@ var perform = (action) => (
388759
388829
  yield* git.removePackageDir(`${GTD_DIR}/${first2.name}`);
388760
388830
  }
388761
388831
  yield* git.commitAllWithPrefix(PACKAGE_DONE_SUBJECT);
388762
- return;
388832
+ return { stop: false };
388763
388833
  }
388764
388834
  // Await Review: commit REVIEW.md as `gtd: awaiting review`.
388765
388835
  case "commitReview": {
388766
388836
  yield* formatFile(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388767
388837
  yield* git.commitAllWithPrefix(AWAITING_REVIEW_SUBJECT);
388768
- return;
388838
+ return { stop: false };
388769
388839
  }
388770
388840
  // Done: rm REVIEW.md, commit `gtd: done`.
388771
388841
  case "done": {
388772
388842
  yield* fs9.remove(resolve5(REVIEW_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388773
388843
  yield* git.commitAllWithPrefix(DONE_SUBJECT);
388774
- return;
388844
+ return { stop: false };
388775
388845
  }
388776
388846
  // Squash: rm SQUASH_MSG.md, soft-reset to squashBase, commit everything
388777
388847
  // under the provided commit message.
@@ -388779,7 +388849,7 @@ var perform = (action) => (
388779
388849
  yield* fs9.remove(resolve5(SQUASH_MSG_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388780
388850
  yield* git.softResetTo(action.squashBase);
388781
388851
  yield* git.commitAllWithPrefix(action.commitMessage);
388782
- return;
388852
+ return { stop: false };
388783
388853
  }
388784
388854
  }
388785
388855
  }).pipe(Effect_exports.mapError((e7) => e7 instanceof Error ? e7 : new Error(String(e7))))
@@ -388797,14 +388867,17 @@ var GtdStateError = class extends Error {
388797
388867
  var foldCounters = (events) => {
388798
388868
  let testFixCount = 0;
388799
388869
  let reviewFixCount = 0;
388870
+ let healthFixCount = 0;
388800
388871
  for (const event of events) {
388801
388872
  if (event.type !== "COMMIT") continue;
388802
388873
  if (event.isPackageStart || event.isFeedback || event.removedErrors) testFixCount = 0;
388803
388874
  if (event.isErrors) testFixCount += 1;
388804
388875
  if (event.isPackageStart) reviewFixCount = 0;
388805
388876
  if (event.isFeedback) reviewFixCount += 1;
388877
+ if (event.isPackageStart || event.removedErrors) healthFixCount = 0;
388878
+ if (event.isHealthCheck) healthFixCount += 1;
388806
388879
  }
388807
- return { testFixCount, reviewFixCount };
388880
+ return { testFixCount, reviewFixCount, healthFixCount };
388808
388881
  };
388809
388882
  var isBoundary = (subject) => !subject.startsWith("gtd: ") || subject === "gtd: done";
388810
388883
  var DEFAULT_PAYLOAD = {
@@ -388834,7 +388907,10 @@ var DEFAULT_PAYLOAD = {
388834
388907
  reviewThreshold: 3,
388835
388908
  squashEnabled: false,
388836
388909
  squashMsgPresent: false,
388837
- squashMsgContent: ""
388910
+ squashMsgContent: "",
388911
+ healthPresent: false,
388912
+ healthContent: "",
388913
+ healthCommitted: false
388838
388914
  };
388839
388915
  var buildContext = (p4, counters, grillingCase) => ({
388840
388916
  testFixCount: counters.testFixCount,
@@ -388847,7 +388923,7 @@ var buildContext = (p4, counters, grillingCase) => ({
388847
388923
  ...p4.squashDiff !== void 0 ? { squashDiff: p4.squashDiff } : {},
388848
388924
  lastCommitSubject: p4.lastCommitSubject,
388849
388925
  workingTreeClean: p4.workingTreeClean,
388850
- feedbackContent: p4.feedbackContent,
388926
+ feedbackContent: p4.feedbackContent !== "" ? p4.feedbackContent : p4.healthContent,
388851
388927
  ...grillingCase !== void 0 ? { grillingCase } : {}
388852
388928
  });
388853
388929
  var assertLegal = (p4) => {
@@ -388861,7 +388937,32 @@ var assertLegal = (p4) => {
388861
388937
  if (p4.feedbackPresent && p4.reviewPresent) fail20("illegal combination: FEEDBACK.md + REVIEW.md");
388862
388938
  if (p4.feedbackPresent && !p4.gtdDirExists) fail20("illegal combination: FEEDBACK.md without .gtd");
388863
388939
  if (p4.errorsPresent && p4.feedbackPresent) fail20("illegal combination: ERRORS.md + FEEDBACK.md");
388864
- 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
+ };
388865
388966
  };
388866
388967
  var resolveFeedback = (p4, counters) => {
388867
388968
  if (p4.feedbackEmpty) {
@@ -388995,7 +389096,13 @@ var resolveGrilling = (p4, counters, head7) => {
388995
389096
  };
388996
389097
  };
388997
389098
  var resolveCleanOrIdle = (p4, counters, head7) => {
388998
- 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;
388999
389106
  if (head7 === "gtd: done" && p4.squashEnabled && p4.squashBase !== void 0) {
389000
389107
  if (p4.squashMsgPresent) {
389001
389108
  return {
@@ -389012,9 +389119,20 @@ var resolveCleanOrIdle = (p4, counters, head7) => {
389012
389119
  return { state: "squashing", autoAdvance: true, context: buildContext(p4, counters) };
389013
389120
  }
389014
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;
389015
389126
  return {
389016
- state: reviewable ? "clean" : "idle",
389017
- 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
+ },
389018
389136
  context: buildContext(p4, counters)
389019
389137
  };
389020
389138
  };
@@ -389041,6 +389159,7 @@ var resolve4 = (events) => {
389041
389159
  }
389042
389160
  if (p4.errorsPresent)
389043
389161
  return { state: "escalate", autoAdvance: false, context: buildContext(p4, counters) };
389162
+ if (p4.healthPresent) return resolveHealth(p4, counters);
389044
389163
  if (p4.feedbackPresent) return resolveFeedback(p4, counters);
389045
389164
  if (p4.gtdDirExists) return resolveGtdLifecycle(p4, counters, head7, corrupt);
389046
389165
  if (p4.reviewPresent) return resolveReviewLifecycle(p4, counters, head7);
@@ -389056,6 +389175,18 @@ var resolve4 = (events) => {
389056
389175
  context: buildContext(p4, counters)
389057
389176
  };
389058
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
+ }
389059
389190
  if (isBoundary(head7) && !p4.workingTreeClean && !p4.todoCommitted || head7 === "gtd: new task" && p4.workingTreeClean) {
389060
389191
  return {
389061
389192
  state: "new-feature",
@@ -389076,7 +389207,8 @@ var EDGE_ONLY_STATES2 = /* @__PURE__ */ new Set([
389076
389207
  "await-review",
389077
389208
  "accept-review",
389078
389209
  "close-package",
389079
- "done"
389210
+ "done",
389211
+ "health-check"
389080
389212
  ]);
389081
389213
  var isEdgeOnly = (state) => EDGE_ONLY_STATES2.has(state);
389082
389214
  var detect = () => Effect_exports.gen(function* () {
@@ -389088,6 +389220,19 @@ var detect = () => Effect_exports.gen(function* () {
389088
389220
  });
389089
389221
 
389090
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
+ };
389091
389236
  var MAX_EDGE_HOPS = 100;
389092
389237
  function makeProgram(opts = {}) {
389093
389238
  const argv = opts.argv ?? process.argv;
@@ -389138,7 +389283,22 @@ function makeProgram(opts = {}) {
389138
389283
  }
389139
389284
  const result = yield* detect();
389140
389285
  if (result.edgeAction !== void 0) {
389141
- 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
+ }
389142
389302
  }
389143
389303
  if (isEdgeOnly(result.state)) {
389144
389304
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "1.6.1",
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": {