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