@pmelab/gtd 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,8 +7,9 @@
7
7
  A git-aware CLI that drives a turn-taking loop between a human and an autonomous
8
8
  coding agent: capture an idea, grill it into a plan, decompose it into work
9
9
  packages, execute with parallel subagents, test, agentically review each
10
- package, and finally walk a human through a review squashing the whole cycle
11
- into one conventional-commits commit at the end.
10
+ package, walk a human through a review, distill durable lessons from the cycle
11
+ into the project's own docs, and finally squash the whole cycle into one
12
+ conventional-commits commit at the end.
12
13
 
13
14
  Internally, gtd is a **pure fold** over git history. The decision core
14
15
  (`src/Machine.ts`) is a single IO-free function, `resolve(events)` — **no
@@ -18,7 +19,7 @@ merge-base with the default branch (whole-history fallback when there is no
18
19
  default branch, when HEAD equals the merge-base, or when there is no merge-base)
19
20
  plus the working tree, turns them into a `COMMIT[]` + single terminal `RESOLVE`
20
21
  event stream, and folds them through the machine. The fold lands on exactly
21
- **one** of 16 states, plus which actor (human or agent) is awaited there. A
22
+ **one** of 20 states, plus which actor (human or agent) is awaited there. A
22
23
  single call resolves to a single state.
23
24
 
24
25
  Steering is entirely **machine-authored commit subjects** — there are no marker
@@ -29,12 +30,12 @@ the machine performs itself between turns) looks like `gtd: tests green`.
29
30
 
30
31
  All workflow state lives under **`.gtd/`**: the plan (`.gtd/TODO.md`), work
31
32
  packages (`.gtd/01-…/`), review records (`.gtd/REVIEW.md`, `.gtd/FEEDBACK.md`),
32
- and loop bookkeeping (`.gtd/ERRORS.md`, `.gtd/HEALTH.md`, `.gtd/SQUASH_MSG.md`).
33
- One rule follows for every agent in the loop: **never touch `.gtd/`** except the
34
- single file a prompt explicitly grants. A `TODO.md` or `REVIEW.md` at the
35
- repository root is the project's own file — gtd never reads, consumes, or
36
- deletes it. (Corollary: don't gitignore `.gtd/` — the workflow commits its state
37
- through it.)
33
+ and loop bookkeeping (`.gtd/ERRORS.md`, `.gtd/HEALTH.md`, `.gtd/LEARNINGS.md`,
34
+ `.gtd/SQUASH_MSG.md`). One rule follows for every agent in the loop: **never
35
+ touch `.gtd/`** except the single file a prompt explicitly grants. A `TODO.md`
36
+ or `REVIEW.md` at the repository root is the project's own file — gtd never
37
+ reads, consumes, or deletes it. (Corollary: don't gitignore `.gtd/` — the
38
+ workflow commits its state through it.)
38
39
 
39
40
  ## Quick start: the two-beat loop
40
41
 
@@ -116,6 +117,12 @@ to cwd, so it refuses with a clear error if invoked from a subdirectory.
116
117
  `--jsn`) is rejected with a usage error rather than silently ignored, so a
117
118
  mistyped flag can never degrade a JSON caller to plain-text mode.
118
119
 
120
+ One nuance to "(no mutation)": `next` and `status` never author commits or
121
+ change workflow state, but while a human review is pending they do maintain the
122
+ review checkout window (closing it to read state, re-arming it on the way out —
123
+ see [Human review gate](#human-review-gate)), which transiently moves HEAD and
124
+ the index. The working tree is never touched.
125
+
119
126
  ### `gtd step` / `gtd step-agent`
120
127
 
121
128
  Both drive the **same fixpoint loop** — gather → resolve → perform the returned
@@ -422,12 +429,14 @@ GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd-loop
422
429
 
423
430
  ## States & subjects overview
424
431
 
425
- `resolve()` lands on exactly one of **16 states**: `grilling`, `grilled`,
432
+ `resolve()` lands on exactly one of **20 states**: `grilling`, `grilled`,
426
433
  `planning`, `building`, `testing`, `fixing`, `escalate`, `agentic-review`,
427
- `close-package`, `review`, `await-review`, `done`, `squashing`, `idle`,
428
- `health-check`, `health-fixing`. Each state has a fixed awaited actor (see
429
- `awaitedActor` in `src/Machine.ts`): `idle`, `escalate`, and `await-review`
430
- await the **human**; every other state awaits the **agent**.
434
+ `close-package`, `review`, `await-review`, `done`, `learning`,
435
+ `await-learning-review`, `learning-apply`, `learning-applied`, `squashing`,
436
+ `idle`, `health-check`, `health-fixing`. Each state has a fixed awaited actor
437
+ (see `awaitedActor` in `src/Machine.ts`): `idle`, `escalate`, `await-review`,
438
+ and `await-learning-review` await the **human**; every other state awaits the
439
+ **agent**.
431
440
 
432
441
  For the full precedence ladder, illegal combinations, and the counter folds that
433
442
  drive the fix loops, see [STATES.md](STATES.md) — this section is a summary.
@@ -446,6 +455,8 @@ The closed set of gates:
446
455
  | `agentic-review` | agent (writes .gtd/FEEDBACK.md verdict) |
447
456
  | `review` | agent (writes .gtd/REVIEW.md) / human (approves or gives feedback) |
448
457
  | `squashing` | agent (overwrites .gtd/SQUASH_MSG.md) |
458
+ | `learning` | agent (overwrites .gtd/LEARNINGS.md) / human (accepts or edits) |
459
+ | `learning-apply` | agent (integrates .gtd/LEARNINGS.md into CLAUDE.md/AGENTS.md/docs) |
449
460
  | `health-fixing` | agent (idle health-check repair) |
450
461
  | `escalate` | human (deletes .gtd/ERRORS.md to resume) |
451
462
 
@@ -456,7 +467,8 @@ agent "wins": `gtd: grilled`, `gtd: planning`, `gtd: tests green`,
456
467
  `gtd: errors`, `gtd: package done`, `gtd: awaiting review`,
457
468
  `gtd: review feedback`, `gtd: done`, `gtd: squash template`,
458
469
  `gtd: reviewing <hash>` (parameterized, from `gtd review`), `gtd: health-check`,
459
- `gtd: health-fix`.
470
+ `gtd: health-fix`, `gtd: learning template`, `gtd: learning drafted`,
471
+ `gtd: learning approved`, `gtd: learning applied`.
460
472
 
461
473
  Everything else — any non-`gtd` subject, and any `gtd: *` subject outside this
462
474
  closed set — is a **boundary commit**: inert as far as the machine's grammar is
@@ -520,23 +532,25 @@ the threshold simply closes the package as usual.) Setting
520
532
 
521
533
  A **do-nothing agent invocation** — `gtd step-agent` on a clean tree at ANY
522
534
  agent-awaited rest whose move is a file artifact (`grilling`, `grilled`,
523
- `building`, `fixing`, `agentic-review`, `review`, and `squashing` while
524
- `.gtd/SQUASH_MSG.md` still holds the unmodified template) is inert: zero
525
- commits, no state consumed; `gtd next` re-emits the same prompt. This is
526
- load-bearing for the loop protocol, whose every iteration opens with
527
- `gtd step-agent` before the agent has acted: without the guard that opening beat
528
- would author junk empty turns and worse, consume workflow state (an empty
529
- decompose turn would delete `.gtd/TODO.md` with no packages written; an empty
530
- squashing turn would squash the cycle under the placeholder template). The same
531
- guards hold at the classification layer for histories that already carry such
532
- turns: a `gtd(agent): grilled` HEAD only routes to `gtd: planning` when packages
533
- exist, a `gtd(agent): review` HEAD only routes to `gtd: awaiting review` when
534
- `.gtd/REVIEW.md` exists, and a squashing turn only squashes once the template
535
- has been overwritten. The one deliberate exception is `health-fixing`, whose
536
- empty turn is meaningful (the failure may have been environmental the machine
537
- removes `.gtd/HEALTH.md` and re-tests). Human gates are unaffected: an empty
538
- **human** turn stays a signal (accept-defaults at grilling, clean approval at
539
- review).
535
+ `building`, `fixing`, `agentic-review`, `review`, `squashing` while
536
+ `.gtd/SQUASH_MSG.md` still holds the unmodified template, `learning` while
537
+ `.gtd/LEARNINGS.md` still holds the unmodified template, and `learning-apply`
538
+ unconditionally) is inert: zero commits, no state consumed; `gtd next`
539
+ re-emits the same prompt. This is load-bearing for the loop protocol, whose
540
+ every iteration opens with `gtd step-agent` before the agent has acted: without
541
+ the guard that opening beat would author junk empty turns and worse, consume
542
+ workflow state (an empty decompose turn would delete `.gtd/TODO.md` with no
543
+ packages written; an empty squashing turn would squash the cycle under the
544
+ placeholder template). The same guards hold at the classification layer for
545
+ histories that already carry such turns: a `gtd(agent): grilled` HEAD only
546
+ routes to `gtd: planning` when packages exist, a `gtd(agent): review` HEAD only
547
+ routes to `gtd: awaiting review` when `.gtd/REVIEW.md` exists, and a squashing
548
+ (or learning) turn only proceeds once its template has been overwritten. The one
549
+ deliberate exception is `health-fixing`, whose empty turn is meaningful (the
550
+ failure may have been environmental the machine removes `.gtd/HEALTH.md` and
551
+ re-tests). Human gates are unaffected: an empty **human** turn stays a signal
552
+ (accept-defaults at grilling, clean approval at review, accept-the-draft-as-is
553
+ at the learning review gate).
540
554
 
541
555
  ### Human review gate
542
556
 
@@ -547,28 +561,79 @@ Once `.gtd/` is fully closed, the machine writes `.gtd/REVIEW.md` and rests at
547
561
  plus routing `gtd: done`.
548
562
  - Flipping only `- [ ]` → `- [x]` checkboxes in `.gtd/REVIEW.md` — checkbox-only
549
563
  edits are also treated as clean approval.
564
+ - Deleting `.gtd/REVIEW.md` outright.
550
565
 
551
566
  Any **substantive** edit — to `.gtd/REVIEW.md` prose, or to the reviewed code
552
567
  itself — is feedback: `gtd(human): review` plus routing `gtd: review feedback`,
553
568
  `.gtd/REVIEW.md` removed, and `gtd next` re-emits a grilling prompt to the agent
554
569
  that inlines the human's finding.
555
570
 
571
+ **The review diff lives in your editor.** While the gate is pending, gtd holds
572
+ open a _review checkout window_: it saves the real head to
573
+ `refs/gtd/review-head`, then rewinds HEAD and the index to the review base with
574
+ `git reset --mixed`, leaving the working tree untouched. Every editor's standard
575
+ git integration now shows the entire reviewable diff as ordinary uncommitted
576
+ changes — SCM panel, gutter marks, per-file diffs. Review it there:
577
+
578
+ - **Edit** anything (code or `.gtd/REVIEW.md` prose) → feedback.
579
+ - **Discard a hunk** in the editor → that reversion IS the feedback: the agent
580
+ is re-grilled with it.
581
+ - **Delete a surfaced file** → reject-this-file feedback.
582
+ - Touch nothing (or tick checkboxes / delete `.gtd/REVIEW.md`) → approval.
583
+
584
+ Any gtd invocation closes the window first (restoring HEAD/index exactly, so
585
+ only your own edits remain dirty — they land as their own separate
586
+ `gtd(human): review` commit, never mixed into the reviewed work), and
587
+ `gtd next`/`gtd status` re-arm it on their way out. The mechanics are
588
+ crash-safe; details and invariants in STATES.md ("The review checkout window").
589
+
590
+ Caveats while a review is pending: don't push (the branch tip rests at the
591
+ review base — the real head is safe under `refs/gtd/review-head`); commits you
592
+ make manually survive as working-tree content and become review feedback, but
593
+ their commit message is discarded; linked `git worktree` checkouts are
594
+ unsupported. If you switch branches mid-review, gtd refuses to touch the foreign
595
+ branch and prints the manual recovery command.
596
+
597
+ ### Learning
598
+
599
+ With `learning: true` (the default), `gtd: done` (or the health-fix path's green
600
+ re-test) is **not** a rest — the chain continues straight to
601
+ `gtd: learning template`, writing and committing a `.gtd/LEARNINGS.md` template,
602
+ running _before_ the squash decision so it still sees the pre-squash history.
603
+ `gtd next` then emits the learning prompt: the agent walks the cycle's test
604
+ failures, review feedback, and health-check rounds, keeps only
605
+ durable/generalizable lessons, and overwrites `.gtd/LEARNINGS.md` with them.
606
+ Once `gtd step-agent` captures that draft (`gtd(agent): learning`), it rests at
607
+ **await-learning-review** for a human — who either accepts the draft as-is (an
608
+ empty turn) or edits it; there is no reject path, so the very next `gtd step`
609
+ always proceeds (`gtd(human): learning` → `gtd: learning approved`), resting at
610
+ **learning-apply** for the agent. The agent integrates the approved learnings
611
+ into the project's own docs (`CLAUDE.md`/`AGENTS.md`/wherever fits, its
612
+ judgment); its turn (`gtd(agent): learning-apply`) removes `.gtd/LEARNINGS.md`
613
+ and lands at `gtd: learning applied`, which then runs the same squash decision
614
+ `gtd: done` runs today. With `learning: false`, `gtd: done` behaves exactly as
615
+ it does without this section: no `.gtd/LEARNINGS.md` is ever written. Learning
616
+ and squash are independent flags — either can be on without the other.
617
+
556
618
  ### Squash
557
619
 
558
- With `squash: true` (the default), `gtd: done` is **not** a rest — the same
559
- chain continues straight to `gtd: squash template`, writing and committing a
560
- `.gtd/SQUASH_MSG.md` template. `gtd next` then emits the squashing prompt: the
561
- agent overwrites `.gtd/SQUASH_MSG.md` with a real conventional-commits message
562
- (drawing on grilling-round decisions from history) and finishes its turn.
563
- `gtd step-agent` then performs the squash itself: `git reset --soft <base>` +
564
- `git commit`, collapsing every intermediate `gtd: *` commit of the cycle into
565
- one including any review-feedback detours: the squash base is the cycle's
566
- ORIGINAL start (the first grilling run since the previous `gtd: done` boundary,
567
- or the `gtd: reviewing <hash>` anchor for an ad-hoc review cycle), not the most
568
- recent re-grilling round the collapse folds the whole cycle into one, using
569
- the overwritten message's content verbatim (turn position, not message content,
570
- triggers the squash). With `squash: false`, `gtd: done` is the resting boundary
571
- and no template is ever written.
620
+ With `squash: true` (the default), `gtd: done` (or, once learning has run,
621
+ `gtd: learning applied`) is **not** a rest the same chain continues straight
622
+ to `gtd: squash template`, writing and committing a `.gtd/SQUASH_MSG.md`
623
+ template. `gtd next` then emits the squashing prompt: the agent overwrites
624
+ `.gtd/SQUASH_MSG.md` with a real conventional-commits message (drawing on
625
+ grilling-round decisions from history) and finishes its turn. `gtd step-agent`
626
+ then performs the squash itself: `git reset --soft <base>` + `git commit`,
627
+ collapsing every intermediate `gtd: *` commit of the cycle into one including
628
+ any review-feedback detours, and the learning phase's own commits if learning
629
+ ran: the squash base is the cycle's ORIGINAL start (the first grilling run since
630
+ the previous `gtd: done` boundary, or the `gtd: reviewing <hash>` anchor for an
631
+ ad-hoc review cycle), not the most recent re-grilling round the collapse folds
632
+ the whole cycle into one, using the overwritten message's content verbatim (turn
633
+ position, not message content, triggers the squash). Doc edits made during
634
+ `learning-apply` survive in the squashed tree, not as their own commit. With
635
+ `squash: false`, `gtd: done` (or `gtd: learning applied`) is the resting
636
+ boundary and no template is ever written.
572
637
 
573
638
  ### Health check
574
639
 
@@ -577,9 +642,9 @@ runs `testCommand` as a health check rather than settling immediately. Green
577
642
  settles idle with zero commits. Red below `fixAttemptCap` writes
578
643
  `.gtd/HEALTH.md` and rests at **Health Fixing** for the agent; the fixer's own
579
644
  turn (`gtd(agent): health-fixing`) removes `.gtd/HEALTH.md` and re-tests in the
580
- same chain — a green re-test continues to squash (if enabled) or idle; red
581
- repeats the health-fix loop; red at the cap writes `.gtd/ERRORS.md` and
582
- escalates.
645
+ same chain — a green re-test continues to learning (if enabled), then squash (if
646
+ enabled), or idle; red repeats the health-fix loop; red at the cap writes
647
+ `.gtd/ERRORS.md` and escalates.
583
648
 
584
649
  ### Escalate / budget reset
585
650
 
@@ -591,24 +656,28 @@ fix-attempt budget to zero.
591
656
 
592
657
  ## States & subjects: overview table
593
658
 
594
- | State | Awaits | Turn/routing subject at rest |
595
- | ---------------- | -------------- | ---------------------------------------------------------------- |
596
- | `grilling` | human or agent | `gtd(human): grilling` / `gtd(agent): grilling` |
597
- | `grilled` | agent | `gtd: grilled` |
598
- | `planning` | agent | `.gtd/` modified |
599
- | `building` | agent | `gtd: planning` / `gtd: package done` |
600
- | `testing` | — (edge-only) | mid-chain only |
601
- | `fixing` | agent | `gtd: errors` |
602
- | `escalate` | human | `.gtd/ERRORS.md` present |
603
- | `agentic-review` | agent | `gtd: tests green` |
604
- | `close-package` | — (edge-only) | mid-chain only |
605
- | `review` | agent | `gtd: package done` (no more packages) / `gtd: reviewing <hash>` |
606
- | `await-review` | human | `gtd: awaiting review` |
607
- | `done` | — (edge-only) | `gtd: done` |
608
- | `squashing` | agent | `gtd: squash template` |
609
- | `idle` | human | no steering files, green health check |
610
- | `health-check` | — (edge-only) | mid-chain only |
611
- | `health-fixing` | agent | `.gtd/HEALTH.md` present |
659
+ | State | Awaits | Turn/routing subject at rest |
660
+ | ----------------------- | -------------- | ---------------------------------------------------------------- |
661
+ | `grilling` | human or agent | `gtd(human): grilling` / `gtd(agent): grilling` |
662
+ | `grilled` | agent | `gtd: grilled` |
663
+ | `planning` | agent | `.gtd/` modified |
664
+ | `building` | agent | `gtd: planning` / `gtd: package done` |
665
+ | `testing` | — (edge-only) | mid-chain only |
666
+ | `fixing` | agent | `gtd: errors` |
667
+ | `escalate` | human | `.gtd/ERRORS.md` present |
668
+ | `agentic-review` | agent | `gtd: tests green` |
669
+ | `close-package` | — (edge-only) | mid-chain only |
670
+ | `review` | agent | `gtd: package done` (no more packages) / `gtd: reviewing <hash>` |
671
+ | `await-review` | human | `gtd: awaiting review` |
672
+ | `done` | — (edge-only) | `gtd: done` |
673
+ | `learning` | agent | `gtd: learning template` |
674
+ | `await-learning-review` | human | `gtd: learning drafted` |
675
+ | `learning-apply` | agent | `gtd: learning approved` |
676
+ | `learning-applied` | — (edge-only) | `gtd: learning applied` |
677
+ | `squashing` | agent | `gtd: squash template` |
678
+ | `idle` | human | no steering files, green health check |
679
+ | `health-check` | — (edge-only) | mid-chain only |
680
+ | `health-fixing` | agent | `.gtd/HEALTH.md` present |
612
681
 
613
682
  See [STATES.md](STATES.md) for the full precedence ladder, the counter folds,
614
683
  and every illegal steering-file combination.
@@ -641,13 +710,19 @@ built-in defaults apply. Supported filenames (searched in this order):
641
710
  - **`agenticReview`** (boolean, default `true`) — kill-switch for the
642
711
  per-package Agentic Review gate. Set `false` to force-approve every package
643
712
  and proceed directly to human review.
644
- - **`squash`** (boolean, default `true`) — after `gtd: done`, collapse the
645
- cycle's `gtd: *` commits into a single conventional-commits commit. Set
646
- `false` to keep the granular history.
713
+ - **`squash`** (boolean, default `true`) — after `gtd: done` (or, once learning
714
+ has run, `gtd: learning applied`), collapse the cycle's `gtd: *` commits into
715
+ a single conventional-commits commit. Set `false` to keep the granular
716
+ history.
717
+ - **`learning`** (boolean, default `true`) — after `gtd: done` (or the
718
+ health-fix path's green re-test), distill durable lessons from the cycle into
719
+ `.gtd/LEARNINGS.md`, have a human review them, then integrate them into the
720
+ project's own docs before the squash decision runs. Set `false` to skip the
721
+ phase entirely — independent of `squash`.
647
722
  - **`models`** — model selection for the subagent-spawning states:
648
723
  - `planning` — high-reasoning tier (default `claude-opus-4-8`), used by
649
724
  `decompose` (the `grilled`/`planning` states), `grilling`, `agentic-review`,
650
- and `clean` (the `review`/`squashing` states).
725
+ and `clean` (the `review`/`squashing`/`learning`/`learning-apply` states).
651
726
  - `execution` — everyday tier (default `claude-sonnet-4-8`), used by
652
727
  `building` and `fixing`.
653
728
  - `states.*` — per-state overrides keyed by `decompose`, `grilling`,
@@ -706,6 +781,7 @@ fixAttemptCap: 3
706
781
  reviewThreshold: 3
707
782
  agenticReview: true
708
783
  squash: true
784
+ learning: true
709
785
  models:
710
786
  planning: claude-opus-4-8
711
787
  execution: claude-sonnet-4-8
@@ -367051,6 +367051,7 @@ var builtinTierDefault = {
367051
367051
  var DEFAULT_TEST_COMMAND = "npm run test";
367052
367052
  var DEFAULT_AGENTIC_REVIEW = true;
367053
367053
  var DEFAULT_SQUASH = true;
367054
+ var DEFAULT_LEARNING = true;
367054
367055
  var DEFAULT_FIX_ATTEMPT_CAP = 3;
367055
367056
  var DEFAULT_REVIEW_THRESHOLD = 3;
367056
367057
  var ModelStatesSchema = Schema_exports.Struct({
@@ -367071,6 +367072,7 @@ var ConfigSchema = Schema_exports.Struct({
367071
367072
  models: Schema_exports.optional(ModelsSchema),
367072
367073
  agenticReview: Schema_exports.optional(Schema_exports.Boolean),
367073
367074
  squash: Schema_exports.optional(Schema_exports.Boolean),
367075
+ learning: Schema_exports.optional(Schema_exports.Boolean),
367074
367076
  fixAttemptCap: Schema_exports.optional(Schema_exports.Int.pipe(Schema_exports.greaterThanOrEqualTo(0))),
367075
367077
  reviewThreshold: Schema_exports.optional(Schema_exports.Int.pipe(Schema_exports.greaterThanOrEqualTo(1)))
367076
367078
  });
@@ -367202,6 +367204,7 @@ var toOperations = (decoded) => {
367202
367204
  resolveModel,
367203
367205
  agenticReview: decoded.agenticReview ?? DEFAULT_AGENTIC_REVIEW,
367204
367206
  squash: decoded.squash ?? DEFAULT_SQUASH,
367207
+ learning: decoded.learning ?? DEFAULT_LEARNING,
367205
367208
  fixAttemptCap: decoded.fixAttemptCap ?? DEFAULT_FIX_ATTEMPT_CAP,
367206
367209
  reviewThreshold: decoded.reviewThreshold ?? DEFAULT_REVIEW_THRESHOLD
367207
367210
  };
@@ -367862,6 +367865,10 @@ var makeGitImpl = (executor, root2) => {
367862
367865
  (hash3) => /^[0-9a-f]{40}$/.test(hash3) ? Effect_exports.succeed(hash3) : Effect_exports.fail(new Error(`Invalid ref: ${ref}`))
367863
367866
  )
367864
367867
  ),
367868
+ readRefOption: (ref) => exec2("git", "rev-parse", "--verify", "--quiet", ref).pipe(
367869
+ Effect_exports.map((s5) => Option_exports.some(s5.trim())),
367870
+ Effect_exports.catchAll(() => Effect_exports.succeed(Option_exports.none()))
367871
+ ),
367865
367872
  topLevel: () => exec2("git", "rev-parse", "--show-toplevel").pipe(Effect_exports.map((s5) => s5.trim())),
367866
367873
  resolveDefaultBranch: () => exec2("git", "rev-parse", "--abbrev-ref", "origin/HEAD").pipe(
367867
367874
  Effect_exports.map((s5) => s5.trim()),
@@ -367995,7 +368002,20 @@ var makeGitImpl = (executor, root2) => {
367995
368002
  )
367996
368003
  );
367997
368004
  }).pipe(Effect_exports.asVoid),
367998
- softResetTo: (ref) => exec2("git", "reset", "--soft", ref).pipe(Effect_exports.asVoid)
368005
+ softResetTo: (ref) => exec2("git", "reset", "--soft", ref).pipe(Effect_exports.asVoid),
368006
+ updateRef: (ref, hash3) => exec2("git", "update-ref", ref, hash3).pipe(Effect_exports.asVoid),
368007
+ deleteRef: (ref) => exec2("git", "update-ref", "-d", ref).pipe(
368008
+ Effect_exports.asVoid,
368009
+ Effect_exports.catchAll(() => Effect_exports.void)
368010
+ ),
368011
+ mixedResetTo: (ref) => exec2("git", "reset", "--mixed", ref).pipe(Effect_exports.asVoid),
368012
+ restoreStagedFrom: (source2, paths) => paths.length === 0 ? Effect_exports.void : exec2("git", "restore", "--staged", `--source=${source2}`, "--", ...paths).pipe(
368013
+ Effect_exports.asVoid,
368014
+ // `git restore` errors when a pathspec matches nothing at either
368015
+ // side — for an optional dir like `.gtd/` that's a no-op, not a failure.
368016
+ Effect_exports.catchAll(() => Effect_exports.void)
368017
+ ),
368018
+ addIntentToAdd: () => exec2("git", "add", "--intent-to-add", ".").pipe(Effect_exports.asVoid)
367999
368019
  };
368000
368020
  };
368001
368021
  var makeLiveEffect = Effect_exports.gen(function* () {
@@ -387748,7 +387768,11 @@ var ROUTING_SUBJECT = {
387748
387768
  done: "gtd: done",
387749
387769
  "squash-template": "gtd: squash template",
387750
387770
  "health-check": "gtd: health-check",
387751
- "health-fix": "gtd: health-fix"
387771
+ "health-fix": "gtd: health-fix",
387772
+ "learning-template": "gtd: learning template",
387773
+ "learning-drafted": "gtd: learning drafted",
387774
+ "learning-approved": "gtd: learning approved",
387775
+ "learning-applied": "gtd: learning applied"
387752
387776
  };
387753
387777
  var reviewingSubject = (baseHash) => `gtd: reviewing ${baseHash}`;
387754
387778
  var TURN_RE = /^gtd\((human|agent)\): (.+)$/;
@@ -387761,7 +387785,9 @@ var TURN_GATES = /* @__PURE__ */ new Set([
387761
387785
  "review",
387762
387786
  "squashing",
387763
387787
  "health-fixing",
387764
- "escalate"
387788
+ "escalate",
387789
+ "learning",
387790
+ "learning-apply"
387765
387791
  ]);
387766
387792
  var ROUTING_SUBJECT_TO_PHASE = new Map(
387767
387793
  Object.entries(ROUTING_SUBJECT).map(([phase, subject]) => [
@@ -387801,6 +387827,7 @@ var FEEDBACK_FILE = `${GTD_DIR}/FEEDBACK.md`;
387801
387827
  var ERRORS_FILE = `${GTD_DIR}/ERRORS.md`;
387802
387828
  var HEALTH_FILE = `${GTD_DIR}/HEALTH.md`;
387803
387829
  var SQUASH_MSG_FILE = `${GTD_DIR}/SQUASH_MSG.md`;
387830
+ var LEARNINGS_FILE = `${GTD_DIR}/LEARNINGS.md`;
387804
387831
  var LEGACY_FEEDBACK_FILE = "FEEDBACK.md";
387805
387832
  var emptyFailureSentinel = (command, exitCode3) => `Test command \`${command}\` failed with exit code ${exitCode3} and produced no output.`;
387806
387833
  var DONE_SUBJECT = "gtd: done";
@@ -387809,6 +387836,18 @@ var GATE_OWN_STEERING_FILE = {
387809
387836
  grilling: TODO_FILE,
387810
387837
  review: REVIEW_FILE
387811
387838
  };
387839
+ var SQUASH_OR_LEARNING_ROUTING_PHASES = /* @__PURE__ */ new Set([
387840
+ "squash-template",
387841
+ "learning-template",
387842
+ "learning-drafted",
387843
+ "learning-approved",
387844
+ "learning-applied"
387845
+ ]);
387846
+ var SQUASH_OR_LEARNING_TURN_GATES = /* @__PURE__ */ new Set([
387847
+ "squashing",
387848
+ "learning",
387849
+ "learning-apply"
387850
+ ]);
387812
387851
  var turnDiffExcludes = (gate) => {
387813
387852
  const ownFile = GATE_OWN_STEERING_FILE[gate];
387814
387853
  return ownFile ? [...WORKFLOW_FILE_EXCLUDES, `!${ownFile}`] : WORKFLOW_FILE_EXCLUDES;
@@ -388079,8 +388118,8 @@ var gatherEvents = (invoker) => (
388079
388118
  let squashBase;
388080
388119
  let squashDiff;
388081
388120
  const headParsedForSquash = parseSubject(lastCommitSubject);
388082
- const inSquashChain = lastCommitSubject === DONE_SUBJECT || headParsedForSquash.kind === "routing" && headParsedForSquash.phase === "squash-template" || headParsedForSquash.kind === "turn" && headParsedForSquash.actor === "agent" && headParsedForSquash.gate === "squashing";
388083
- if (hasCommits && inSquashChain && config2.squash) {
388121
+ const inSquashOrLearningChain = lastCommitSubject === DONE_SUBJECT || headParsedForSquash.kind === "routing" && SQUASH_OR_LEARNING_ROUTING_PHASES.has(headParsedForSquash.phase) || headParsedForSquash.kind === "turn" && SQUASH_OR_LEARNING_TURN_GATES.has(headParsedForSquash.gate);
388122
+ if (hasCommits && inSquashOrLearningChain && (config2.squash || config2.learning)) {
388084
388123
  const squashHistory = history;
388085
388124
  let lastDoneIdxForSquash = -1;
388086
388125
  let prevDoneIdx = -1;
@@ -388126,11 +388165,13 @@ var gatherEvents = (invoker) => (
388126
388165
  }
388127
388166
  const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388128
388167
  const squashMsgIsTemplate = squashMsgPresent && (yield* fs9.readFileString(resolve5(SQUASH_MSG_FILE))).trim() === SQUASH_TEMPLATE.trim();
388168
+ const learningMsgPresent = yield* fs9.exists(resolve5(LEARNINGS_FILE));
388169
+ const learningMsgIsTemplate = learningMsgPresent && (yield* fs9.readFileString(resolve5(LEARNINGS_FILE))).trim() === LEARNING_TEMPLATE.trim();
388129
388170
  const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388130
388171
  const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388131
388172
  const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388132
388173
  let healthFixBase;
388133
- if (config2.squash) {
388174
+ if (config2.squash || config2.learning) {
388134
388175
  let firstHealthCheckHash;
388135
388176
  let healthCheckCount = 0;
388136
388177
  for (const commit3 of commitEvents) {
@@ -388204,7 +388245,10 @@ var gatherEvents = (invoker) => (
388204
388245
  healthPresent,
388205
388246
  healthContent,
388206
388247
  healthCommitted,
388207
- ...healthFixBase !== void 0 ? { healthFixBase } : {}
388248
+ ...healthFixBase !== void 0 ? { healthFixBase } : {},
388249
+ learningEnabled: config2.learning,
388250
+ learningMsgPresent,
388251
+ learningMsgIsTemplate
388208
388252
  };
388209
388253
  const resolveEvent = { type: "RESOLVE", payload };
388210
388254
  return [...commitEvents, resolveEvent];
@@ -388228,6 +388272,15 @@ var SQUASH_TEMPLATE = [
388228
388272
  "Longer description of the change, if needed.",
388229
388273
  ""
388230
388274
  ].join("\n");
388275
+ var LEARNING_TEMPLATE = [
388276
+ "<!-- gtd: replace this file's content with the actual distilled learnings for this cycle. -->",
388277
+ "<!-- Keep only durable, generalizable lessons \u2014 delete anything that's a one-off detail. -->",
388278
+ "",
388279
+ "## Learnings",
388280
+ "",
388281
+ "- ...",
388282
+ ""
388283
+ ].join("\n");
388231
388284
  var perform = (action) => (
388232
388285
  // fallow-ignore-next-line complexity
388233
388286
  Effect_exports.gen(function* () {
@@ -388262,6 +388315,9 @@ var perform = (action) => (
388262
388315
  if (action.removeHealth === true) {
388263
388316
  yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388264
388317
  }
388318
+ if (action.removeLearning === true) {
388319
+ yield* fs9.remove(resolve5(LEARNINGS_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388320
+ }
388265
388321
  yield* git.commitAllWithPrefix(action.subject);
388266
388322
  return { stop: false };
388267
388323
  }
@@ -388309,6 +388365,14 @@ var perform = (action) => (
388309
388365
  yield* git.commitAllWithPrefix("gtd: squash template");
388310
388366
  return { stop: false };
388311
388367
  }
388368
+ // Write the LEARNINGS.md template (a durable-lessons skeleton) and
388369
+ // commit routing `gtd: learning template`.
388370
+ case "writeLearningTemplate": {
388371
+ yield* ensureGtdDir;
388372
+ yield* fs9.writeFileString(resolve5(LEARNINGS_FILE), LEARNING_TEMPLATE);
388373
+ yield* git.commitAllWithPrefix("gtd: learning template");
388374
+ return { stop: false };
388375
+ }
388312
388376
  // Squash: read SQUASH_MSG.md content (the real message authored by the
388313
388377
  // squashing turn), rm it, soft-reset to squashBase, commit-all under the
388314
388378
  // file's content as the message.
@@ -388320,10 +388384,11 @@ var perform = (action) => (
388320
388384
  return { stop: false };
388321
388385
  }
388322
388386
  // Health check: run tests on an idle/clean tree.
388323
- // Green, no squash-after chain queued → stop immediately, no commit/write.
388324
- // Green, `squashAfterGreen` → commit routing `gtd: tests green` (the
388387
+ // Green, no learning/squash-after chain queued → stop immediately, no
388388
+ // commit/write.
388389
+ // Green, `chainAfterGreen` → commit routing `gtd: tests green` (the
388325
388390
  // observable green marker) and continue — the resolver chains
388326
- // `writeSquashTemplate` at that HEAD next.
388391
+ // `writeLearningTemplate` or `writeSquashTemplate` at that HEAD next.
388327
388392
  // Red below cap → write HEALTH.md, commit routing `gtd: health-check` (the
388328
388393
  // always-clean invariant: write-and-commit in the same chain).
388329
388394
  // Red at cap → write ERRORS.md, commit routing `gtd: health-check`.
@@ -388331,7 +388396,7 @@ var perform = (action) => (
388331
388396
  const runner = yield* TestRunner;
388332
388397
  const result = yield* runner.run();
388333
388398
  if (result.exitCode === 0) {
388334
- if (action.squashAfterGreen) {
388399
+ if (action.chainAfterGreen) {
388335
388400
  yield* git.commitAllWithPrefix("gtd: tests green");
388336
388401
  return { stop: false };
388337
388402
  }
@@ -388428,7 +388493,10 @@ var DEFAULT_PAYLOAD = {
388428
388493
  squashMsgIsTemplate: false,
388429
388494
  healthPresent: false,
388430
388495
  healthContent: "",
388431
- healthCommitted: false
388496
+ healthCommitted: false,
388497
+ learningEnabled: false,
388498
+ learningMsgPresent: false,
388499
+ learningMsgIsTemplate: false
388432
388500
  };
388433
388501
  var buildContext = (p4, counters) => ({
388434
388502
  testFixCount: counters.testFixCount,
@@ -388459,6 +388527,32 @@ var healthFileConflictRules = [
388459
388527
  message: ".gtd/HEALTH.md + .gtd/ERRORS.md"
388460
388528
  }
388461
388529
  ];
388530
+ var learningFileConflictRules = [
388531
+ {
388532
+ isViolated: (p4) => p4.learningMsgPresent && p4.packagesPresent,
388533
+ message: ".gtd/LEARNINGS.md + packages"
388534
+ },
388535
+ {
388536
+ isViolated: (p4) => p4.learningMsgPresent && p4.reviewPresent,
388537
+ message: ".gtd/LEARNINGS.md + .gtd/REVIEW.md"
388538
+ },
388539
+ {
388540
+ isViolated: (p4) => p4.learningMsgPresent && p4.feedbackPresent,
388541
+ message: ".gtd/LEARNINGS.md + .gtd/FEEDBACK.md"
388542
+ },
388543
+ {
388544
+ isViolated: (p4) => p4.learningMsgPresent && p4.errorsPresent,
388545
+ message: ".gtd/LEARNINGS.md + .gtd/ERRORS.md"
388546
+ },
388547
+ {
388548
+ isViolated: (p4) => p4.learningMsgPresent && p4.healthPresent,
388549
+ message: ".gtd/LEARNINGS.md + .gtd/HEALTH.md"
388550
+ },
388551
+ {
388552
+ isViolated: (p4) => p4.learningMsgPresent && p4.squashMsgPresent,
388553
+ message: ".gtd/LEARNINGS.md + .gtd/SQUASH_MSG.md"
388554
+ }
388555
+ ];
388462
388556
  var isReviewPackagesConflict = (p4) => p4.reviewPresent && p4.packagesPresent;
388463
388557
  var isReviewCommittedTodoConflict = (p4) => p4.reviewPresent && p4.todoCommitted;
388464
388558
  var isUncommittedReviewWithTodoConflict = (p4) => p4.reviewPresent && !(p4.reviewCommitted || p4.reviewDirty) && p4.todoExists;
@@ -388483,12 +388577,53 @@ var reviewAndFeedbackRules = [
388483
388577
  { isViolated: isErrorsWithoutPackages, message: ".gtd/ERRORS.md without packages" }
388484
388578
  ];
388485
388579
  var assertLegal = (p4) => {
388486
- for (const rule of [...healthFileConflictRules, ...reviewAndFeedbackRules]) {
388580
+ for (const rule of [
388581
+ ...healthFileConflictRules,
388582
+ ...learningFileConflictRules,
388583
+ ...reviewAndFeedbackRules
388584
+ ]) {
388487
388585
  if (rule.isViolated(p4)) {
388488
388586
  throw new GtdStateError("illegal-combination", `illegal combination: ${rule.message}`);
388489
388587
  }
388490
388588
  }
388491
388589
  };
388590
+ var nextAfterReviewOrLearning = (flags, state, learningAlreadyRan) => {
388591
+ if (!learningAlreadyRan && flags.learningEnabled && flags.hasSquashBase) {
388592
+ return { kind: "mid-chain", state, actor: "agent", action: { kind: "writeLearningTemplate" } };
388593
+ }
388594
+ return flags.squashEnabled && flags.hasSquashBase ? { kind: "mid-chain", state, actor: "agent", action: { kind: "writeSquashTemplate" } } : { kind: "rest", state: "idle", actor: "human" };
388595
+ };
388596
+ var classifyLearningTurn = (actor, gate, flags) => {
388597
+ if (actor === "agent" && gate === "learning") {
388598
+ return flags.hasSquashBase && !flags.learningMsgIsTemplate ? {
388599
+ kind: "mid-chain",
388600
+ state: "learning",
388601
+ actor: "agent",
388602
+ action: { kind: "commitRouting", subject: "gtd: learning drafted" }
388603
+ } : { kind: "rest", state: "learning", actor: "agent" };
388604
+ }
388605
+ if (actor === "human" && gate === "learning") {
388606
+ return {
388607
+ kind: "mid-chain",
388608
+ state: "learning",
388609
+ actor: "human",
388610
+ action: { kind: "commitRouting", subject: "gtd: learning approved" }
388611
+ };
388612
+ }
388613
+ if (actor === "agent" && gate === "learning-apply") {
388614
+ return {
388615
+ kind: "mid-chain",
388616
+ state: "learning-apply",
388617
+ actor: "agent",
388618
+ action: {
388619
+ kind: "commitRouting",
388620
+ subject: "gtd: learning applied",
388621
+ removeLearning: true
388622
+ }
388623
+ };
388624
+ }
388625
+ return null;
388626
+ };
388492
388627
  var classifyHead = (subject, flags) => {
388493
388628
  const parsed = parseSubject(subject);
388494
388629
  if (parsed.kind === "turn") {
@@ -388561,6 +388696,8 @@ var classifyHead = (subject, flags) => {
388561
388696
  // squashBase filled in by the caller
388562
388697
  } : { kind: "rest", state: "squashing", actor: "agent" };
388563
388698
  }
388699
+ const learningTurn = classifyLearningTurn(actor, gate, flags);
388700
+ if (learningTurn !== null) return learningTurn;
388564
388701
  if (actor === "agent" && gate === "health-fixing") {
388565
388702
  return {
388566
388703
  kind: "mid-chain",
@@ -388594,12 +388731,7 @@ var classifyHead = (subject, flags) => {
388594
388731
  action: { kind: "closePackage" }
388595
388732
  } : { kind: "rest", state: "agentic-review", actor: "agent" };
388596
388733
  }
388597
- return flags.squashEnabled && flags.squashAfterGreen ? {
388598
- kind: "mid-chain",
388599
- state: "testing",
388600
- actor: "agent",
388601
- action: { kind: "writeSquashTemplate" }
388602
- } : { kind: "rest", state: "idle", actor: "human" };
388734
+ return nextAfterReviewOrLearning(flags, "testing", false);
388603
388735
  case "errors":
388604
388736
  return flags.errorsPresent ? { kind: "rest", state: "escalate", actor: "human" } : { kind: "rest", state: "fixing", actor: "agent" };
388605
388737
  case "package-done":
@@ -388609,16 +388741,19 @@ var classifyHead = (subject, flags) => {
388609
388741
  case "review-feedback":
388610
388742
  return { kind: "rest", state: "grilling", actor: "agent" };
388611
388743
  case "done":
388612
- return flags.squashEnabled && flags.hasSquashBase ? {
388613
- kind: "mid-chain",
388614
- state: "done",
388615
- actor: "agent",
388616
- action: { kind: "writeSquashTemplate" }
388617
- } : { kind: "rest", state: "idle", actor: "human" };
388744
+ return nextAfterReviewOrLearning(flags, "done", false);
388618
388745
  case "squash-template":
388619
388746
  return { kind: "rest", state: "squashing", actor: "agent" };
388620
388747
  case "reviewing":
388621
388748
  return { kind: "rest", state: "review", actor: "agent" };
388749
+ case "learning-template":
388750
+ return { kind: "rest", state: "learning", actor: "agent" };
388751
+ case "learning-drafted":
388752
+ return { kind: "rest", state: "await-learning-review", actor: "human" };
388753
+ case "learning-approved":
388754
+ return { kind: "rest", state: "learning-apply", actor: "agent" };
388755
+ case "learning-applied":
388756
+ return nextAfterReviewOrLearning(flags, "learning-applied", true);
388622
388757
  case "health-check":
388623
388758
  return flags.errorsPresent ? { kind: "rest", state: "escalate", actor: "human" } : { kind: "rest", state: "health-fixing", actor: "agent" };
388624
388759
  case "health-fix":
@@ -388636,18 +388771,24 @@ var STATE_IS_OWN_GATE = /* @__PURE__ */ new Set([
388636
388771
  "review",
388637
388772
  "squashing",
388638
388773
  "health-fixing",
388639
- "escalate"
388774
+ "escalate",
388775
+ "learning",
388776
+ "learning-apply"
388640
388777
  ]);
388641
- var gateForState = (state) => STATE_IS_OWN_GATE.has(state) ? state : "review";
388778
+ var GATE_OVERRIDES = {
388779
+ "await-learning-review": "learning"
388780
+ };
388781
+ var gateForState = (state) => STATE_IS_OWN_GATE.has(state) ? state : GATE_OVERRIDES[state] ?? "review";
388642
388782
  var INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
388643
388783
  "grilling",
388644
388784
  "grilled",
388645
388785
  "building",
388646
388786
  "fixing",
388647
388787
  "agentic-review",
388648
- "review"
388788
+ "review",
388789
+ "learning-apply"
388649
388790
  ]);
388650
- var isInertEmptyAgentRest = (state, p4) => p4.workingTreeClean && (INERT_EMPTY_AGENT_GATES.has(state) || state === "squashing" && p4.squashMsgIsTemplate);
388791
+ var isInertEmptyAgentRest = (state, p4) => p4.workingTreeClean && (INERT_EMPTY_AGENT_GATES.has(state) || state === "squashing" && p4.squashMsgIsTemplate || state === "learning" && p4.learningMsgIsTemplate);
388651
388792
  var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388652
388793
  const headIsFixerTurn = (() => {
388653
388794
  const parsed = parseSubject(head7);
@@ -388676,7 +388817,6 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388676
388817
  action: { kind: "closePackage" }
388677
388818
  } : { kind: "rest", state: "fixing", actor: "agent" };
388678
388819
  }
388679
- const squashAfterGreen = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388680
388820
  const reviewSubstantive = p4.headTurnReviewSubstantive !== void 0 ? p4.headTurnReviewSubstantive : p4.reviewDirty && !p4.reviewCheckboxOnly;
388681
388821
  const flags = {
388682
388822
  headTurnIsEmpty: p4.headTurnIsEmpty,
@@ -388684,7 +388824,8 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388684
388824
  agenticReviewForceApproved: forceApprove,
388685
388825
  squashEnabled: p4.squashEnabled,
388686
388826
  hasSquashBase: p4.squashBase !== void 0,
388687
- squashAfterGreen,
388827
+ learningEnabled: p4.learningEnabled,
388828
+ learningMsgIsTemplate: p4.learningMsgIsTemplate,
388688
388829
  reviewSubstantive,
388689
388830
  // A pending (uncommitted) deletion of ERRORS.md still counts as "ERRORS.md
388690
388831
  // was committed at this HEAD" for classification purposes: `fs.exists`
@@ -388722,7 +388863,7 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388722
388863
  kind: "runHealthCheck",
388723
388864
  errorCount: counters.healthFixCount,
388724
388865
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388725
- squashAfterGreen: classified.action.squashAfterGreen
388866
+ chainAfterGreen: classified.action.chainAfterGreen
388726
388867
  }
388727
388868
  };
388728
388869
  }
@@ -388791,7 +388932,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388791
388932
  }
388792
388933
  const healthCheckCapReached = head7 === "gtd: health-check" && counters.healthFixCount >= p4.fixAttemptCap;
388793
388934
  if (head7 === "gtd: health-fix" && baseline.state === "idle" || healthCheckCapReached && baseline.state === "health-fixing") {
388794
- const squashAfterGreenAtHealthFix = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388935
+ const chainAfterGreenAtHealthFix = (p4.squashEnabled || p4.learningEnabled) && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388795
388936
  return {
388796
388937
  state: "idle",
388797
388938
  actor: "agent",
@@ -388800,7 +388941,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388800
388941
  kind: "runHealthCheck",
388801
388942
  errorCount: counters.healthFixCount,
388802
388943
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388803
- squashAfterGreen: squashAfterGreenAtHealthFix
388944
+ chainAfterGreen: chainAfterGreenAtHealthFix
388804
388945
  },
388805
388946
  context: context10
388806
388947
  };
@@ -388815,7 +388956,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388815
388956
  };
388816
388957
  }
388817
388958
  if (baseline.state === "idle" && invoker === "human") {
388818
- const squashAfterGreen = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388959
+ const chainAfterGreen = (p4.squashEnabled || p4.learningEnabled) && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388819
388960
  return {
388820
388961
  state: "idle",
388821
388962
  actor: "human",
@@ -388824,7 +388965,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388824
388965
  kind: "runHealthCheck",
388825
388966
  errorCount: counters.healthFixCount,
388826
388967
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388827
- squashAfterGreen
388968
+ chainAfterGreen
388828
388969
  },
388829
388970
  context: context10
388830
388971
  };
@@ -389399,6 +389540,15 @@ var await_review_default = '<%~ include("@header") %>\n\n`.gtd/REVIEW.md` holds
389399
389540
  // src/prompts/squashing.md
389400
389541
  var squashing_default = '<%~ include("@header") %>\n\nThe process is **approved and done**. `.gtd/SQUASH_MSG.md` has already been\ncommitted with a template \u2014 your job is to overwrite it with the real\nconventional-commits squash message and finish your turn.\n\n### Step 1 \u2014 Extract decisions from grilling rounds\n\nScan the git history of recent `gtd: ...` / `gtd(agent): ...` /\n`gtd(human): ...` commits. Look for changes to `.gtd/TODO.md` \u2014 specifically the\n`## Captured input` sections and any edits to plan text. Extract **key\ndecisions, trade-offs, and design choices** made during grilling rounds. These\nwill appear in the commit body so the history is self-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\n sessions. Omit if there were no meaningful decisions to capture.\n\n### Step 3 \u2014 Overwrite .gtd/SQUASH_MSG.md\n\nReplace the **entire content** of `.gtd/SQUASH_MSG.md` (plain text, no markdown\nwrapper, no leftover template scaffolding) with the message from Step 2, then\nleave it uncommitted and finish your turn \u2014 `gtd step-agent` performs the\nsquash using this file\'s content.\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<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n';
389401
389542
 
389543
+ // src/prompts/learning.md
389544
+ var learning_default = '<%~ include("@header") %>\n\nThe process is **approved and done**, and headed for a squash. Before that\nhappens, `.gtd/LEARNINGS.md` has been committed with a template \u2014 your job is\nto overwrite it with the real, distilled learnings from this cycle so they\nsurvive the squash even though the granular history won\'t.\n\n### Step 1 \u2014 Walk the cycle\'s history\n\nScan the git history back to the cycle\'s start (the full-process diff is\ninlined below, and `git log` over the same range shows every `gtd: ...` /\n`gtd(agent): ...` / `gtd(human): ...` commit). Look specifically for:\n\n- **Test failures and fixes** \u2014 each `gtd: errors` round and the fix that\n followed it. What broke, and what actually made it pass?\n- **Review feedback** \u2014 human `.gtd/REVIEW.md` notes and `gtd: review\n feedback` detours. What did the human have to correct that the agent got\n wrong on its own?\n- **Health-check rounds** \u2014 any `gtd: health-check` / `gtd: health-fix`\n cycles. Environmental or systemic issues surfaced outside normal building?\n- **Grilling decisions** \u2014 `.gtd/TODO.md`\'s "## Captured input" sections and\n plan edits. Trade-offs or conventions the human specified that a future\n agent should already know.\n\n### Step 2 \u2014 Distill, don\'t transcribe\n\nFor each thing you found, ask: is this a **durable, generalizable lesson**\n(a convention, a gotcha, a pattern this project wants followed next time), or\na **one-off detail** specific to this change? Keep only the former. A future\nagent reading this should learn something that changes how it works on this\nproject, not a recap of what happened.\n\n### Step 3 \u2014 Overwrite .gtd/LEARNINGS.md\n\nReplace the **entire content** of `.gtd/LEARNINGS.md` with your distilled\nlearnings (plain markdown, no leftover template scaffolding). If nothing\ndurable surfaced this cycle, write a short explicit note saying so \u2014 don\'t\nleave the placeholder text in place. Leave the file uncommitted and finish\nyour turn; a human reviews it next before it\'s integrated into the project\'s\nown memory (CLAUDE.md/AGENTS.md/docs).\n<% if (it.context.squashDiff && it.context.squashDiff.trim()) { %>\n<%= it.context.squashBase !== undefined ? "\\nCycle 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<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n';
389545
+
389546
+ // src/prompts/await-learning-review.md
389547
+ var await_learning_review_default = "<%~ include(\"@header\") %>\n\n`.gtd/LEARNINGS.md` holds the distilled learnings from this cycle, drafted by\nthe agent. This is a human gate; there is nothing for the agent to do.\n\nTell the user:\n\n- Read `.gtd/LEARNINGS.md`.\n- Delete anything not worth keeping, or edit/add anything the draft missed.\n- Run `gtd step` \u2014 with or without edits, either way proceeds: the agent\n integrates whatever remains into the project's own memory\n (CLAUDE.md/AGENTS.md/docs) next.\n\nThere is no reject path \u2014 this gate only refines what gets kept, it doesn't\nsend the cycle back for rework.\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
389548
+
389549
+ // src/prompts/learning-apply.md
389550
+ var learning_apply_default = "<%~ include(\"@header\") %>\n\nThe human has approved (and possibly amended) `.gtd/LEARNINGS.md`. Your job is\nto integrate those learnings into the project's own memory, not to touch\n`.gtd/LEARNINGS.md` itself.\n\n1. **Read `.gtd/LEARNINGS.md`.**\n2. **Pick the right home for each point**, using your judgment and this\n project's existing convention:\n - An existing `CLAUDE.md` or `AGENTS.md` at the repo root (or a directory\n closer to what the learning concerns) is usually the right place.\n - If neither exists and the learnings warrant one, create `AGENTS.md` at\n the repo root.\n - A learning about a specific subsystem may fit better in that\n subsystem's own docs than a root-level file \u2014 use your judgment.\n3. **Integrate, don't just append** \u2014 merge each point into the relevant\n section of the target file(s); rephrase for clarity, dedupe against what's\n already documented, and keep the existing document's structure and tone.\n4. **Never delete or edit `.gtd/LEARNINGS.md`** \u2014 the machine removes it once\n your turn is captured.\n5. **Leave every change uncommitted and finish your turn.**\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
389551
+
389402
389552
  // src/prompts/escalate.md
389403
389553
  var escalate_default = '<%~ include("@header") %>\n\nThe agent was not able to fix all errors on its own. The last error report is\nstored in `.gtd/ERRORS.md` for a human to investigate.\n\nNext steps for the human developer:\n\n1. Investigate and fix errors reported in `.gtd/ERRORS.md`\n2. Delete `.gtd/ERRORS.md`\n3. Run `gtd step` when you are ready to continue\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n';
389404
389554
 
@@ -389415,6 +389565,9 @@ var PROMPT_STATES = /* @__PURE__ */ new Set([
389415
389565
  "review",
389416
389566
  "await-review",
389417
389567
  "squashing",
389568
+ "learning",
389569
+ "await-learning-review",
389570
+ "learning-apply",
389418
389571
  "escalate",
389419
389572
  "idle",
389420
389573
  "health-fixing"
@@ -389428,7 +389581,9 @@ var MODEL_STATE = {
389428
389581
  "health-fixing": "fixing",
389429
389582
  "agentic-review": "agentic-review",
389430
389583
  review: "clean",
389431
- squashing: "clean"
389584
+ squashing: "clean",
389585
+ learning: "clean",
389586
+ "learning-apply": "clean"
389432
389587
  };
389433
389588
  var builtinResolveModel = (state) => builtinTierDefault[stateTier[state]];
389434
389589
  var fenceFor = (content) => {
@@ -389452,6 +389607,9 @@ eta.loadTemplate("@agentic-review", agentic_review_default);
389452
389607
  eta.loadTemplate("@review", review_default);
389453
389608
  eta.loadTemplate("@await-review", await_review_default);
389454
389609
  eta.loadTemplate("@squashing", squashing_default);
389610
+ eta.loadTemplate("@learning", learning_default);
389611
+ eta.loadTemplate("@await-learning-review", await_learning_review_default);
389612
+ eta.loadTemplate("@learning-apply", learning_apply_default);
389455
389613
  eta.loadTemplate("@escalate", escalate_default);
389456
389614
  eta.loadTemplate("@idle", idle_default);
389457
389615
  eta.readFile = null;
@@ -389464,6 +389622,9 @@ var STATE_TEMPLATE = {
389464
389622
  review: "@review",
389465
389623
  "await-review": "@await-review",
389466
389624
  squashing: "@squashing",
389625
+ learning: "@learning",
389626
+ "await-learning-review": "@await-learning-review",
389627
+ "learning-apply": "@learning-apply",
389467
389628
  escalate: "@escalate",
389468
389629
  idle: "@idle",
389469
389630
  "health-fixing": "@health-fixing"
@@ -389493,6 +389654,78 @@ var buildPrompt = (result, resolveModel = builtinResolveModel, output = "plain")
389493
389654
  return raw.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
389494
389655
  };
389495
389656
 
389657
+ // src/ReviewWindow.ts
389658
+ var REVIEW_HEAD_REF = "refs/gtd/review-head";
389659
+ var REVIEW_BASE_REF = "refs/gtd/review-base";
389660
+ var AWAITING_REVIEW_SUBJECT = ROUTING_SUBJECT["awaiting-review"];
389661
+ var subjectOf2 = (message) => message.split("\n")[0] ?? "";
389662
+ var reviewWindowBase = (history) => {
389663
+ const beforeHead = history.slice(0, -1);
389664
+ let lastDoneIdx = -1;
389665
+ for (let i = 0; i < beforeHead.length; i++) {
389666
+ if (subjectOf2(beforeHead[i].message) === ROUTING_SUBJECT.done) lastDoneIdx = i;
389667
+ }
389668
+ const currentCycle = beforeHead.slice(lastDoneIdx + 1);
389669
+ let anchor;
389670
+ let lastAwaitingReview;
389671
+ let firstGrilling;
389672
+ for (const c7 of currentCycle) {
389673
+ const parsed = parseSubject(subjectOf2(c7.message));
389674
+ if (parsed.kind === "routing" && parsed.phase === "reviewing" && parsed.param !== void 0) {
389675
+ anchor = parsed.param;
389676
+ }
389677
+ if (parsed.kind === "routing" && parsed.phase === "awaiting-review") {
389678
+ lastAwaitingReview = c7.hash;
389679
+ }
389680
+ if (parsed.kind === "turn" && parsed.gate === "grilling" && firstGrilling === void 0) {
389681
+ firstGrilling = c7.hash;
389682
+ }
389683
+ }
389684
+ return anchor ?? lastAwaitingReview ?? firstGrilling;
389685
+ };
389686
+ var closeReviewWindow = Effect_exports.gen(
389687
+ function* () {
389688
+ const git = yield* GitService;
389689
+ const savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
389690
+ if (Option_exports.isNone(savedHead)) return { closed: false };
389691
+ const base = yield* git.readRefOption(REVIEW_BASE_REF);
389692
+ if (Option_exports.isSome(base)) {
389693
+ const onReviewedBranch = yield* git.isAncestor(base.value, "HEAD");
389694
+ if (!onReviewedBranch) {
389695
+ return yield* Effect_exports.fail(
389696
+ new Error(
389697
+ "a gtd review checkout window is open but HEAD has moved off the reviewed branch \u2014 return to it, or restore manually with `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"
389698
+ )
389699
+ );
389700
+ }
389701
+ }
389702
+ yield* git.mixedResetTo(REVIEW_HEAD_REF);
389703
+ yield* git.deleteRef(REVIEW_HEAD_REF);
389704
+ yield* git.deleteRef(REVIEW_BASE_REF);
389705
+ return { closed: true };
389706
+ }
389707
+ );
389708
+ var openReviewWindow = Effect_exports.gen(
389709
+ function* () {
389710
+ const git = yield* GitService;
389711
+ const hasCommits = yield* git.hasCommits();
389712
+ if (!hasCommits) return { opened: false };
389713
+ const subject = yield* git.lastCommitSubject();
389714
+ if (subject !== AWAITING_REVIEW_SUBJECT) return { opened: false };
389715
+ const history = yield* git.commitHistory();
389716
+ const base = reviewWindowBase(history);
389717
+ if (base === void 0) return { opened: false };
389718
+ const headHash = yield* git.resolveRef("HEAD");
389719
+ if (base === headHash) return { opened: false };
389720
+ yield* git.updateRef(REVIEW_BASE_REF, base);
389721
+ yield* git.updateRef(REVIEW_HEAD_REF, headHash);
389722
+ yield* git.mixedResetTo(base);
389723
+ yield* git.restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"]);
389724
+ yield* git.addIntentToAdd();
389725
+ return { opened: true };
389726
+ }
389727
+ );
389728
+
389496
389729
  // src/State.ts
389497
389730
  var edgeActionHandlers = {
389498
389731
  captureTurn: (a5) => `capture the ${a5.actor} turn as "gtd(${a5.actor}): ${a5.gate}"`,
@@ -389503,6 +389736,7 @@ var edgeActionHandlers = {
389503
389736
  if (a5.removeReview) removed.push(".gtd/REVIEW.md");
389504
389737
  if (a5.removeFeedback) removed.push(".gtd/FEEDBACK.md");
389505
389738
  if (a5.removeHealth) removed.push(".gtd/HEALTH.md");
389739
+ if (a5.removeLearning) removed.push(".gtd/LEARNINGS.md");
389506
389740
  if (removed.length > 0) msg += ` (removing ${removed.join(", ")})`;
389507
389741
  return msg;
389508
389742
  },
@@ -389510,7 +389744,8 @@ var edgeActionHandlers = {
389510
389744
  closePackage: () => "close the active package",
389511
389745
  writeSquashTemplate: () => "write the squash message template",
389512
389746
  squashCommit: (a5) => `squash the cycle onto ${a5.squashBase}`,
389513
- runHealthCheck: (a5) => `run the health check (attempt ${a5.errorCount + 1}${a5.capReached ? ", cap reached" : ""}${a5.squashAfterGreen ? ", squash after green" : ""})`
389747
+ writeLearningTemplate: () => "write the learnings template",
389748
+ runHealthCheck: (a5) => `run the health check (attempt ${a5.errorCount + 1}${a5.capReached ? ", cap reached" : ""}${a5.chainAfterGreen ? ", chain after green" : ""})`
389514
389749
  };
389515
389750
  var describeEdgeAction = (a5) => edgeActionHandlers[a5.kind](a5);
389516
389751
  var describeStatus = (prediction) => ({
@@ -389807,9 +390042,13 @@ function makeProgram(opts = {}) {
389807
390042
  const git = yield* GitService;
389808
390043
  const fs9 = yield* FileSystem_exports.FileSystem;
389809
390044
  yield* assertRunningFromRepoRoot(git, fs9);
390045
+ yield* closeReviewWindow;
389810
390046
  yield* (yield* ConfigInit).ensure;
389811
390047
  const config2 = yield* ConfigService;
389812
- yield* dispatchKnownSubcommand(sub, argv, git, config2, json2, write4);
390048
+ yield* dispatchKnownSubcommand(sub, argv, git, config2, json2, write4).pipe(
390049
+ Effect_exports.tap(() => openReviewWindow),
390050
+ Effect_exports.tapError(() => openReviewWindow.pipe(Effect_exports.ignore))
390051
+ );
389813
390052
  }).pipe(
389814
390053
  json2 ? Effect_exports.catchAll(
389815
390054
  (error) => Effect_exports.sync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -57,6 +57,9 @@
57
57
  "squash": {
58
58
  "type": "boolean"
59
59
  },
60
+ "learning": {
61
+ "type": "boolean"
62
+ },
60
63
  "fixAttemptCap": {
61
64
  "$ref": "#/$defs/Int",
62
65
  "description": "a non-negative number",