@pmelab/gtd 2.2.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
 
@@ -428,12 +429,14 @@ GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd-loop
428
429
 
429
430
  ## States & subjects overview
430
431
 
431
- `resolve()` lands on exactly one of **16 states**: `grilling`, `grilled`,
432
+ `resolve()` lands on exactly one of **20 states**: `grilling`, `grilled`,
432
433
  `planning`, `building`, `testing`, `fixing`, `escalate`, `agentic-review`,
433
- `close-package`, `review`, `await-review`, `done`, `squashing`, `idle`,
434
- `health-check`, `health-fixing`. Each state has a fixed awaited actor (see
435
- `awaitedActor` in `src/Machine.ts`): `idle`, `escalate`, and `await-review`
436
- 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**.
437
440
 
438
441
  For the full precedence ladder, illegal combinations, and the counter folds that
439
442
  drive the fix loops, see [STATES.md](STATES.md) — this section is a summary.
@@ -452,6 +455,8 @@ The closed set of gates:
452
455
  | `agentic-review` | agent (writes .gtd/FEEDBACK.md verdict) |
453
456
  | `review` | agent (writes .gtd/REVIEW.md) / human (approves or gives feedback) |
454
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) |
455
460
  | `health-fixing` | agent (idle health-check repair) |
456
461
  | `escalate` | human (deletes .gtd/ERRORS.md to resume) |
457
462
 
@@ -462,7 +467,8 @@ agent "wins": `gtd: grilled`, `gtd: planning`, `gtd: tests green`,
462
467
  `gtd: errors`, `gtd: package done`, `gtd: awaiting review`,
463
468
  `gtd: review feedback`, `gtd: done`, `gtd: squash template`,
464
469
  `gtd: reviewing <hash>` (parameterized, from `gtd review`), `gtd: health-check`,
465
- `gtd: health-fix`.
470
+ `gtd: health-fix`, `gtd: learning template`, `gtd: learning drafted`,
471
+ `gtd: learning approved`, `gtd: learning applied`.
466
472
 
467
473
  Everything else — any non-`gtd` subject, and any `gtd: *` subject outside this
468
474
  closed set — is a **boundary commit**: inert as far as the machine's grammar is
@@ -526,23 +532,25 @@ the threshold simply closes the package as usual.) Setting
526
532
 
527
533
  A **do-nothing agent invocation** — `gtd step-agent` on a clean tree at ANY
528
534
  agent-awaited rest whose move is a file artifact (`grilling`, `grilled`,
529
- `building`, `fixing`, `agentic-review`, `review`, and `squashing` while
530
- `.gtd/SQUASH_MSG.md` still holds the unmodified template) is inert: zero
531
- commits, no state consumed; `gtd next` re-emits the same prompt. This is
532
- load-bearing for the loop protocol, whose every iteration opens with
533
- `gtd step-agent` before the agent has acted: without the guard that opening beat
534
- would author junk empty turns and worse, consume workflow state (an empty
535
- decompose turn would delete `.gtd/TODO.md` with no packages written; an empty
536
- squashing turn would squash the cycle under the placeholder template). The same
537
- guards hold at the classification layer for histories that already carry such
538
- turns: a `gtd(agent): grilled` HEAD only routes to `gtd: planning` when packages
539
- exist, a `gtd(agent): review` HEAD only routes to `gtd: awaiting review` when
540
- `.gtd/REVIEW.md` exists, and a squashing turn only squashes once the template
541
- has been overwritten. The one deliberate exception is `health-fixing`, whose
542
- empty turn is meaningful (the failure may have been environmental the machine
543
- removes `.gtd/HEALTH.md` and re-tests). Human gates are unaffected: an empty
544
- **human** turn stays a signal (accept-defaults at grilling, clean approval at
545
- 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).
546
554
 
547
555
  ### Human review gate
548
556
 
@@ -586,22 +594,46 @@ their commit message is discarded; linked `git worktree` checkouts are
586
594
  unsupported. If you switch branches mid-review, gtd refuses to touch the foreign
587
595
  branch and prints the manual recovery command.
588
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
+
589
618
  ### Squash
590
619
 
591
- With `squash: true` (the default), `gtd: done` is **not** a rest — the same
592
- chain continues straight to `gtd: squash template`, writing and committing a
593
- `.gtd/SQUASH_MSG.md` template. `gtd next` then emits the squashing prompt: the
594
- agent overwrites `.gtd/SQUASH_MSG.md` with a real conventional-commits message
595
- (drawing on grilling-round decisions from history) and finishes its turn.
596
- `gtd step-agent` then performs the squash itself: `git reset --soft <base>` +
597
- `git commit`, collapsing every intermediate `gtd: *` commit of the cycle into
598
- one including any review-feedback detours: the squash base is the cycle's
599
- ORIGINAL start (the first grilling run since the previous `gtd: done` boundary,
600
- or the `gtd: reviewing <hash>` anchor for an ad-hoc review cycle), not the most
601
- recent re-grilling round the collapse folds the whole cycle into one, using
602
- the overwritten message's content verbatim (turn position, not message content,
603
- triggers the squash). With `squash: false`, `gtd: done` is the resting boundary
604
- 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.
605
637
 
606
638
  ### Health check
607
639
 
@@ -610,9 +642,9 @@ runs `testCommand` as a health check rather than settling immediately. Green
610
642
  settles idle with zero commits. Red below `fixAttemptCap` writes
611
643
  `.gtd/HEALTH.md` and rests at **Health Fixing** for the agent; the fixer's own
612
644
  turn (`gtd(agent): health-fixing`) removes `.gtd/HEALTH.md` and re-tests in the
613
- same chain — a green re-test continues to squash (if enabled) or idle; red
614
- repeats the health-fix loop; red at the cap writes `.gtd/ERRORS.md` and
615
- 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.
616
648
 
617
649
  ### Escalate / budget reset
618
650
 
@@ -624,24 +656,28 @@ fix-attempt budget to zero.
624
656
 
625
657
  ## States & subjects: overview table
626
658
 
627
- | State | Awaits | Turn/routing subject at rest |
628
- | ---------------- | -------------- | ---------------------------------------------------------------- |
629
- | `grilling` | human or agent | `gtd(human): grilling` / `gtd(agent): grilling` |
630
- | `grilled` | agent | `gtd: grilled` |
631
- | `planning` | agent | `.gtd/` modified |
632
- | `building` | agent | `gtd: planning` / `gtd: package done` |
633
- | `testing` | — (edge-only) | mid-chain only |
634
- | `fixing` | agent | `gtd: errors` |
635
- | `escalate` | human | `.gtd/ERRORS.md` present |
636
- | `agentic-review` | agent | `gtd: tests green` |
637
- | `close-package` | — (edge-only) | mid-chain only |
638
- | `review` | agent | `gtd: package done` (no more packages) / `gtd: reviewing <hash>` |
639
- | `await-review` | human | `gtd: awaiting review` |
640
- | `done` | — (edge-only) | `gtd: done` |
641
- | `squashing` | agent | `gtd: squash template` |
642
- | `idle` | human | no steering files, green health check |
643
- | `health-check` | — (edge-only) | mid-chain only |
644
- | `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 |
645
681
 
646
682
  See [STATES.md](STATES.md) for the full precedence ladder, the counter folds,
647
683
  and every illegal steering-file combination.
@@ -674,13 +710,19 @@ built-in defaults apply. Supported filenames (searched in this order):
674
710
  - **`agenticReview`** (boolean, default `true`) — kill-switch for the
675
711
  per-package Agentic Review gate. Set `false` to force-approve every package
676
712
  and proceed directly to human review.
677
- - **`squash`** (boolean, default `true`) — after `gtd: done`, collapse the
678
- cycle's `gtd: *` commits into a single conventional-commits commit. Set
679
- `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`.
680
722
  - **`models`** — model selection for the subagent-spawning states:
681
723
  - `planning` — high-reasoning tier (default `claude-opus-4-8`), used by
682
724
  `decompose` (the `grilled`/`planning` states), `grilling`, `agentic-review`,
683
- and `clean` (the `review`/`squashing` states).
725
+ and `clean` (the `review`/`squashing`/`learning`/`learning-apply` states).
684
726
  - `execution` — everyday tier (default `claude-sonnet-4-8`), used by
685
727
  `building` and `fixing`.
686
728
  - `states.*` — per-state overrides keyed by `decompose`, `grilling`,
@@ -739,6 +781,7 @@ fixAttemptCap: 3
739
781
  reviewThreshold: 3
740
782
  agenticReview: true
741
783
  squash: true
784
+ learning: true
742
785
  models:
743
786
  planning: claude-opus-4-8
744
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
  };
@@ -387765,7 +387768,11 @@ var ROUTING_SUBJECT = {
387765
387768
  done: "gtd: done",
387766
387769
  "squash-template": "gtd: squash template",
387767
387770
  "health-check": "gtd: health-check",
387768
- "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"
387769
387776
  };
387770
387777
  var reviewingSubject = (baseHash) => `gtd: reviewing ${baseHash}`;
387771
387778
  var TURN_RE = /^gtd\((human|agent)\): (.+)$/;
@@ -387778,7 +387785,9 @@ var TURN_GATES = /* @__PURE__ */ new Set([
387778
387785
  "review",
387779
387786
  "squashing",
387780
387787
  "health-fixing",
387781
- "escalate"
387788
+ "escalate",
387789
+ "learning",
387790
+ "learning-apply"
387782
387791
  ]);
387783
387792
  var ROUTING_SUBJECT_TO_PHASE = new Map(
387784
387793
  Object.entries(ROUTING_SUBJECT).map(([phase, subject]) => [
@@ -387818,6 +387827,7 @@ var FEEDBACK_FILE = `${GTD_DIR}/FEEDBACK.md`;
387818
387827
  var ERRORS_FILE = `${GTD_DIR}/ERRORS.md`;
387819
387828
  var HEALTH_FILE = `${GTD_DIR}/HEALTH.md`;
387820
387829
  var SQUASH_MSG_FILE = `${GTD_DIR}/SQUASH_MSG.md`;
387830
+ var LEARNINGS_FILE = `${GTD_DIR}/LEARNINGS.md`;
387821
387831
  var LEGACY_FEEDBACK_FILE = "FEEDBACK.md";
387822
387832
  var emptyFailureSentinel = (command, exitCode3) => `Test command \`${command}\` failed with exit code ${exitCode3} and produced no output.`;
387823
387833
  var DONE_SUBJECT = "gtd: done";
@@ -387826,6 +387836,18 @@ var GATE_OWN_STEERING_FILE = {
387826
387836
  grilling: TODO_FILE,
387827
387837
  review: REVIEW_FILE
387828
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
+ ]);
387829
387851
  var turnDiffExcludes = (gate) => {
387830
387852
  const ownFile = GATE_OWN_STEERING_FILE[gate];
387831
387853
  return ownFile ? [...WORKFLOW_FILE_EXCLUDES, `!${ownFile}`] : WORKFLOW_FILE_EXCLUDES;
@@ -388096,8 +388118,8 @@ var gatherEvents = (invoker) => (
388096
388118
  let squashBase;
388097
388119
  let squashDiff;
388098
388120
  const headParsedForSquash = parseSubject(lastCommitSubject);
388099
- const inSquashChain = lastCommitSubject === DONE_SUBJECT || headParsedForSquash.kind === "routing" && headParsedForSquash.phase === "squash-template" || headParsedForSquash.kind === "turn" && headParsedForSquash.actor === "agent" && headParsedForSquash.gate === "squashing";
388100
- 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)) {
388101
388123
  const squashHistory = history;
388102
388124
  let lastDoneIdxForSquash = -1;
388103
388125
  let prevDoneIdx = -1;
@@ -388143,11 +388165,13 @@ var gatherEvents = (invoker) => (
388143
388165
  }
388144
388166
  const squashMsgPresent = yield* fs9.exists(resolve5(SQUASH_MSG_FILE));
388145
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();
388146
388170
  const healthPresent = yield* fs9.exists(resolve5(HEALTH_FILE));
388147
388171
  const healthContent = healthPresent ? yield* fs9.readFileString(resolve5(HEALTH_FILE)) : "";
388148
388172
  const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
388149
388173
  let healthFixBase;
388150
- if (config2.squash) {
388174
+ if (config2.squash || config2.learning) {
388151
388175
  let firstHealthCheckHash;
388152
388176
  let healthCheckCount = 0;
388153
388177
  for (const commit3 of commitEvents) {
@@ -388221,7 +388245,10 @@ var gatherEvents = (invoker) => (
388221
388245
  healthPresent,
388222
388246
  healthContent,
388223
388247
  healthCommitted,
388224
- ...healthFixBase !== void 0 ? { healthFixBase } : {}
388248
+ ...healthFixBase !== void 0 ? { healthFixBase } : {},
388249
+ learningEnabled: config2.learning,
388250
+ learningMsgPresent,
388251
+ learningMsgIsTemplate
388225
388252
  };
388226
388253
  const resolveEvent = { type: "RESOLVE", payload };
388227
388254
  return [...commitEvents, resolveEvent];
@@ -388245,6 +388272,15 @@ var SQUASH_TEMPLATE = [
388245
388272
  "Longer description of the change, if needed.",
388246
388273
  ""
388247
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");
388248
388284
  var perform = (action) => (
388249
388285
  // fallow-ignore-next-line complexity
388250
388286
  Effect_exports.gen(function* () {
@@ -388279,6 +388315,9 @@ var perform = (action) => (
388279
388315
  if (action.removeHealth === true) {
388280
388316
  yield* fs9.remove(resolve5(HEALTH_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388281
388317
  }
388318
+ if (action.removeLearning === true) {
388319
+ yield* fs9.remove(resolve5(LEARNINGS_FILE)).pipe(Effect_exports.catchAll(() => Effect_exports.void));
388320
+ }
388282
388321
  yield* git.commitAllWithPrefix(action.subject);
388283
388322
  return { stop: false };
388284
388323
  }
@@ -388326,6 +388365,14 @@ var perform = (action) => (
388326
388365
  yield* git.commitAllWithPrefix("gtd: squash template");
388327
388366
  return { stop: false };
388328
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
+ }
388329
388376
  // Squash: read SQUASH_MSG.md content (the real message authored by the
388330
388377
  // squashing turn), rm it, soft-reset to squashBase, commit-all under the
388331
388378
  // file's content as the message.
@@ -388337,10 +388384,11 @@ var perform = (action) => (
388337
388384
  return { stop: false };
388338
388385
  }
388339
388386
  // Health check: run tests on an idle/clean tree.
388340
- // Green, no squash-after chain queued → stop immediately, no commit/write.
388341
- // 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
388342
388390
  // observable green marker) and continue — the resolver chains
388343
- // `writeSquashTemplate` at that HEAD next.
388391
+ // `writeLearningTemplate` or `writeSquashTemplate` at that HEAD next.
388344
388392
  // Red below cap → write HEALTH.md, commit routing `gtd: health-check` (the
388345
388393
  // always-clean invariant: write-and-commit in the same chain).
388346
388394
  // Red at cap → write ERRORS.md, commit routing `gtd: health-check`.
@@ -388348,7 +388396,7 @@ var perform = (action) => (
388348
388396
  const runner = yield* TestRunner;
388349
388397
  const result = yield* runner.run();
388350
388398
  if (result.exitCode === 0) {
388351
- if (action.squashAfterGreen) {
388399
+ if (action.chainAfterGreen) {
388352
388400
  yield* git.commitAllWithPrefix("gtd: tests green");
388353
388401
  return { stop: false };
388354
388402
  }
@@ -388445,7 +388493,10 @@ var DEFAULT_PAYLOAD = {
388445
388493
  squashMsgIsTemplate: false,
388446
388494
  healthPresent: false,
388447
388495
  healthContent: "",
388448
- healthCommitted: false
388496
+ healthCommitted: false,
388497
+ learningEnabled: false,
388498
+ learningMsgPresent: false,
388499
+ learningMsgIsTemplate: false
388449
388500
  };
388450
388501
  var buildContext = (p4, counters) => ({
388451
388502
  testFixCount: counters.testFixCount,
@@ -388476,6 +388527,32 @@ var healthFileConflictRules = [
388476
388527
  message: ".gtd/HEALTH.md + .gtd/ERRORS.md"
388477
388528
  }
388478
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
+ ];
388479
388556
  var isReviewPackagesConflict = (p4) => p4.reviewPresent && p4.packagesPresent;
388480
388557
  var isReviewCommittedTodoConflict = (p4) => p4.reviewPresent && p4.todoCommitted;
388481
388558
  var isUncommittedReviewWithTodoConflict = (p4) => p4.reviewPresent && !(p4.reviewCommitted || p4.reviewDirty) && p4.todoExists;
@@ -388500,12 +388577,53 @@ var reviewAndFeedbackRules = [
388500
388577
  { isViolated: isErrorsWithoutPackages, message: ".gtd/ERRORS.md without packages" }
388501
388578
  ];
388502
388579
  var assertLegal = (p4) => {
388503
- for (const rule of [...healthFileConflictRules, ...reviewAndFeedbackRules]) {
388580
+ for (const rule of [
388581
+ ...healthFileConflictRules,
388582
+ ...learningFileConflictRules,
388583
+ ...reviewAndFeedbackRules
388584
+ ]) {
388504
388585
  if (rule.isViolated(p4)) {
388505
388586
  throw new GtdStateError("illegal-combination", `illegal combination: ${rule.message}`);
388506
388587
  }
388507
388588
  }
388508
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
+ };
388509
388627
  var classifyHead = (subject, flags) => {
388510
388628
  const parsed = parseSubject(subject);
388511
388629
  if (parsed.kind === "turn") {
@@ -388578,6 +388696,8 @@ var classifyHead = (subject, flags) => {
388578
388696
  // squashBase filled in by the caller
388579
388697
  } : { kind: "rest", state: "squashing", actor: "agent" };
388580
388698
  }
388699
+ const learningTurn = classifyLearningTurn(actor, gate, flags);
388700
+ if (learningTurn !== null) return learningTurn;
388581
388701
  if (actor === "agent" && gate === "health-fixing") {
388582
388702
  return {
388583
388703
  kind: "mid-chain",
@@ -388611,12 +388731,7 @@ var classifyHead = (subject, flags) => {
388611
388731
  action: { kind: "closePackage" }
388612
388732
  } : { kind: "rest", state: "agentic-review", actor: "agent" };
388613
388733
  }
388614
- return flags.squashEnabled && flags.squashAfterGreen ? {
388615
- kind: "mid-chain",
388616
- state: "testing",
388617
- actor: "agent",
388618
- action: { kind: "writeSquashTemplate" }
388619
- } : { kind: "rest", state: "idle", actor: "human" };
388734
+ return nextAfterReviewOrLearning(flags, "testing", false);
388620
388735
  case "errors":
388621
388736
  return flags.errorsPresent ? { kind: "rest", state: "escalate", actor: "human" } : { kind: "rest", state: "fixing", actor: "agent" };
388622
388737
  case "package-done":
@@ -388626,16 +388741,19 @@ var classifyHead = (subject, flags) => {
388626
388741
  case "review-feedback":
388627
388742
  return { kind: "rest", state: "grilling", actor: "agent" };
388628
388743
  case "done":
388629
- return flags.squashEnabled && flags.hasSquashBase ? {
388630
- kind: "mid-chain",
388631
- state: "done",
388632
- actor: "agent",
388633
- action: { kind: "writeSquashTemplate" }
388634
- } : { kind: "rest", state: "idle", actor: "human" };
388744
+ return nextAfterReviewOrLearning(flags, "done", false);
388635
388745
  case "squash-template":
388636
388746
  return { kind: "rest", state: "squashing", actor: "agent" };
388637
388747
  case "reviewing":
388638
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);
388639
388757
  case "health-check":
388640
388758
  return flags.errorsPresent ? { kind: "rest", state: "escalate", actor: "human" } : { kind: "rest", state: "health-fixing", actor: "agent" };
388641
388759
  case "health-fix":
@@ -388653,18 +388771,24 @@ var STATE_IS_OWN_GATE = /* @__PURE__ */ new Set([
388653
388771
  "review",
388654
388772
  "squashing",
388655
388773
  "health-fixing",
388656
- "escalate"
388774
+ "escalate",
388775
+ "learning",
388776
+ "learning-apply"
388657
388777
  ]);
388658
- 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";
388659
388782
  var INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
388660
388783
  "grilling",
388661
388784
  "grilled",
388662
388785
  "building",
388663
388786
  "fixing",
388664
388787
  "agentic-review",
388665
- "review"
388788
+ "review",
388789
+ "learning-apply"
388666
388790
  ]);
388667
- 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);
388668
388792
  var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388669
388793
  const headIsFixerTurn = (() => {
388670
388794
  const parsed = parseSubject(head7);
@@ -388693,7 +388817,6 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388693
388817
  action: { kind: "closePackage" }
388694
388818
  } : { kind: "rest", state: "fixing", actor: "agent" };
388695
388819
  }
388696
- const squashAfterGreen = p4.squashEnabled && counters.healthFixCount > 0 && p4.healthFixBase !== void 0;
388697
388820
  const reviewSubstantive = p4.headTurnReviewSubstantive !== void 0 ? p4.headTurnReviewSubstantive : p4.reviewDirty && !p4.reviewCheckboxOnly;
388698
388821
  const flags = {
388699
388822
  headTurnIsEmpty: p4.headTurnIsEmpty,
@@ -388701,7 +388824,8 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388701
388824
  agenticReviewForceApproved: forceApprove,
388702
388825
  squashEnabled: p4.squashEnabled,
388703
388826
  hasSquashBase: p4.squashBase !== void 0,
388704
- squashAfterGreen,
388827
+ learningEnabled: p4.learningEnabled,
388828
+ learningMsgIsTemplate: p4.learningMsgIsTemplate,
388705
388829
  reviewSubstantive,
388706
388830
  // A pending (uncommitted) deletion of ERRORS.md still counts as "ERRORS.md
388707
388831
  // was committed at this HEAD" for classification purposes: `fs.exists`
@@ -388739,7 +388863,7 @@ var resolveBaseline = (p4, counters, head7, corrupt, lastTurn) => {
388739
388863
  kind: "runHealthCheck",
388740
388864
  errorCount: counters.healthFixCount,
388741
388865
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388742
- squashAfterGreen: classified.action.squashAfterGreen
388866
+ chainAfterGreen: classified.action.chainAfterGreen
388743
388867
  }
388744
388868
  };
388745
388869
  }
@@ -388808,7 +388932,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388808
388932
  }
388809
388933
  const healthCheckCapReached = head7 === "gtd: health-check" && counters.healthFixCount >= p4.fixAttemptCap;
388810
388934
  if (head7 === "gtd: health-fix" && baseline.state === "idle" || healthCheckCapReached && baseline.state === "health-fixing") {
388811
- 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;
388812
388936
  return {
388813
388937
  state: "idle",
388814
388938
  actor: "agent",
@@ -388817,7 +388941,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388817
388941
  kind: "runHealthCheck",
388818
388942
  errorCount: counters.healthFixCount,
388819
388943
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388820
- squashAfterGreen: squashAfterGreenAtHealthFix
388944
+ chainAfterGreen: chainAfterGreenAtHealthFix
388821
388945
  },
388822
388946
  context: context10
388823
388947
  };
@@ -388832,7 +388956,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388832
388956
  };
388833
388957
  }
388834
388958
  if (baseline.state === "idle" && invoker === "human") {
388835
- 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;
388836
388960
  return {
388837
388961
  state: "idle",
388838
388962
  actor: "human",
@@ -388841,7 +388965,7 @@ var applyTurnTaking = (p4, counters, head7, baseline) => {
388841
388965
  kind: "runHealthCheck",
388842
388966
  errorCount: counters.healthFixCount,
388843
388967
  capReached: counters.healthFixCount >= p4.fixAttemptCap,
388844
- squashAfterGreen
388968
+ chainAfterGreen
388845
388969
  },
388846
388970
  context: context10
388847
388971
  };
@@ -389416,6 +389540,15 @@ var await_review_default = '<%~ include("@header") %>\n\n`.gtd/REVIEW.md` holds
389416
389540
  // src/prompts/squashing.md
389417
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';
389418
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
+
389419
389552
  // src/prompts/escalate.md
389420
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';
389421
389554
 
@@ -389432,6 +389565,9 @@ var PROMPT_STATES = /* @__PURE__ */ new Set([
389432
389565
  "review",
389433
389566
  "await-review",
389434
389567
  "squashing",
389568
+ "learning",
389569
+ "await-learning-review",
389570
+ "learning-apply",
389435
389571
  "escalate",
389436
389572
  "idle",
389437
389573
  "health-fixing"
@@ -389445,7 +389581,9 @@ var MODEL_STATE = {
389445
389581
  "health-fixing": "fixing",
389446
389582
  "agentic-review": "agentic-review",
389447
389583
  review: "clean",
389448
- squashing: "clean"
389584
+ squashing: "clean",
389585
+ learning: "clean",
389586
+ "learning-apply": "clean"
389449
389587
  };
389450
389588
  var builtinResolveModel = (state) => builtinTierDefault[stateTier[state]];
389451
389589
  var fenceFor = (content) => {
@@ -389469,6 +389607,9 @@ eta.loadTemplate("@agentic-review", agentic_review_default);
389469
389607
  eta.loadTemplate("@review", review_default);
389470
389608
  eta.loadTemplate("@await-review", await_review_default);
389471
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);
389472
389613
  eta.loadTemplate("@escalate", escalate_default);
389473
389614
  eta.loadTemplate("@idle", idle_default);
389474
389615
  eta.readFile = null;
@@ -389481,6 +389622,9 @@ var STATE_TEMPLATE = {
389481
389622
  review: "@review",
389482
389623
  "await-review": "@await-review",
389483
389624
  squashing: "@squashing",
389625
+ learning: "@learning",
389626
+ "await-learning-review": "@await-learning-review",
389627
+ "learning-apply": "@learning-apply",
389484
389628
  escalate: "@escalate",
389485
389629
  idle: "@idle",
389486
389630
  "health-fixing": "@health-fixing"
@@ -389592,6 +389736,7 @@ var edgeActionHandlers = {
389592
389736
  if (a5.removeReview) removed.push(".gtd/REVIEW.md");
389593
389737
  if (a5.removeFeedback) removed.push(".gtd/FEEDBACK.md");
389594
389738
  if (a5.removeHealth) removed.push(".gtd/HEALTH.md");
389739
+ if (a5.removeLearning) removed.push(".gtd/LEARNINGS.md");
389595
389740
  if (removed.length > 0) msg += ` (removing ${removed.join(", ")})`;
389596
389741
  return msg;
389597
389742
  },
@@ -389599,7 +389744,8 @@ var edgeActionHandlers = {
389599
389744
  closePackage: () => "close the active package",
389600
389745
  writeSquashTemplate: () => "write the squash message template",
389601
389746
  squashCommit: (a5) => `squash the cycle onto ${a5.squashBase}`,
389602
- 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" : ""})`
389603
389749
  };
389604
389750
  var describeEdgeAction = (a5) => edgeActionHandlers[a5.kind](a5);
389605
389751
  var describeStatus = (prediction) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "2.2.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",