@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.
- package/README.md +151 -64
- package/dist/gtd.bundle.mjs +190 -42
- 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
|
|
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
|
|
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 /
|
|
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`
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
work)
|
|
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|
|
|
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
|
|
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"|
|
|
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|
|
|
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
|
|
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
|
|
304
|
-
| ------------------ | -------------------------------- |
|
|
305
|
-
| **Transport** | edge-only, auto | HEAD `gtd: transport` (hand-made handoff commit)
|
|
306
|
-
| **Escalate** | STOP | ERRORS.md present
|
|
307
|
-
| **Fixing** | agent, auto | non-empty FEEDBACK.md present
|
|
308
|
-
| **
|
|
309
|
-
| **
|
|
310
|
-
| **
|
|
311
|
-
| **
|
|
312
|
-
| **
|
|
313
|
-
| **
|
|
314
|
-
| **
|
|
315
|
-
| **
|
|
316
|
-
| **
|
|
317
|
-
| **
|
|
318
|
-
| **
|
|
319
|
-
| **
|
|
320
|
-
| **Grilled** | agent, auto | TODO.md present, no markers, clean tree
|
|
321
|
-
| **Clean** | agent | no steering files, clean tree, boundary or `gtd: package done` HEAD, and the review base yields a **non-empty** diff
|
|
322
|
-
| **
|
|
325
|
+
| State | Kind | Wins when | Action & commit | Advances to |
|
|
326
|
+
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
327
|
+
| **Transport** | edge-only, auto | HEAD `gtd: transport` (hand-made handoff commit) | mixed-reset HEAD (`git reset HEAD~1`), keep work in tree; **no commit** | re-derive from the restored tree |
|
|
328
|
+
| **Escalate** | STOP | ERRORS.md present | none | held until the human deletes ERRORS.md |
|
|
329
|
+
| **Fixing** | agent, auto | non-empty FEEDBACK.md present | inline FEEDBACK into the prompt, remove FEEDBACK.md; commit its removal `gtd: fixing` (FEEDBACK was committed by Testing) or `gtd: feedback` (uncommitted, written by Agentic Review) | fixer edits → Testing |
|
|
330
|
+
| **Health Fixing** | agent, auto | HEALTH.md present (no `.gtd`, REVIEW.md, or FEEDBACK.md) | read HEALTH.md into the prompt, commit its removal `gtd: health-fix` | fixer edits → Health check |
|
|
331
|
+
| **Close package** | edge-only, auto | empty FEEDBACK.md present (clean review); also reached from Agentic Review force-approve | rm FEEDBACK.md, rm the first (finished) package dir (+ the now-empty `.gtd/`); commit `gtd: package done` | more packages → Building; `.gtd` gone → Clean |
|
|
332
|
+
| **Planning** | agent, auto | `.gtd` present **and modified**; HEAD `gtd: grilled` or `gtd: planning` | commit the `.gtd/` changes `gtd: planning` | continue decomposing, else → Building |
|
|
333
|
+
| **Testing** | edge-only, auto | `.gtd` present, no FEEDBACK/ERRORS, and a reason to test: code changes, a pending ERRORS.md deletion (human resume), or a clean tree under HEAD `gtd: fixing` (no-op fixer) | commit pending tree `gtd: building`, run `testCommand`; green → proceed; red → write FEEDBACK (below cap) or ERRORS (at cap), commit `gtd: errors`; if captured output is empty/whitespace, a sentinel string is written so the file is never empty (empty FEEDBACK remains reserved for agentic-review approval) | green → Agentic Review; FEEDBACK → Fixing; ERRORS → Escalate |
|
|
334
|
+
| **Building** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: planning` or `gtd: package done` | if HEAD `gtd: planning` and TODO.md present, delete TODO.md and commit (prefix unchanged, fires once); select the first package, inline its tasks; agent leaves work **uncommitted** | Testing |
|
|
335
|
+
| **Agentic Review** | agent, auto | `.gtd` present and clean, clean tree; HEAD `gtd: building` | reviewer writes FEEDBACK.md (empty = approval), uncommitted — **unless** force-approved (kill-switch off or review-fix threshold hit), which routes straight to Close package | empty FEEDBACK → Close package; non-empty → Fixing |
|
|
336
|
+
| **Done** | edge-only, auto | REVIEW.md committed + clean tree, **or** committed + checkbox-only edits (only `- [ ]`→`- [x]` flips in REVIEW.md = approval) | rm REVIEW.md, commit `gtd: done` | Squashing (if enabled) or Idle |
|
|
337
|
+
| **Squashing** | agent, auto | no steering files, HEAD `gtd: done` or green Health check with ≥1 `gtd: health-fix`, `squash` enabled, squash base present, no unrelated code dirty (a lone untracked `SQUASH_MSG.md` is allowed) | agent authors a conventional-commits message from the full `<base>..HEAD` diff, then runs `git reset --soft <base>` + `git commit` — collapses all intermediate `gtd: *` commits (including any interleaved non-gtd commits) into one; **gtd then STOPs** — post-squash review fires only on the next manual `gtd` run | Idle (STOP) |
|
|
338
|
+
| **Accept Review** | edge-only, auto | REVIEW.md committed + pending **non-checkbox** edits (human annotated REVIEW.md with comments / edited code) | seed TODO.md from the changeset, `git checkout` to discard the code edits, rm REVIEW.md; **all uncommitted** | Grilling |
|
|
339
|
+
| **Await Review** | edge-only, auto | REVIEW.md present and **uncommitted** (freshly written by Clean) | commit REVIEW.md `gtd: awaiting review` | Done (auto, same run) |
|
|
340
|
+
| **New Feature** | edge-only, auto | boundary HEAD + pending changes (code and/or a new uncommitted TODO.md), **or** HEAD `gtd: new task` + clean tree (lost-seed regen) | commit the raw input verbatim `gtd: new task` (unless already there), `git revert --no-commit` it back to a clean baseline, seed TODO.md from that diff — revert + seed left **uncommitted** | Grilling |
|
|
341
|
+
| **Grilling** | agent (iterate) / STOP (answers) | TODO.md present, not New Feature | commit pending edits `gtd: grilling`. Open-question markers present → STOP for the human to answer inline; no markers but dirty → grilling agent iterates | converge (no markers, clean tree) → Grilled |
|
|
342
|
+
| **Grilled** | agent, auto | TODO.md present, no markers, clean tree | commit pending `gtd: grilled` | decompose into `.gtd/` → Planning |
|
|
343
|
+
| **Clean** | agent | no steering files, clean tree, boundary or `gtd: package done` HEAD, and the review base yields a **non-empty** diff | compute the review base (three rules — see below); agent writes REVIEW.md **uncommitted** with `# Review: <short-hash>` heading, `<!-- base: <full-hash> -->` marker, and per-hunk `- [ ]` checkboxes (ticking them signals approval → Done) | Await Review |
|
|
344
|
+
| **Health check** | edge-only, auto | no steering files, clean tree, outside a process with no reviewable diff (any branch) — the `!reviewable` case from rule 8 | run `testCommand`: green + no prior `gtd: health-fix` → Idle (no commit); red below `fixAttemptCap` → write HEALTH.md, commit `gtd: health-check` → Health Fixing; red at cap → write ERRORS.md, commit `gtd: health-check` → Escalate; green + ≥1 `gtd: health-fix` → Squashing (if `squash`) or Idle | green → Idle or Squashing; red below cap → Health Fixing; red at cap → Escalate |
|
|
345
|
+
| **Idle** | STOP | no steering files, clean tree, health check passed with no prior `gtd: health-fix` commits, and nothing to review | none (no commit — the health check edge terminates the driver loop directly) | — |
|
|
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 —
|
|
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
|
-
|
|
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
|
|
341
|
-
`gtd: done
|
|
342
|
-
|
|
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` unset → Idle (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
|
-
|
|
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
|
|
429
|
-
(`gtd: grilled`)
|
|
430
|
-
|
|
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
|
|
451
|
-
cycle's start marker **nearest to HEAD** (the last `gtd: new task`;
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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:`,
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
389004
|
-
autoAdvance:
|
|
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
|
-
|
|
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:
|
|
389029
|
-
autoAdvance:
|
|
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;
|