@pmelab/gtd 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/gtd-loop CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- # Drives the two-beat protocol (README.md "The reference loop driver") to a
4
+ # Drives the two-beat protocol (docs/loop.md "The reference loop driver") to a
5
5
  # human gate. Both mutators are attempted every iteration, refusal-tolerant:
6
6
  # whichever actor is actually awaited claims the turn (captures the human's
7
7
  # uncommitted edit after a halt, or the agent's own edit from the previous
@@ -189138,6 +189138,7 @@ const DEFAULT_TEST_COMMAND = "npm run test";
189138
189138
  const DEFAULT_AGENTIC_REVIEW = true;
189139
189139
  const DEFAULT_SQUASH = true;
189140
189140
  const DEFAULT_LEARNING = true;
189141
+ const DEFAULT_DECISION_LOG = true;
189141
189142
  const DEFAULT_FIX_ATTEMPT_CAP = 3;
189142
189143
  const DEFAULT_REVIEW_THRESHOLD = 3;
189143
189144
  const ModelStatesSchema = Struct({
@@ -189160,6 +189161,7 @@ const ConfigSchema = Struct({
189160
189161
  agenticReview: optional(Boolean$),
189161
189162
  squash: optional(Boolean$),
189162
189163
  learning: optional(Boolean$),
189164
+ decisionLog: optional(Boolean$),
189163
189165
  fixAttemptCap: optional(Int.pipe(greaterThanOrEqualTo(0))),
189164
189166
  reviewThreshold: optional(Int.pipe(greaterThanOrEqualTo(1)))
189165
189167
  });
@@ -189300,6 +189302,7 @@ const toOperations = (decoded) => {
189300
189302
  agenticReview: decoded.agenticReview ?? DEFAULT_AGENTIC_REVIEW,
189301
189303
  squash: decoded.squash ?? DEFAULT_SQUASH,
189302
189304
  learning: decoded.learning ?? DEFAULT_LEARNING,
189305
+ decisionLog: decoded.decisionLog ?? DEFAULT_DECISION_LOG,
189303
189306
  fixAttemptCap: decoded.fixAttemptCap ?? DEFAULT_FIX_ATTEMPT_CAP,
189304
189307
  reviewThreshold: decoded.reviewThreshold ?? DEFAULT_REVIEW_THRESHOLD
189305
189308
  };
@@ -351781,6 +351784,7 @@ const parseReviewDoc = (content) => {
351781
351784
  const GTD_DIR = ".gtd";
351782
351785
  const TODO_FILE = `${GTD_DIR}/TODO.md`;
351783
351786
  const ARCHITECTURE_FILE = `${GTD_DIR}/ARCHITECTURE.md`;
351787
+ const PLAN_FILE = `${GTD_DIR}/PLAN.md`;
351784
351788
  const REVIEW_FILE = `${GTD_DIR}/REVIEW.md`;
351785
351789
  const FEEDBACK_FILE = `${GTD_DIR}/FEEDBACK.md`;
351786
351790
  const ERRORS_FILE = `${GTD_DIR}/ERRORS.md`;
@@ -351792,6 +351796,7 @@ const LEGACY_FEEDBACK_FILE = "FEEDBACK.md";
351792
351796
  const STEERING_FILES = {
351793
351797
  todo: TODO_FILE,
351794
351798
  architecture: ARCHITECTURE_FILE,
351799
+ plan: PLAN_FILE,
351795
351800
  review: REVIEW_FILE,
351796
351801
  feedback: FEEDBACK_FILE,
351797
351802
  errors: ERRORS_FILE,
@@ -351972,6 +351977,25 @@ const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
351972
351977
  * keeps pre-namespaced history classifying identically.
351973
351978
  */
351974
351979
  const touchedFeedback = (touched) => touched.includes(FEEDBACK_FILE) || touched.includes(LEGACY_FEEDBACK_FILE);
351980
+ const DECISIONS_TRAILER_RE = /^Gtd-Decisions:\s*true\s*$/m;
351981
+ const DECISIONS_HEADING = "## Decisions";
351982
+ /**
351983
+ * Extracts one commit message's `## Decisions` section verbatim (heading
351984
+ * through the next `#`/`##` heading or end of message, trailer line
351985
+ * excluded), or `undefined` when the trailer is absent or the heading can't
351986
+ * be found despite it (a malformed historical commit — skipped, not an
351987
+ * error, since this reads commits the current turn doesn't own and can't be
351988
+ * asked to fix). Total, never throws.
351989
+ */
351990
+ const extractDecisionsSection = (message) => {
351991
+ if (!DECISIONS_TRAILER_RE.test(message)) return void 0;
351992
+ const lines = message.split(/\r?\n/);
351993
+ const headingIdx = lines.findIndex((line) => line.trim() === DECISIONS_HEADING);
351994
+ if (headingIdx === -1) return void 0;
351995
+ const endIdx = lines.findIndex((line, i) => i > headingIdx && /^#{1,2}\s+\S/.test(line.trim()));
351996
+ const section = lines.slice(headingIdx, endIdx === -1 ? lines.length : endIdx).filter((line) => !DECISIONS_TRAILER_RE.test(line)).join("\n").trim();
351997
+ return section.length > 0 ? section : void 0;
351998
+ };
351975
351999
  /**
351976
352000
  * Gather ALL git/filesystem facts and produce the typed event stream the pure
351977
352001
  * machine folds: one `COMMIT` per first-parent commit (oldest→newest) followed
@@ -351992,18 +352016,21 @@ const gatherEvents = (invoker) => gen(function* () {
351992
352016
  const parsed = parseSubject(subjectOf$1(commit.message));
351993
352017
  const isTurn = parsed.kind === "turn";
351994
352018
  const isRouting = parsed.kind === "routing";
352019
+ const routingPhase = parsed.kind === "routing" ? parsed.phase : "";
352020
+ const turnGate = parsed.kind === "turn" ? parsed.gate : "";
351995
352021
  return {
351996
352022
  type: "COMMIT",
351997
- ...isTurn ? {
352023
+ ...parsed.kind === "turn" ? {
351998
352024
  turnActor: parsed.actor,
351999
352025
  turnGate: parsed.gate
352000
352026
  } : {},
352001
- isErrors: isRouting && parsed.phase === "errors",
352002
- isFeedback: isTurn && parsed.gate === "agentic-review" && touchedFeedback(commit.touched),
352003
- isPackageStart: isRouting && (parsed.phase === "planning" || parsed.phase === "package-done"),
352027
+ isErrors: routingPhase === "errors",
352028
+ isFeedback: turnGate === "agentic-review" && touchedFeedback(commit.touched),
352029
+ isPackageStart: routingPhase === "planning" || routingPhase === "package-done",
352004
352030
  isWorkflowCommit: isTurn || isRouting,
352005
352031
  removedErrors: commit.removedErrors,
352006
- isHealthCheck: isRouting && parsed.phase === "health-check"
352032
+ isHealthCheck: routingPhase === "health-check",
352033
+ isTestsGreen: routingPhase === "tests-green"
352007
352034
  };
352008
352035
  });
352009
352036
  const hasCommits = yield* git.hasCommits();
@@ -352036,6 +352063,7 @@ const gatherEvents = (invoker) => gen(function* () {
352036
352063
  const codeDirty = entries.some((e) => !isGtdPath(e.path));
352037
352064
  const todoExists = yield* fs.exists(resolve(TODO_FILE));
352038
352065
  const architectureExists = yield* fs.exists(resolve(ARCHITECTURE_FILE));
352066
+ const planExists = yield* fs.exists(resolve(PLAN_FILE));
352039
352067
  const reviewPresent = yield* fs.exists(resolve(REVIEW_FILE));
352040
352068
  const feedbackPresent = yield* fs.exists(resolve(FEEDBACK_FILE));
352041
352069
  const errorsPresent = yield* fs.exists(resolve(ERRORS_FILE));
@@ -352051,6 +352079,7 @@ const gatherEvents = (invoker) => gen(function* () {
352051
352079
  const reviewDirty = reviewTrackedAtHead && !workingTreeClean;
352052
352080
  const todoCommitted = todoExists && !isUncommitted(TODO_FILE);
352053
352081
  const architectureCommitted = architectureExists && !isUncommitted(ARCHITECTURE_FILE);
352082
+ const planCommitted = planExists && !isUncommitted(PLAN_FILE);
352054
352083
  const grillingDocPath = todoExists ? TODO_FILE : architectureExists ? ARCHITECTURE_FILE : void 0;
352055
352084
  const grillingDocErrors = grillingDocPath ? parseOpenQuestions(yield* fs.readFileString(resolve(grillingDocPath))).errors : [];
352056
352085
  const reviewDocErrors = reviewPresent ? parseReviewDoc(yield* fs.readFileString(resolve(REVIEW_FILE))).errors : [];
@@ -352062,8 +352091,10 @@ const gatherEvents = (invoker) => gen(function* () {
352062
352091
  let refDiff;
352063
352092
  let reviewAnchor;
352064
352093
  let hasCommitsAfterLastDone = true;
352094
+ let decisionLog = "";
352065
352095
  if (hasCommits) {
352066
352096
  const allHistory = isNone(base) ? history : yield* git.commitHistory();
352097
+ if (config.decisionLog) decisionLog = allHistory.map((c) => extractDecisionsSection(c.message)).filter((section) => section !== void 0).join("\n\n");
352067
352098
  const lastDoneIdx = (() => {
352068
352099
  let idx = -1;
352069
352100
  for (let i = 0; i < allHistory.length; i++) if (subjectOf$1(allHistory[i].message) === DONE_SUBJECT) idx = i;
@@ -352077,7 +352108,7 @@ const gatherEvents = (invoker) => gen(function* () {
352077
352108
  }
352078
352109
  const isGrillingTurn = (message) => {
352079
352110
  const parsed = parseSubject(subjectOf$1(message));
352080
- return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting");
352111
+ return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting" || parsed.gate === "grilled");
352081
352112
  };
352082
352113
  const firstGrilling = currentCycle.find((c) => isGrillingTurn(c.message));
352083
352114
  const lastAwaitingReview = (() => {
@@ -352117,7 +352148,7 @@ const gatherEvents = (invoker) => gen(function* () {
352117
352148
  const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
352118
352149
  const isGrillingTurnSubject = (subject) => {
352119
352150
  const parsed = parseSubject(subject);
352120
- return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting");
352151
+ return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting" || parsed.gate === "grilled");
352121
352152
  };
352122
352153
  const isReviewingAnchor = (subject) => {
352123
352154
  const parsed = parseSubject(subject);
@@ -352152,28 +352183,23 @@ const gatherEvents = (invoker) => gen(function* () {
352152
352183
  const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
352153
352184
  let healthFixBase;
352154
352185
  if (config.squash || config.learning) {
352155
- let firstHealthCheckHash;
352156
- let healthCheckCount = 0;
352157
- for (const commit of commitEvents) {
352158
- if (commit.isPackageStart || commit.removedErrors) {
352159
- firstHealthCheckHash = void 0;
352160
- healthCheckCount = 0;
352161
- }
352162
- if (commit.isHealthCheck) healthCheckCount++;
352163
- }
352164
- if (healthCheckCount > 0) {
352165
- let resetAt = -1;
352166
- for (let i = 0; i < history.length; i++) {
352167
- const c = commitEvents[i];
352168
- if (c.isPackageStart || c.removedErrors) resetAt = i;
352169
- }
352170
- for (let i = resetAt + 1; i < history.length; i++) if (commitEvents[i].isHealthCheck) {
352171
- firstHealthCheckHash = history[i].hash;
352172
- break;
352186
+ const inHealthProcessingChain = headParsedForSquash.kind === "routing" && (headParsedForSquash.phase === "tests-green" || headParsedForSquash.phase === "learning-template" || headParsedForSquash.phase === "learning-drafted" || headParsedForSquash.phase === "learning-approved" || headParsedForSquash.phase === "squash-template" || headParsedForSquash.phase === "learning-applied" && config.squash) || headParsedForSquash.kind === "turn" && SQUASH_OR_LEARNING_TURN_GATES.has(headParsedForSquash.gate);
352187
+ let lastTestsGreenIdx = -1;
352188
+ for (let i = 0; i < commitEvents.length; i++) if (commitEvents[i].isTestsGreen) lastTestsGreenIdx = i;
352189
+ const ignoredTestsGreenIdx = inHealthProcessingChain ? lastTestsGreenIdx : -1;
352190
+ const isHealthEntryTurn = (c) => c.turnActor === "human" && c.turnGate === "health-fixing";
352191
+ let anchorIdx = -1;
352192
+ for (let i = 0; i < commitEvents.length; i++) {
352193
+ const c = commitEvents[i];
352194
+ if (c.isPackageStart || c.removedErrors || c.isTestsGreen && i !== ignoredTestsGreenIdx) {
352195
+ anchorIdx = -1;
352196
+ continue;
352173
352197
  }
352198
+ if (anchorIdx === -1 && (c.isHealthCheck || isHealthEntryTurn(c))) anchorIdx = i;
352174
352199
  }
352175
- if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
352176
- const healthBase = yield* git.resolveRef(`${firstHealthCheckHash}~1`).pipe(catchAll(() => succeed$3(EMPTY_TREE)));
352200
+ if (anchorIdx !== -1) {
352201
+ const anchorHash = history[anchorIdx].hash;
352202
+ const healthBase = yield* git.resolveRef(`${anchorHash}~1`).pipe(catchAll(() => succeed$3(EMPTY_TREE)));
352177
352203
  healthFixBase = healthBase;
352178
352204
  const healthCandidateDiff = yield* git.diffRef(healthBase).pipe(catchAll(() => succeed$3("")));
352179
352205
  if (healthCandidateDiff.trim().length > 0) {
@@ -352193,6 +352219,8 @@ const gatherEvents = (invoker) => gen(function* () {
352193
352219
  todoCommitted,
352194
352220
  architectureExists,
352195
352221
  architectureCommitted,
352222
+ planExists,
352223
+ planCommitted,
352196
352224
  packagesPresent: packages.length > 0,
352197
352225
  reviewPresent,
352198
352226
  feedbackPresent,
@@ -352230,7 +352258,8 @@ const gatherEvents = (invoker) => gen(function* () {
352230
352258
  ...healthFixBase !== void 0 ? { healthFixBase } : {},
352231
352259
  learningEnabled: config.learning,
352232
352260
  learningMsgPresent,
352233
- learningMsgIsTemplate
352261
+ learningMsgIsTemplate,
352262
+ decisionLog
352234
352263
  }
352235
352264
  };
352236
352265
  return [...commitEvents, resolveEvent];
@@ -352324,6 +352353,13 @@ const SQUASH_TEMPLATE = [
352324
352353
  */
352325
352354
  const ARCHITECTURE_SEED_BANNER = "<!-- gtd: seeded from the converged product plan (.gtd/TODO.md); decide the technical/architectural questions below. -->\n\n";
352326
352355
  /**
352356
+ * Banner prepended to `.gtd/ARCHITECTURE.md` when it is seeded from a
352357
+ * hand-written `.gtd/PLAN.md` (the direct-to-decompose entry) — same
352358
+ * orientation role as `ARCHITECTURE_SEED_BANNER`, one phase later: the plan
352359
+ * is final, the next turn decomposes it.
352360
+ */
352361
+ const ARCHITECTURE_PLAN_SEED_BANNER = "<!-- gtd: seeded from the final plan (.gtd/PLAN.md); decompose into ordered work packages. -->\n\n";
352362
+ /**
352327
352363
  * A short skeleton written by `writeLearningTemplate`, instructing the
352328
352364
  * learning agent to replace it with the real distilled learnings.
352329
352365
  */
@@ -352354,8 +352390,11 @@ const perform = (action) => gen(function* () {
352354
352390
  const ensureGtdDir = fs.makeDirectory(resolve(GTD_DIR), { recursive: true }).pipe(catchAll(() => _void));
352355
352391
  switch (action.kind) {
352356
352392
  case "captureTurn":
352357
- if (yield* fs.exists(resolve(TODO_FILE))) yield* formatFile(resolve(TODO_FILE)).pipe(catchAll(() => _void));
352358
- if (yield* fs.exists(resolve(ARCHITECTURE_FILE))) yield* formatFile(resolve(ARCHITECTURE_FILE)).pipe(catchAll(() => _void));
352393
+ for (const file of [
352394
+ TODO_FILE,
352395
+ ARCHITECTURE_FILE,
352396
+ PLAN_FILE
352397
+ ]) if (yield* fs.exists(resolve(file))) yield* formatFile(resolve(file)).pipe(catchAll(() => _void));
352359
352398
  yield* git.commitAllWithPrefix(turnSubject(action.actor, action.gate));
352360
352399
  return { stop: false };
352361
352400
  case "commitRouting":
@@ -352365,6 +352404,12 @@ const perform = (action) => gen(function* () {
352365
352404
  yield* fs.writeFileString(resolve(ARCHITECTURE_FILE), ARCHITECTURE_SEED_BANNER + todoContent);
352366
352405
  yield* fs.remove(resolve(TODO_FILE)).pipe(catchAll(() => _void));
352367
352406
  }
352407
+ if (action.seedArchitectureFromPlan === true) {
352408
+ const planContent = yield* fs.readFileString(resolve(PLAN_FILE)).pipe(catchAll(() => succeed$3("")));
352409
+ yield* ensureGtdDir;
352410
+ yield* fs.writeFileString(resolve(ARCHITECTURE_FILE), ARCHITECTURE_PLAN_SEED_BANNER + planContent);
352411
+ yield* fs.remove(resolve(PLAN_FILE)).pipe(catchAll(() => _void));
352412
+ }
352368
352413
  if (action.removeArchitecture === true) yield* fs.remove(resolve(ARCHITECTURE_FILE)).pipe(catchAll(() => _void));
352369
352414
  if (action.removeReview === true) yield* fs.remove(resolve(REVIEW_FILE)).pipe(catchAll(() => _void));
352370
352415
  if (action.removeFeedback === true) yield* fs.remove(resolve(FEEDBACK_FILE)).pipe(catchAll(() => _void));
@@ -352456,7 +352501,7 @@ const counterRules = {
352456
352501
  incrementsOn: (e) => e.isFeedback
352457
352502
  },
352458
352503
  healthFixCount: {
352459
- resetsOn: (e) => e.isPackageStart || e.removedErrors,
352504
+ resetsOn: (e) => e.isPackageStart || e.removedErrors || e.isTestsGreen,
352460
352505
  incrementsOn: (e) => e.isHealthCheck
352461
352506
  }
352462
352507
  };
@@ -352508,6 +352553,8 @@ const DEFAULT_PAYLOAD = {
352508
352553
  todoCommitted: false,
352509
352554
  architectureExists: false,
352510
352555
  architectureCommitted: false,
352556
+ planExists: false,
352557
+ planCommitted: false,
352511
352558
  packagesPresent: false,
352512
352559
  reviewPresent: false,
352513
352560
  feedbackPresent: false,
@@ -352537,7 +352584,8 @@ const DEFAULT_PAYLOAD = {
352537
352584
  healthCommitted: false,
352538
352585
  learningEnabled: false,
352539
352586
  learningMsgPresent: false,
352540
- learningMsgIsTemplate: false
352587
+ learningMsgIsTemplate: false,
352588
+ decisionLog: ""
352541
352589
  };
352542
352590
  /** Build the prompt context from the payload passthrough + the folded counters. */
352543
352591
  const buildContext = (p, counters) => ({
@@ -352549,7 +352597,8 @@ const buildContext = (p, counters) => ({
352549
352597
  ...p.squashBase !== void 0 ? { squashBase: p.squashBase } : {},
352550
352598
  ...p.squashDiff !== void 0 ? { squashDiff: p.squashDiff } : {},
352551
352599
  feedbackContent: p.feedbackContent !== "" ? p.feedbackContent : p.healthContent,
352552
- ...p.headTurnDiff !== "" ? { turnDiff: p.headTurnDiff } : {}
352600
+ ...p.headTurnDiff !== "" ? { turnDiff: p.headTurnDiff } : {},
352601
+ decisionLog: p.decisionLog
352553
352602
  });
352554
352603
  const healthFileConflictRules = [
352555
352604
  {
@@ -352567,6 +352616,52 @@ const healthFileConflictRules = [
352567
352616
  {
352568
352617
  isViolated: (p) => p.healthPresent && p.errorsPresent,
352569
352618
  message: ".gtd/HEALTH.md + .gtd/ERRORS.md"
352619
+ },
352620
+ {
352621
+ isViolated: (p) => p.healthPresent && p.todoExists,
352622
+ message: ".gtd/HEALTH.md + .gtd/TODO.md"
352623
+ },
352624
+ {
352625
+ isViolated: (p) => p.healthPresent && p.architectureExists,
352626
+ message: ".gtd/HEALTH.md + .gtd/ARCHITECTURE.md"
352627
+ },
352628
+ {
352629
+ isViolated: (p) => p.healthPresent && p.planExists,
352630
+ message: ".gtd/HEALTH.md + .gtd/PLAN.md"
352631
+ }
352632
+ ];
352633
+ const planFileConflictRules = [
352634
+ {
352635
+ isViolated: (p) => p.planExists && p.todoExists,
352636
+ message: ".gtd/PLAN.md + .gtd/TODO.md"
352637
+ },
352638
+ {
352639
+ isViolated: (p) => p.planExists && p.architectureExists,
352640
+ message: ".gtd/PLAN.md + .gtd/ARCHITECTURE.md"
352641
+ },
352642
+ {
352643
+ isViolated: (p) => p.planExists && p.packagesPresent,
352644
+ message: ".gtd/PLAN.md + packages"
352645
+ },
352646
+ {
352647
+ isViolated: (p) => p.planExists && p.reviewPresent,
352648
+ message: ".gtd/PLAN.md + .gtd/REVIEW.md"
352649
+ },
352650
+ {
352651
+ isViolated: (p) => p.planExists && p.feedbackPresent,
352652
+ message: ".gtd/PLAN.md + .gtd/FEEDBACK.md"
352653
+ },
352654
+ {
352655
+ isViolated: (p) => p.planExists && p.errorsPresent,
352656
+ message: ".gtd/PLAN.md + .gtd/ERRORS.md"
352657
+ },
352658
+ {
352659
+ isViolated: (p) => p.planExists && p.squashMsgPresent,
352660
+ message: ".gtd/PLAN.md + .gtd/SQUASH_MSG.md"
352661
+ },
352662
+ {
352663
+ isViolated: (p) => p.planExists && p.learningMsgPresent,
352664
+ message: ".gtd/PLAN.md + .gtd/LEARNINGS.md"
352570
352665
  }
352571
352666
  ];
352572
352667
  const learningFileConflictRules = [
@@ -352662,6 +352757,7 @@ const assertLegal = (p) => {
352662
352757
  for (const rule of [
352663
352758
  ...healthFileConflictRules,
352664
352759
  ...learningFileConflictRules,
352760
+ ...planFileConflictRules,
352665
352761
  ...reviewAndFeedbackRules
352666
352762
  ]) if (rule.isViolated(p)) throw new GtdStateError("illegal-combination", `illegal combination: ${rule.message}`);
352667
352763
  };
@@ -352791,6 +352887,16 @@ const classifyHead = (subject, flags) => {
352791
352887
  actor: "agent"
352792
352888
  };
352793
352889
  }
352890
+ if (actor === "human" && gate === "grilled") return flags.planExists ? {
352891
+ kind: "mid-chain",
352892
+ state: "grilled",
352893
+ actor: "agent",
352894
+ action: {
352895
+ kind: "commitRouting",
352896
+ subject: "gtd: grilled",
352897
+ seedArchitectureFromPlan: true
352898
+ }
352899
+ } : null;
352794
352900
  if (actor === "agent" && gate === "grilled") return flags.hasPackages ? {
352795
352901
  kind: "mid-chain",
352796
352902
  state: "grilled",
@@ -353037,7 +353143,11 @@ const gateForState = (state) => STATE_IS_OWN_GATE.has(state) ? state : GATE_OVER
353037
353143
  * (CLAUDE.md/AGENTS.md/docs), so it's unconditionally inert on a clean tree,
353038
353144
  * same as `building`/`fixing`. `health-fixing` is deliberately absent: its
353039
353145
  * empty turn is meaningful (environmental fix; the machine removes
353040
- * HEALTH.md and re-tests).
353146
+ * HEALTH.md and re-tests) — EXCEPT while HEAD is the human's hand-written
353147
+ * HEALTH.md entry turn (`gtd(human): health-fixing`), where the loop
353148
+ * protocol's opening clean-tree `gtd step-agent` would otherwise consume the
353149
+ * human's error description before any agent read it (see
353150
+ * `isInertEmptyAgentRest`).
353041
353151
  */
353042
353152
  const INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
353043
353153
  "grilling",
@@ -353050,11 +353160,20 @@ const INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
353050
353160
  "learning-apply"
353051
353161
  ]);
353052
353162
  /**
353163
+ * HEAD is the human's HEALTH.md entry turn (`gtd(human): health-fixing`) —
353164
+ * the hand-written error description was just captured and no agent has
353165
+ * acted on it yet.
353166
+ */
353167
+ const isHumanHealthEntryHead = (head) => {
353168
+ const parsed = parseSubject(head);
353169
+ return parsed.kind === "turn" && parsed.actor === "human" && parsed.gate === "health-fixing";
353170
+ };
353171
+ /**
353053
353172
  * True when a `step-agent` invocation at this rest has nothing to capture —
353054
353173
  * see `INERT_EMPTY_AGENT_GATES`. Shared by `applyTurnTaking` (author nothing)
353055
353174
  * and `predictTurn` (predict null) so the two can never disagree.
353056
353175
  */
353057
- const isInertEmptyAgentRest = (state, p) => p.workingTreeClean && (INERT_EMPTY_AGENT_GATES.has(state) || state === "squashing" && p.squashMsgIsTemplate || state === "learning" && p.learningMsgIsTemplate);
353176
+ const isInertEmptyAgentRest = (state, p) => p.workingTreeClean && (INERT_EMPTY_AGENT_GATES.has(state) || state === "squashing" && p.squashMsgIsTemplate || state === "learning" && p.learningMsgIsTemplate || state === "health-fixing" && isHumanHealthEntryHead(p.lastCommitSubject));
353058
353177
  /**
353059
353178
  * The baseline decision: classify HEAD, falling through to the payload-driven
353060
353179
  * ladder for the rows `classifyHead` cannot resolve alone (boundary subjects,
@@ -353105,6 +353224,7 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353105
353224
  const flags = {
353106
353225
  headTurnIsEmpty: p.headTurnIsEmpty,
353107
353226
  hasPackages: p.packagesPresent,
353227
+ planExists: p.planExists,
353108
353228
  agenticReviewForceApproved: forceApprove,
353109
353229
  squashEnabled: p.squashEnabled,
353110
353230
  hasSquashBase: p.squashBase !== void 0,
@@ -353171,6 +353291,11 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353171
353291
  state: "architecting",
353172
353292
  actor: "agent"
353173
353293
  };
353294
+ if (p.planExists) return {
353295
+ kind: "rest",
353296
+ state: "grilled",
353297
+ actor: "human"
353298
+ };
353174
353299
  if (p.packagesPresent && p.packages.length > 0 && lastTurn?.actor === "agent" && lastTurn.gate === "building") return {
353175
353300
  kind: "mid-chain",
353176
353301
  state: "building",
@@ -353181,7 +353306,7 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353181
353306
  capReached: false
353182
353307
  }
353183
353308
  };
353184
- if (!p.packagesPresent && !p.reviewPresent && !p.feedbackPresent && !p.errorsPresent && !p.healthPresent && !p.todoExists && !p.architectureExists) return resolveIdleOrHealth(p);
353309
+ if (!p.packagesPresent && !p.reviewPresent && !p.feedbackPresent && !p.errorsPresent && !p.healthPresent && !p.todoExists && !p.architectureExists && !p.planExists) return resolveIdleOrHealth(p);
353185
353310
  return corrupt();
353186
353311
  };
353187
353312
  /** Boundary/idle lifecycle: no steering files, unrecognized or absent workflow HEAD. */
@@ -353206,8 +353331,8 @@ const resolveIdleOrHealth = (p) => {
353206
353331
  const applyTurnTaking = (p, counters, head, baseline) => {
353207
353332
  const context = buildContext(p, counters);
353208
353333
  const invoker = p.invoker;
353209
- if (invoker === "human" && !p.workingTreeClean && !p.todoCommitted && !p.architectureCommitted && !p.packagesPresent && !p.reviewPresent && !p.feedbackPresent && !p.errorsPresent && !p.healthPresent && (parseSubject(head).kind === "boundary" || head === "gtd: done")) {
353210
- const entryGate = p.architectureExists ? "architecting" : "grilling";
353334
+ if (invoker === "human" && !p.workingTreeClean && !p.todoCommitted && !p.architectureCommitted && !p.planCommitted && !p.packagesPresent && !p.reviewPresent && !p.feedbackPresent && !p.errorsPresent && !p.healthCommitted && (parseSubject(head).kind === "boundary" || head === "gtd: done")) {
353335
+ const entryGate = p.healthPresent ? "health-fixing" : p.planExists ? "grilled" : p.architectureExists ? "architecting" : "grilling";
353211
353336
  return {
353212
353337
  state: entryGate,
353213
353338
  actor: "human",
@@ -353244,7 +353369,7 @@ const applyTurnTaking = (p, counters, head, baseline) => {
353244
353369
  };
353245
353370
  const healthCheckCapReached = head === "gtd: health-check" && counters.healthFixCount >= p.fixAttemptCap;
353246
353371
  if (head === "gtd: health-fix" && baseline.state === "idle" || healthCheckCapReached && baseline.state === "health-fixing") {
353247
- const chainAfterGreenAtHealthFix = (p.squashEnabled || p.learningEnabled) && counters.healthFixCount > 0 && p.healthFixBase !== void 0;
353372
+ const chainAfterGreenAtHealthFix = (p.squashEnabled || p.learningEnabled) && p.healthFixBase !== void 0;
353248
353373
  return {
353249
353374
  state: "idle",
353250
353375
  actor: "agent",
@@ -353266,7 +353391,7 @@ const applyTurnTaking = (p, counters, head, baseline) => {
353266
353391
  context
353267
353392
  };
353268
353393
  if (baseline.state === "idle" && invoker === "human") {
353269
- const chainAfterGreen = (p.squashEnabled || p.learningEnabled) && counters.healthFixCount > 0 && p.healthFixBase !== void 0;
353394
+ const chainAfterGreen = (p.squashEnabled || p.learningEnabled) && p.healthFixBase !== void 0;
353270
353395
  return {
353271
353396
  state: "idle",
353272
353397
  actor: "human",
@@ -353374,8 +353499,8 @@ const predictTurn = (events) => {
353374
353499
  state: baseline.state
353375
353500
  };
353376
353501
  }
353377
- if (!payload.workingTreeClean && !payload.todoCommitted && !payload.architectureCommitted && !payload.packagesPresent && !payload.reviewPresent && !payload.feedbackPresent && !payload.errorsPresent && !payload.healthPresent && (parseSubject(head).kind === "boundary" || head === "gtd: done")) {
353378
- const entryGate = payload.architectureExists ? "architecting" : "grilling";
353502
+ if (!payload.workingTreeClean && !payload.todoCommitted && !payload.architectureCommitted && !payload.planCommitted && !payload.packagesPresent && !payload.reviewPresent && !payload.feedbackPresent && !payload.errorsPresent && !payload.healthCommitted && (parseSubject(head).kind === "boundary" || head === "gtd: done")) {
353503
+ const entryGate = payload.healthPresent ? "health-fixing" : payload.planExists ? "grilled" : payload.architectureExists ? "architecting" : "grilling";
353379
353504
  return {
353380
353505
  actor: "human",
353381
353506
  subject: `gtd(human): ${entryGate}`,
@@ -353910,17 +354035,20 @@ var feedback_default = "<%\n const body = it.content.replace(/\\n$/, \"\")\n c
353910
354035
  //#region src/prompts/partials/package.md
353911
354036
  var package_default = "<%\n const lines = [\"\", \"### Package: `\" + it.pkg.name + \"/`\", \"\"]\n for (const task of it.pkg.taskContents) {\n const fence = it.fenceFor(task.content)\n lines.push(\"#### `\" + task.name + \"`\", \"\", fence, task.content.replace(/\\n$/, \"\"), fence, \"\")\n }\n%><%~ lines.join(\"\\n\") %>\n";
353912
354037
  //#endregion
354038
+ //#region src/prompts/partials/decision-log.md
354039
+ var decision_log_default = "<%\n const body = it.decisionLog.replace(/\\n$/, \"\")\n const fence = it.fenceFor(body)\n const out = [\n \"\",\n \"### Prior decisions\",\n \"\",\n \"Recorded in past squash commits, oldest to newest, with no deduplication\" +\n \" — if two entries answer the same question differently, the LATER one\" +\n \" (further down) is the current, authoritative answer. Don't re-ask a\" +\n \" settled question unless the codebase now genuinely contradicts it.\",\n \"\",\n fence,\n body,\n fence,\n \"\",\n ].join(\"\\n\")\n%><%~ out %>\n";
354040
+ //#endregion
353913
354041
  //#region src/prompts/partials/agent-turn.md
353914
354042
  var agent_turn_default = "Finish your turn by running `gtd step-agent`. Then run `gtd next` and follow\nits output — repeat this cycle as long as the output is addressed to you (the\nagent); when it awaits the human, stop and hand off.\n";
353915
354043
  //#endregion
353916
354044
  //#region src/prompts/grilling-agent.md
353917
- var grilling_agent_default = "<%~ include(\"@header\") %>\n\n`.gtd/TODO.md` holds the plan under development. Develop it into a concrete,\nproduct-level plan **in this one turn** — use subagents / internal iteration\nto go as deep as needed; there is no further agent-only round after this one.\n\n**Scope: product and user-facing decisions only.** Do not decide\nimplementation details in this file — file/module structure, data models,\nlibrary or tech-stack choices, and other architecture questions belong to the\nnext phase (`.gtd/ARCHITECTURE.md`) and must not be treated as open questions\nhere.\n\n### Develop the plan\n\nSpawn a **planning-model subagent** using model `<%= it.model %>` to develop the plan.\nThe subagent works entirely by editing `.gtd/TODO.md`:\n\n1. **Explore the codebase before asking anything** — read the relevant files,\n tests, and docs so every question below is one the codebase genuinely\n cannot answer.\n2. **Replace the captured input / seed template with a real product plan** —\n what to build and why, and the user-facing behavior it should have,\n grounded in the codebase. Leave *how* to build it for the next phase.\n3. **Iterate internally** (spawn further subagents, reconsider, cross-check)\n until the plan is as complete as it can get without human input. Do not\n leave this to a future round — there isn't one before the human is asked.\n4. For every remaining open question, add it under a `## Open Questions`\n section near the top of the file, one `### <question>` sub-heading per\n question, whose first body line is `Suggested default: <answer>` — your\n best-guess answer, stated plainly, that the human can accept as-is. This\n structure is enforced: a `###` question with no `Suggested default:` line\n blocks your turn — `gtd step-agent` refuses until it's fixed. Omit the\n `## Open Questions` section entirely once there are none.\n5. Leave `.gtd/TODO.md` **uncommitted** — the human's turn (`gtd step`) reads it\n next.\n<% if (it.context.turnDiff && it.context.turnDiff.trim()) { %>\n<%~ include(\"@diff\", { heading: \"Latest human input (answers / sketch / review feedback)\", diff: it.context.turnDiff, fenceFor: it.fenceFor }) %>\n\nRead the diff above as **feedback, not finished work**:\n\n- Code changes are suggestions — fold them into the plan and re-implement them\n properly, including test coverage, rather than restoring them verbatim.\n- Code comments are positional feedback about the code at that location.\n- Plan-text changes are global feedback on the plan (or the reviewed work) as\n a whole.\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
354045
+ var grilling_agent_default = "<%~ include(\"@header\") %>\n\n`.gtd/TODO.md` holds the plan under development. Develop it into a concrete,\nproduct-level plan **in this one turn** — use subagents / internal iteration\nto go as deep as needed; there is no further agent-only round after this one.\n\n**Scope: product and user-facing decisions only.** Do not decide\nimplementation details in this file — file/module structure, data models,\nlibrary or tech-stack choices, and other architecture questions belong to the\nnext phase (`.gtd/ARCHITECTURE.md`) and must not be treated as open questions\nhere.\n<% if (it.context.decisionLog && it.context.decisionLog.trim()) { %>\n<%~ include(\"@decision-log\", { decisionLog: it.context.decisionLog, fenceFor: it.fenceFor }) %>\n<% } %>\n\n### Develop the plan\n\nSpawn a **planning-model subagent** using model `<%= it.model %>` to develop the plan.\nThe subagent works entirely by editing `.gtd/TODO.md`:\n\n1. **Explore the codebase before asking anything** — read the relevant files,\n tests, and docs so every question below is one the codebase genuinely\n cannot answer.\n2. **Replace the captured input / seed template with a real product plan** —\n what to build and why, and the user-facing behavior it should have,\n grounded in the codebase. Leave *how* to build it for the next phase.\n3. **Iterate internally** (spawn further subagents, reconsider, cross-check)\n until the plan is as complete as it can get without human input. Do not\n leave this to a future round — there isn't one before the human is asked.\n4. For every remaining open question, add it under a `## Open Questions`\n section near the top of the file, one `### <question>` sub-heading per\n question, whose first body line is `Suggested default: <answer>` — your\n best-guess answer, stated plainly, that the human can accept as-is. This\n structure is enforced: a `###` question with no `Suggested default:` line\n blocks your turn — `gtd step-agent` refuses until it's fixed. Omit the\n `## Open Questions` section entirely once there are none.\n5. Leave `.gtd/TODO.md` **uncommitted** — the human's turn (`gtd step`) reads it\n next.\n<% if (it.context.turnDiff && it.context.turnDiff.trim()) { %>\n<%~ include(\"@diff\", { heading: \"Latest human input (answers / sketch / review feedback)\", diff: it.context.turnDiff, fenceFor: it.fenceFor }) %>\n\nRead the diff above as **feedback, not finished work**:\n\n- Code changes are suggestions — fold them into the plan and re-implement them\n properly, including test coverage, rather than restoring them verbatim.\n- Code comments are positional feedback about the code at that location.\n- Plan-text changes are global feedback on the plan (or the reviewed work) as\n a whole.\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353918
354046
  //#endregion
353919
354047
  //#region src/prompts/grilling-answers.md
353920
354048
  var grilling_answers_default = "<%~ include(\"@header\") %>\n\n`.gtd/TODO.md` holds the plan under development, with open questions under a\n`## Open Questions` section — each `### <question>` carrying a suggested\ndefault.\n\nThis is a human gate; there is nothing for the agent to do.\n\nTell the user:\n\n- To answer a question, edit its `### <question>` entry under `## Open\n Questions` in place — replace the `Suggested default: ...` line with\n `Answer: ...` (or annotate it further), then run `gtd step`.\n- To accept **all** suggested defaults as-is, run `gtd step` with **no\n edits** — this converges the plan and moves on to technical architecting.\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353921
354049
  //#endregion
353922
354050
  //#region src/prompts/architecting-agent.md
353923
- var architecting_agent_default = "<%~ include(\"@header\") %>\n\n`.gtd/ARCHITECTURE.md` holds the technical plan under development — seeded\nfrom the converged product plan, or, if this is the very first turn on\nalready-technical input, written directly by the human. Develop it into a concrete,\nimplementation-ready technical plan **in this one turn** — use subagents /\ninternal iteration to go as deep as needed; there is no further agent-only\nround after this one.\n\n**Scope: technical/architectural decisions only.** Decide file/module\nstructure, data models, module boundaries, library or tech-stack choices, and\nerror-handling/concurrency strategy — the *how*, building on the *what*\nalready settled in this file's product content. Do not re-open or re-litigate\nproduct/user-facing decisions from the prior phase; treat them as settled\ncontext.\n\n### Develop the architecture\n\nSpawn a **planning-model subagent** using model `<%= it.model %>` to develop the\narchitecture. The subagent works entirely by editing `.gtd/ARCHITECTURE.md`:\n\n1. **Explore the codebase before asking anything** — read the relevant files,\n tests, and docs so every question below is one the codebase genuinely\n cannot answer.\n2. **Replace the seeded/captured content with a real architecture** — the\n files to change, module/data-model structure, and why, grounded in the\n codebase.\n3. **Iterate internally** (spawn further subagents, reconsider, cross-check)\n until the architecture is as complete as it can get without human input.\n Do not leave this to a future round — there isn't one before the human is\n asked.\n4. For every remaining open question, add it under a `## Open Questions`\n section near the top of the file, one `### <question>` sub-heading per\n question, whose first body line is `Suggested default: <answer>` — your\n best-guess answer, stated plainly, that the human can accept as-is. This\n structure is enforced: a `###` question with no `Suggested default:` line\n blocks your turn — `gtd step-agent` refuses until it's fixed. Omit the\n `## Open Questions` section entirely once there are none.\n5. Leave `.gtd/ARCHITECTURE.md` **uncommitted** — the human's turn (`gtd step`)\n reads it next.\n<% if (it.context.turnDiff && it.context.turnDiff.trim()) { %>\n<%~ include(\"@diff\", { heading: \"Latest human input (answers / sketch / review feedback)\", diff: it.context.turnDiff, fenceFor: it.fenceFor }) %>\n\nRead the diff above as **feedback, not finished work**:\n\n- Code changes are suggestions — fold them into the architecture and\n re-implement them properly, including test coverage, rather than restoring\n them verbatim.\n- Code comments are positional feedback about the code at that location.\n- Plan-text changes are global feedback on the architecture (or the reviewed\n work) as a whole.\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
354051
+ var architecting_agent_default = "<%~ include(\"@header\") %>\n\n`.gtd/ARCHITECTURE.md` holds the technical plan under development — seeded\nfrom the converged product plan, or, if this is the very first turn on\nalready-technical input, written directly by the human. Develop it into a concrete,\nimplementation-ready technical plan **in this one turn** — use subagents /\ninternal iteration to go as deep as needed; there is no further agent-only\nround after this one.\n\n**Scope: technical/architectural decisions only.** Decide file/module\nstructure, data models, module boundaries, library or tech-stack choices, and\nerror-handling/concurrency strategy — the *how*, building on the *what*\nalready settled in this file's product content. Do not re-open or re-litigate\nproduct/user-facing decisions from the prior phase; treat them as settled\ncontext.\n<% if (it.context.decisionLog && it.context.decisionLog.trim()) { %>\n<%~ include(\"@decision-log\", { decisionLog: it.context.decisionLog, fenceFor: it.fenceFor }) %>\n<% } %>\n\n### Develop the architecture\n\nSpawn a **planning-model subagent** using model `<%= it.model %>` to develop the\narchitecture. The subagent works entirely by editing `.gtd/ARCHITECTURE.md`:\n\n1. **Explore the codebase before asking anything** — read the relevant files,\n tests, and docs so every question below is one the codebase genuinely\n cannot answer.\n2. **Replace the seeded/captured content with a real architecture** — the\n files to change, module/data-model structure, and why, grounded in the\n codebase.\n3. **Iterate internally** (spawn further subagents, reconsider, cross-check)\n until the architecture is as complete as it can get without human input.\n Do not leave this to a future round — there isn't one before the human is\n asked.\n4. For every remaining open question, add it under a `## Open Questions`\n section near the top of the file, one `### <question>` sub-heading per\n question, whose first body line is `Suggested default: <answer>` — your\n best-guess answer, stated plainly, that the human can accept as-is. This\n structure is enforced: a `###` question with no `Suggested default:` line\n blocks your turn — `gtd step-agent` refuses until it's fixed. Omit the\n `## Open Questions` section entirely once there are none.\n5. Leave `.gtd/ARCHITECTURE.md` **uncommitted** — the human's turn (`gtd step`)\n reads it next.\n<% if (it.context.turnDiff && it.context.turnDiff.trim()) { %>\n<%~ include(\"@diff\", { heading: \"Latest human input (answers / sketch / review feedback)\", diff: it.context.turnDiff, fenceFor: it.fenceFor }) %>\n\nRead the diff above as **feedback, not finished work**:\n\n- Code changes are suggestions — fold them into the architecture and\n re-implement them properly, including test coverage, rather than restoring\n them verbatim.\n- Code comments are positional feedback about the code at that location.\n- Plan-text changes are global feedback on the architecture (or the reviewed\n work) as a whole.\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353924
354052
  //#endregion
353925
354053
  //#region src/prompts/architecting-answers.md
353926
354054
  var architecting_answers_default = "<%~ include(\"@header\") %>\n\n`.gtd/ARCHITECTURE.md` holds the technical plan under development, with open\nquestions under a `## Open Questions` section — each `### <question>`\ncarrying a suggested default.\n\nThis is a human gate; there is nothing for the agent to do.\n\nTell the user:\n\n- To answer a question, edit its `### <question>` entry under `## Open\n Questions` in place — replace the `Suggested default: ...` line with\n `Answer: ...` (or annotate it further), then run `gtd step`.\n- To accept **all** suggested defaults as-is, run `gtd step` with **no\n edits** — this converges the architecture and moves on to decomposition.\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
@@ -353935,7 +354063,7 @@ var building_default = "<%~ include(\"@header\") %>\n\nBuild the package describ
353935
354063
  var fixing_default = "<%~ include(\"@header\") %>\n\nSpawn a **fix subagent** using model `<%= it.model %>` to apply fixes for the feedback\nbelow:\n\n1. **Work through the Feedback to address section below** — address every item\n it lists. For test output, make the failing tests pass; for review findings,\n satisfy each finding against the package's task specs.\n2. **Make the fix in place** — change the code to resolve the feedback. Keep the\n change focused; do not refactor unrelated code.\n3. **Or dispute the feedback** — if a finding is wrong, empty or delete\n `.gtd/FEEDBACK.md` instead of fixing it. The machine picks the dispute up\n either way.\n4. **Leave every change uncommitted and finish your turn** — do **not** commit\n or stage.\n<% if (it.context.feedbackContent.trim()) { %>\n<%~ include(\"@feedback\", { content: it.context.feedbackContent, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353936
354064
  //#endregion
353937
354065
  //#region src/prompts/health-fixing.md
353938
- var health_fixing_default = "<%~ include(\"@header\") %>\n\nThe idle health check failed — the repository's test suite is red outside any\nwork cycle. Spawn a **fix subagent** using model `<%= it.model %>` to repair\nit:\n\n1. **Work through the failure output below** — make the failing test command\n pass again.\n2. **Make the fix in place** — keep the change focused; do not refactor\n unrelated code.\n3. **If the failure does not reproduce** (or nothing needs changing), change\n nothing and simply finish your turn — the machine re-runs the health check\n either way.\n4. **Never create or edit any file under `.gtd/`** — in particular, do NOT\n write `.gtd/FEEDBACK.md`. The machine removes `.gtd/HEALTH.md` itself when\n your turn is captured.\n5. **Leave every change uncommitted and finish your turn** — do **not** commit\n or stage.\n<% if (it.context.feedbackContent.trim()) { %>\n<%~ include(\"@feedback\", { content: it.context.feedbackContent, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
354066
+ var health_fixing_default = "<%~ include(\"@header\") %>\n\nThe repository's health gate is red outside any work cycle — either the idle\nhealth check failed, or a human hand-wrote `.gtd/HEALTH.md` describing errors\nto fix. Spawn a **fix subagent** using model `<%= it.model %>` to repair it:\n\n1. **Work through the report below** — fix the described errors and make the\n test command pass.\n2. **Make the fix in place** — keep the change focused; do not refactor\n unrelated code.\n3. **If the failure does not reproduce** (or nothing needs changing), change\n nothing and simply finish your turn — the machine re-runs the health check\n either way.\n4. **Never create or edit any file under `.gtd/`** — in particular, do NOT\n write `.gtd/FEEDBACK.md`. The machine removes `.gtd/HEALTH.md` itself when\n your turn is captured.\n5. **Leave every change uncommitted and finish your turn** — do **not** commit\n or stage.\n<% if (it.context.feedbackContent.trim()) { %>\n<%~ include(\"@feedback\", { content: it.context.feedbackContent, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353939
354067
  //#endregion
353940
354068
  //#region src/prompts/agentic-review.md
353941
354069
  var agentic_review_default = "<%~ include(\"@header\") %>\n\nSpawn a **reviewing subagent** using model `<%= it.model %>`. The package's task spec\nfiles and its cumulative diff are found below. The subagent must:\n\n1. **Read the task spec files** — they define the acceptance criteria. Every\n requirement must be checked, with one exception: ignore any spec\n requirement about files under `.gtd/` (or a root-level `TODO.md` /\n `REVIEW.md` / `FEEDBACK.md` acting as workflow state). Those files are\n managed by the workflow machine — created and deleted outside the\n implementation's control — so never report a finding about their\n presence, absence, or content.\n2. **Read the package diff** — examine every hunk to determine whether the\n implementation satisfies the spec.\n3. **Always write `.gtd/FEEDBACK.md`**:\n - **Fully satisfies the spec** → write an **empty** `.gtd/FEEDBACK.md` (zero bytes\n or whitespace only). This empty file is the **approval** signal.\n - **Does not fully satisfy** → write concrete, actionable findings anchored\n to specific file and symbol names, grouped under short headings, so the fix\n agent can act without re-reading the diff.\n4. **Do not edit source files and do not commit** — the reviewer only writes\n `.gtd/FEEDBACK.md`, left uncommitted, then finishes its turn.\n<% if (it.context.packages[0]) { %>\n<%~ include(\"@package\", { pkg: it.context.packages[0], fenceFor: it.fenceFor }) %>\n<% } %><% if (it.context.refDiff && it.context.refDiff.trim()) { %>\n<%~ include(\"@diff\", { heading: \"Package diff\", diff: it.context.refDiff, fenceFor: it.fenceFor }) %>\n<% } %>\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
@@ -353947,7 +354075,7 @@ var review_default = "<%~ include(\"@header\") %>\n\nSpawn a **planning-model su
353947
354075
  var await_review_default = "<%~ include(\"@header\") %>\n\n`.gtd/REVIEW.md` holds the review record for the completed work. This is a human\ngate; there is nothing for the agent to do.\n\nTell the user:\n\n- Read `.gtd/REVIEW.md` (and the underlying work it points at).\n- To **approve**: run `gtd step` with no edits, or only tick checkboxes in\n `.gtd/REVIEW.md` — either way ends the review with an approval.\n- To **request changes**: write substantive edits or annotations into\n `.gtd/REVIEW.md` (or the code) describing what's wrong, then run `gtd step` —\n this sends the feedback back for another round.\n\n<% if (it.tail) { %>\n<%~ include(it.tail) %>\n<% } %>\n";
353948
354076
  //#endregion
353949
354077
  //#region src/prompts/squashing.md
353950
- var squashing_default = "<%~ include(\"@header\") %>\n\nThe process is **approved and done**. `.gtd/SQUASH_MSG.md` has already been\ncommitted with a template — your job is to overwrite it with the real\nconventional-commits squash message and finish your turn.\n\n### Step 1 — 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` and\n`.gtd/ARCHITECTURE.md` — specifically the `## Captured input` sections and any\nedits to plan/architecture text. Extract **key decisions, trade-offs, and\ndesign choices** made during grilling and architecting rounds. These will\nappear in the commit body so the history is self-documenting.\n\n### Step 2 — Draft the commit message\n\nDraft ONE conventional-commits message:\n\n```\ntype(scope): subject\n\nbody (explain the why — motivation, trade-offs, key decisions from grilling)\n```\n\n- **type**: `feat` / `fix` / `refactor` / `chore` / `docs` / `test`\n- **subject**: imperative mood, ≤ 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 — 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 — `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";
354078
+ var squashing_default = "<%~ include(\"@header\") %>\n\nThe process is **approved and done**. `.gtd/SQUASH_MSG.md` has already been\ncommitted with a template — your job is to overwrite it with the real\nconventional-commits squash message and finish your turn.\n\n### Step 1 — 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` and\n`.gtd/ARCHITECTURE.md` — specifically the `## Captured input` sections and any\nedits to plan/architecture text. Extract **key decisions, trade-offs, and\ndesign choices** made during grilling and architecting rounds — specifically\nthe `### <question>` entries under `## Open Questions` that got resolved\n(`Answer:`, or an accepted `Suggested default:`) this cycle.\n\n### Step 2 — Draft the commit message\n\nDraft ONE conventional-commits message:\n\n```\ntype(scope): subject\n\nbody (explain the why — motivation, trade-offs, key decisions from grilling)\n\n## Decisions\n\n### <question, verbatim from Step 1>\nAnswer: <the resolved answer> (default accepted | human override: <reason>)\n\nGtd-Decisions: true\n```\n\n- **type**: `feat` / `fix` / `refactor` / `chore` / `docs` / `test`\n- **subject**: imperative mood, ≤ 72 characters, lowercase after the colon\n- **body**: a brief motivation/trade-off summary.\n- **`## Decisions`**: one block per question resolved THIS cycle (from Step\n 1) — omit the whole section (and the `Gtd-Decisions: true` line) entirely\n when this cycle recorded no decisions. Don't restate decisions from earlier\n cycles — this commit only records what changed now; a later grilling round\n reads the full history of these sections back as context, so repeating old\n entries here would just duplicate them forever. If this cycle's answer\n contradicts an earlier cycle's, don't try to edit or annotate the old one —\n just state the new answer plainly; the newer commit naturally reads as the\n current truth.\n\n### Step 3 — 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 — `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";
353951
354079
  //#endregion
353952
354080
  //#region src/prompts/learning.md
353953
354081
  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 — 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 — 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** — each `gtd: errors` round and the fix that\n followed it. What broke, and what actually made it pass?\n- **Review feedback** — 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** — any `gtd: health-check` / `gtd: health-fix`\n cycles. Environmental or systemic issues surfaced outside normal building?\n- **Grilling decisions** — `.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 — 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 — 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 — 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";
@@ -354034,6 +354162,7 @@ eta.loadTemplate("@header", header_default);
354034
354162
  eta.loadTemplate("@diff", diff_default);
354035
354163
  eta.loadTemplate("@feedback", feedback_default);
354036
354164
  eta.loadTemplate("@package", package_default);
354165
+ eta.loadTemplate("@decision-log", decision_log_default);
354037
354166
  eta.loadTemplate("@agent-turn", agent_turn_default);
354038
354167
  eta.loadTemplate("@grilling-agent", grilling_agent_default);
354039
354168
  eta.loadTemplate("@grilling-answers", grilling_answers_default);
@@ -365108,7 +365237,7 @@ const reviewWindowBase = (history) => {
365108
365237
  const parsed = parseSubject(subjectOf(c.message));
365109
365238
  if (parsed.kind === "routing" && parsed.phase === "reviewing" && parsed.param !== void 0) anchor = parsed.param;
365110
365239
  if (parsed.kind === "routing" && parsed.phase === "awaiting-review") lastAwaitingReview = c.hash;
365111
- if (parsed.kind === "turn" && parsed.gate === "grilling" && firstGrilling === void 0) firstGrilling = c.hash;
365240
+ if (parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting" || parsed.gate === "grilled") && firstGrilling === void 0) firstGrilling = c.hash;
365112
365241
  }
365113
365242
  return anchor ?? lastAwaitingReview ?? firstGrilling;
365114
365243
  };
@@ -365445,6 +365574,7 @@ const edgeActionHandlers = {
365445
365574
  commitRouting: (a) => {
365446
365575
  let msg = `commit routing as "${a.subject}"`;
365447
365576
  if (a.seedArchitectureFromTodo) msg += " (seeding .gtd/ARCHITECTURE.md from .gtd/TODO.md)";
365577
+ if (a.seedArchitectureFromPlan) msg += " (seeding .gtd/ARCHITECTURE.md from .gtd/PLAN.md)";
365448
365578
  const removed = [];
365449
365579
  if (a.removeArchitecture) removed.push(".gtd/ARCHITECTURE.md");
365450
365580
  if (a.removeReview) removed.push(".gtd/REVIEW.md");