@pmelab/gtd 2.7.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
@@ -351784,6 +351784,7 @@ const parseReviewDoc = (content) => {
351784
351784
  const GTD_DIR = ".gtd";
351785
351785
  const TODO_FILE = `${GTD_DIR}/TODO.md`;
351786
351786
  const ARCHITECTURE_FILE = `${GTD_DIR}/ARCHITECTURE.md`;
351787
+ const PLAN_FILE = `${GTD_DIR}/PLAN.md`;
351787
351788
  const REVIEW_FILE = `${GTD_DIR}/REVIEW.md`;
351788
351789
  const FEEDBACK_FILE = `${GTD_DIR}/FEEDBACK.md`;
351789
351790
  const ERRORS_FILE = `${GTD_DIR}/ERRORS.md`;
@@ -351795,6 +351796,7 @@ const LEGACY_FEEDBACK_FILE = "FEEDBACK.md";
351795
351796
  const STEERING_FILES = {
351796
351797
  todo: TODO_FILE,
351797
351798
  architecture: ARCHITECTURE_FILE,
351799
+ plan: PLAN_FILE,
351798
351800
  review: REVIEW_FILE,
351799
351801
  feedback: FEEDBACK_FILE,
351800
351802
  errors: ERRORS_FILE,
@@ -352014,18 +352016,21 @@ const gatherEvents = (invoker) => gen(function* () {
352014
352016
  const parsed = parseSubject(subjectOf$1(commit.message));
352015
352017
  const isTurn = parsed.kind === "turn";
352016
352018
  const isRouting = parsed.kind === "routing";
352019
+ const routingPhase = parsed.kind === "routing" ? parsed.phase : "";
352020
+ const turnGate = parsed.kind === "turn" ? parsed.gate : "";
352017
352021
  return {
352018
352022
  type: "COMMIT",
352019
- ...isTurn ? {
352023
+ ...parsed.kind === "turn" ? {
352020
352024
  turnActor: parsed.actor,
352021
352025
  turnGate: parsed.gate
352022
352026
  } : {},
352023
- isErrors: isRouting && parsed.phase === "errors",
352024
- isFeedback: isTurn && parsed.gate === "agentic-review" && touchedFeedback(commit.touched),
352025
- 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",
352026
352030
  isWorkflowCommit: isTurn || isRouting,
352027
352031
  removedErrors: commit.removedErrors,
352028
- isHealthCheck: isRouting && parsed.phase === "health-check"
352032
+ isHealthCheck: routingPhase === "health-check",
352033
+ isTestsGreen: routingPhase === "tests-green"
352029
352034
  };
352030
352035
  });
352031
352036
  const hasCommits = yield* git.hasCommits();
@@ -352058,6 +352063,7 @@ const gatherEvents = (invoker) => gen(function* () {
352058
352063
  const codeDirty = entries.some((e) => !isGtdPath(e.path));
352059
352064
  const todoExists = yield* fs.exists(resolve(TODO_FILE));
352060
352065
  const architectureExists = yield* fs.exists(resolve(ARCHITECTURE_FILE));
352066
+ const planExists = yield* fs.exists(resolve(PLAN_FILE));
352061
352067
  const reviewPresent = yield* fs.exists(resolve(REVIEW_FILE));
352062
352068
  const feedbackPresent = yield* fs.exists(resolve(FEEDBACK_FILE));
352063
352069
  const errorsPresent = yield* fs.exists(resolve(ERRORS_FILE));
@@ -352073,6 +352079,7 @@ const gatherEvents = (invoker) => gen(function* () {
352073
352079
  const reviewDirty = reviewTrackedAtHead && !workingTreeClean;
352074
352080
  const todoCommitted = todoExists && !isUncommitted(TODO_FILE);
352075
352081
  const architectureCommitted = architectureExists && !isUncommitted(ARCHITECTURE_FILE);
352082
+ const planCommitted = planExists && !isUncommitted(PLAN_FILE);
352076
352083
  const grillingDocPath = todoExists ? TODO_FILE : architectureExists ? ARCHITECTURE_FILE : void 0;
352077
352084
  const grillingDocErrors = grillingDocPath ? parseOpenQuestions(yield* fs.readFileString(resolve(grillingDocPath))).errors : [];
352078
352085
  const reviewDocErrors = reviewPresent ? parseReviewDoc(yield* fs.readFileString(resolve(REVIEW_FILE))).errors : [];
@@ -352101,7 +352108,7 @@ const gatherEvents = (invoker) => gen(function* () {
352101
352108
  }
352102
352109
  const isGrillingTurn = (message) => {
352103
352110
  const parsed = parseSubject(subjectOf$1(message));
352104
- return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting");
352111
+ return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting" || parsed.gate === "grilled");
352105
352112
  };
352106
352113
  const firstGrilling = currentCycle.find((c) => isGrillingTurn(c.message));
352107
352114
  const lastAwaitingReview = (() => {
@@ -352141,7 +352148,7 @@ const gatherEvents = (invoker) => gen(function* () {
352141
352148
  const squashCycle = squashHistory.slice(prevDoneIdx + 1, lastDoneIdxForSquash + 1);
352142
352149
  const isGrillingTurnSubject = (subject) => {
352143
352150
  const parsed = parseSubject(subject);
352144
- return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting");
352151
+ return parsed.kind === "turn" && (parsed.gate === "grilling" || parsed.gate === "architecting" || parsed.gate === "grilled");
352145
352152
  };
352146
352153
  const isReviewingAnchor = (subject) => {
352147
352154
  const parsed = parseSubject(subject);
@@ -352176,28 +352183,23 @@ const gatherEvents = (invoker) => gen(function* () {
352176
352183
  const healthCommitted = healthPresent && !isUncommitted(HEALTH_FILE);
352177
352184
  let healthFixBase;
352178
352185
  if (config.squash || config.learning) {
352179
- let firstHealthCheckHash;
352180
- let healthCheckCount = 0;
352181
- for (const commit of commitEvents) {
352182
- if (commit.isPackageStart || commit.removedErrors) {
352183
- firstHealthCheckHash = void 0;
352184
- healthCheckCount = 0;
352185
- }
352186
- if (commit.isHealthCheck) healthCheckCount++;
352187
- }
352188
- if (healthCheckCount > 0) {
352189
- let resetAt = -1;
352190
- for (let i = 0; i < history.length; i++) {
352191
- const c = commitEvents[i];
352192
- if (c.isPackageStart || c.removedErrors) resetAt = i;
352193
- }
352194
- for (let i = resetAt + 1; i < history.length; i++) if (commitEvents[i].isHealthCheck) {
352195
- firstHealthCheckHash = history[i].hash;
352196
- 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;
352197
352197
  }
352198
+ if (anchorIdx === -1 && (c.isHealthCheck || isHealthEntryTurn(c))) anchorIdx = i;
352198
352199
  }
352199
- if (healthCheckCount > 0 && firstHealthCheckHash !== void 0) {
352200
- 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)));
352201
352203
  healthFixBase = healthBase;
352202
352204
  const healthCandidateDiff = yield* git.diffRef(healthBase).pipe(catchAll(() => succeed$3("")));
352203
352205
  if (healthCandidateDiff.trim().length > 0) {
@@ -352217,6 +352219,8 @@ const gatherEvents = (invoker) => gen(function* () {
352217
352219
  todoCommitted,
352218
352220
  architectureExists,
352219
352221
  architectureCommitted,
352222
+ planExists,
352223
+ planCommitted,
352220
352224
  packagesPresent: packages.length > 0,
352221
352225
  reviewPresent,
352222
352226
  feedbackPresent,
@@ -352349,6 +352353,13 @@ const SQUASH_TEMPLATE = [
352349
352353
  */
352350
352354
  const ARCHITECTURE_SEED_BANNER = "<!-- gtd: seeded from the converged product plan (.gtd/TODO.md); decide the technical/architectural questions below. -->\n\n";
352351
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
+ /**
352352
352363
  * A short skeleton written by `writeLearningTemplate`, instructing the
352353
352364
  * learning agent to replace it with the real distilled learnings.
352354
352365
  */
@@ -352379,8 +352390,11 @@ const perform = (action) => gen(function* () {
352379
352390
  const ensureGtdDir = fs.makeDirectory(resolve(GTD_DIR), { recursive: true }).pipe(catchAll(() => _void));
352380
352391
  switch (action.kind) {
352381
352392
  case "captureTurn":
352382
- if (yield* fs.exists(resolve(TODO_FILE))) yield* formatFile(resolve(TODO_FILE)).pipe(catchAll(() => _void));
352383
- 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));
352384
352398
  yield* git.commitAllWithPrefix(turnSubject(action.actor, action.gate));
352385
352399
  return { stop: false };
352386
352400
  case "commitRouting":
@@ -352390,6 +352404,12 @@ const perform = (action) => gen(function* () {
352390
352404
  yield* fs.writeFileString(resolve(ARCHITECTURE_FILE), ARCHITECTURE_SEED_BANNER + todoContent);
352391
352405
  yield* fs.remove(resolve(TODO_FILE)).pipe(catchAll(() => _void));
352392
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
+ }
352393
352413
  if (action.removeArchitecture === true) yield* fs.remove(resolve(ARCHITECTURE_FILE)).pipe(catchAll(() => _void));
352394
352414
  if (action.removeReview === true) yield* fs.remove(resolve(REVIEW_FILE)).pipe(catchAll(() => _void));
352395
352415
  if (action.removeFeedback === true) yield* fs.remove(resolve(FEEDBACK_FILE)).pipe(catchAll(() => _void));
@@ -352481,7 +352501,7 @@ const counterRules = {
352481
352501
  incrementsOn: (e) => e.isFeedback
352482
352502
  },
352483
352503
  healthFixCount: {
352484
- resetsOn: (e) => e.isPackageStart || e.removedErrors,
352504
+ resetsOn: (e) => e.isPackageStart || e.removedErrors || e.isTestsGreen,
352485
352505
  incrementsOn: (e) => e.isHealthCheck
352486
352506
  }
352487
352507
  };
@@ -352533,6 +352553,8 @@ const DEFAULT_PAYLOAD = {
352533
352553
  todoCommitted: false,
352534
352554
  architectureExists: false,
352535
352555
  architectureCommitted: false,
352556
+ planExists: false,
352557
+ planCommitted: false,
352536
352558
  packagesPresent: false,
352537
352559
  reviewPresent: false,
352538
352560
  feedbackPresent: false,
@@ -352594,6 +352616,52 @@ const healthFileConflictRules = [
352594
352616
  {
352595
352617
  isViolated: (p) => p.healthPresent && p.errorsPresent,
352596
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"
352597
352665
  }
352598
352666
  ];
352599
352667
  const learningFileConflictRules = [
@@ -352689,6 +352757,7 @@ const assertLegal = (p) => {
352689
352757
  for (const rule of [
352690
352758
  ...healthFileConflictRules,
352691
352759
  ...learningFileConflictRules,
352760
+ ...planFileConflictRules,
352692
352761
  ...reviewAndFeedbackRules
352693
352762
  ]) if (rule.isViolated(p)) throw new GtdStateError("illegal-combination", `illegal combination: ${rule.message}`);
352694
352763
  };
@@ -352818,6 +352887,16 @@ const classifyHead = (subject, flags) => {
352818
352887
  actor: "agent"
352819
352888
  };
352820
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;
352821
352900
  if (actor === "agent" && gate === "grilled") return flags.hasPackages ? {
352822
352901
  kind: "mid-chain",
352823
352902
  state: "grilled",
@@ -353064,7 +353143,11 @@ const gateForState = (state) => STATE_IS_OWN_GATE.has(state) ? state : GATE_OVER
353064
353143
  * (CLAUDE.md/AGENTS.md/docs), so it's unconditionally inert on a clean tree,
353065
353144
  * same as `building`/`fixing`. `health-fixing` is deliberately absent: its
353066
353145
  * empty turn is meaningful (environmental fix; the machine removes
353067
- * 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`).
353068
353151
  */
353069
353152
  const INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
353070
353153
  "grilling",
@@ -353077,11 +353160,20 @@ const INERT_EMPTY_AGENT_GATES = /* @__PURE__ */ new Set([
353077
353160
  "learning-apply"
353078
353161
  ]);
353079
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
+ /**
353080
353172
  * True when a `step-agent` invocation at this rest has nothing to capture —
353081
353173
  * see `INERT_EMPTY_AGENT_GATES`. Shared by `applyTurnTaking` (author nothing)
353082
353174
  * and `predictTurn` (predict null) so the two can never disagree.
353083
353175
  */
353084
- 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));
353085
353177
  /**
353086
353178
  * The baseline decision: classify HEAD, falling through to the payload-driven
353087
353179
  * ladder for the rows `classifyHead` cannot resolve alone (boundary subjects,
@@ -353132,6 +353224,7 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353132
353224
  const flags = {
353133
353225
  headTurnIsEmpty: p.headTurnIsEmpty,
353134
353226
  hasPackages: p.packagesPresent,
353227
+ planExists: p.planExists,
353135
353228
  agenticReviewForceApproved: forceApprove,
353136
353229
  squashEnabled: p.squashEnabled,
353137
353230
  hasSquashBase: p.squashBase !== void 0,
@@ -353198,6 +353291,11 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353198
353291
  state: "architecting",
353199
353292
  actor: "agent"
353200
353293
  };
353294
+ if (p.planExists) return {
353295
+ kind: "rest",
353296
+ state: "grilled",
353297
+ actor: "human"
353298
+ };
353201
353299
  if (p.packagesPresent && p.packages.length > 0 && lastTurn?.actor === "agent" && lastTurn.gate === "building") return {
353202
353300
  kind: "mid-chain",
353203
353301
  state: "building",
@@ -353208,7 +353306,7 @@ const resolveBaseline = (p, counters, head, corrupt, lastTurn) => {
353208
353306
  capReached: false
353209
353307
  }
353210
353308
  };
353211
- 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);
353212
353310
  return corrupt();
353213
353311
  };
353214
353312
  /** Boundary/idle lifecycle: no steering files, unrecognized or absent workflow HEAD. */
@@ -353233,8 +353331,8 @@ const resolveIdleOrHealth = (p) => {
353233
353331
  const applyTurnTaking = (p, counters, head, baseline) => {
353234
353332
  const context = buildContext(p, counters);
353235
353333
  const invoker = p.invoker;
353236
- 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")) {
353237
- 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";
353238
353336
  return {
353239
353337
  state: entryGate,
353240
353338
  actor: "human",
@@ -353271,7 +353369,7 @@ const applyTurnTaking = (p, counters, head, baseline) => {
353271
353369
  };
353272
353370
  const healthCheckCapReached = head === "gtd: health-check" && counters.healthFixCount >= p.fixAttemptCap;
353273
353371
  if (head === "gtd: health-fix" && baseline.state === "idle" || healthCheckCapReached && baseline.state === "health-fixing") {
353274
- const chainAfterGreenAtHealthFix = (p.squashEnabled || p.learningEnabled) && counters.healthFixCount > 0 && p.healthFixBase !== void 0;
353372
+ const chainAfterGreenAtHealthFix = (p.squashEnabled || p.learningEnabled) && p.healthFixBase !== void 0;
353275
353373
  return {
353276
353374
  state: "idle",
353277
353375
  actor: "agent",
@@ -353293,7 +353391,7 @@ const applyTurnTaking = (p, counters, head, baseline) => {
353293
353391
  context
353294
353392
  };
353295
353393
  if (baseline.state === "idle" && invoker === "human") {
353296
- const chainAfterGreen = (p.squashEnabled || p.learningEnabled) && counters.healthFixCount > 0 && p.healthFixBase !== void 0;
353394
+ const chainAfterGreen = (p.squashEnabled || p.learningEnabled) && p.healthFixBase !== void 0;
353297
353395
  return {
353298
353396
  state: "idle",
353299
353397
  actor: "human",
@@ -353401,8 +353499,8 @@ const predictTurn = (events) => {
353401
353499
  state: baseline.state
353402
353500
  };
353403
353501
  }
353404
- if (!payload.workingTreeClean && !payload.todoCommitted && !payload.architectureCommitted && !payload.packagesPresent && !payload.reviewPresent && !payload.feedbackPresent && !payload.errorsPresent && !payload.healthPresent && (parseSubject(head).kind === "boundary" || head === "gtd: done")) {
353405
- 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";
353406
353504
  return {
353407
353505
  actor: "human",
353408
353506
  subject: `gtd(human): ${entryGate}`,
@@ -353965,7 +354063,7 @@ var building_default = "<%~ include(\"@header\") %>\n\nBuild the package describ
353965
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";
353966
354064
  //#endregion
353967
354065
  //#region src/prompts/health-fixing.md
353968
- 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";
353969
354067
  //#endregion
353970
354068
  //#region src/prompts/agentic-review.md
353971
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";
@@ -365139,7 +365237,7 @@ const reviewWindowBase = (history) => {
365139
365237
  const parsed = parseSubject(subjectOf(c.message));
365140
365238
  if (parsed.kind === "routing" && parsed.phase === "reviewing" && parsed.param !== void 0) anchor = parsed.param;
365141
365239
  if (parsed.kind === "routing" && parsed.phase === "awaiting-review") lastAwaitingReview = c.hash;
365142
- 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;
365143
365241
  }
365144
365242
  return anchor ?? lastAwaitingReview ?? firstGrilling;
365145
365243
  };
@@ -365476,6 +365574,7 @@ const edgeActionHandlers = {
365476
365574
  commitRouting: (a) => {
365477
365575
  let msg = `commit routing as "${a.subject}"`;
365478
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)";
365479
365578
  const removed = [];
365480
365579
  if (a.removeArchitecture) removed.push(".gtd/ARCHITECTURE.md");
365481
365580
  if (a.removeReview) removed.push(".gtd/REVIEW.md");