@pmelab/gtd 8.6.0 → 9.0.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 +367 -179
- package/dist/gtd.bundle.mjs +1234 -352
- package/package.json +2 -3
- package/bin/gtd +0 -878
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -4,12 +4,13 @@ const require = __createRequire(import.meta.url);
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import * as Crypto from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
8
|
import * as NFS from "node:fs";
|
|
8
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
10
|
import * as OS from "node:os";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import * as Path from "node:path";
|
|
12
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
14
|
import * as ChildProcess from "node:child_process";
|
|
14
15
|
import { spawn } from "node:child_process";
|
|
15
16
|
import * as NodeUrl from "node:url";
|
|
@@ -189824,7 +189825,7 @@ const STATE_FIELDS = {
|
|
|
189824
189825
|
* A STRING is a different shape entirely: an Eta template rendering a
|
|
189825
189826
|
* commitish. Entering that state fixes the WHOLE PROCESS's diff base to the
|
|
189826
189827
|
* rendered value (not a window anchor) — this is how a manual entry (e.g.
|
|
189827
|
-
* `gtd
|
|
189828
|
+
* `gtd --entry review --base <commitish>`) pins what the rest
|
|
189828
189829
|
* of the process diffs against. Rendering the template happens at the edge,
|
|
189829
189830
|
* not here — this module only carries the raw string and, per
|
|
189830
189831
|
* `isReviewBaseState`, a string value is NEVER treated as the `true`/
|
|
@@ -189884,7 +189885,7 @@ const STATE_FIELDS = {
|
|
|
189884
189885
|
},
|
|
189885
189886
|
/**
|
|
189886
189887
|
* Authoring-only: an EXTRA reachability root (`WorkflowEntries.manual`),
|
|
189887
|
-
* enterable via `gtd
|
|
189888
|
+
* enterable via `gtd --entry <this state's qualified name>`.
|
|
189888
189889
|
* Never lands on the compiled `StateDef` — `compileState` validates its
|
|
189889
189890
|
* shape but discards the result; `compileWorkflowConfig` reads the RAW
|
|
189890
189891
|
* `entry: true` flag directly off each qualified state to build
|
|
@@ -189928,8 +189929,8 @@ const validateFieldRules = (name, state, key, spec) => {
|
|
|
189928
189929
|
//#endregion
|
|
189929
189930
|
//#region src/PatternMachine.ts
|
|
189930
189931
|
/**
|
|
189931
|
-
* The
|
|
189932
|
-
*
|
|
189932
|
+
* The pattern machine — gtd's state-machine core. This module is the pure
|
|
189933
|
+
* engine: definition types, the pattern grammar's
|
|
189933
189934
|
* parser/matcher, HEAD resolution, and step decisions (refusals, no-ops,
|
|
189934
189935
|
* commits, retry redirection, and the commit-state squash decision).
|
|
189935
189936
|
*
|
|
@@ -189942,18 +189943,17 @@ const validateFieldRules = (name, state, key, spec) => {
|
|
|
189942
189943
|
* commit states), and an optional `retry` cap. A `WorkflowDefinition`
|
|
189943
189944
|
* separately declares `entries` — the state names a process may START at
|
|
189944
189945
|
* (`default`, plus `manual` — every state declaring `entry: true`, enterable
|
|
189945
|
-
* via `gtd
|
|
189946
|
+
* via `gtd --entry <state>`). A definition
|
|
189946
189947
|
* may also declare `modes:` —
|
|
189947
189948
|
* named pairs of format/validate shell commands a state's `mode:` can point
|
|
189948
189949
|
* at (see `ModeDef`); they are inert data here too, rendered and executed
|
|
189949
189950
|
* only at the edge (`src/SteeringMode.ts`).
|
|
189950
189951
|
*
|
|
189951
189952
|
* This module is intentionally pure — no git, no filesystem, no Effect, no
|
|
189952
|
-
* IO of any kind
|
|
189953
|
-
*
|
|
189954
|
-
*
|
|
189955
|
-
*
|
|
189956
|
-
* are all EDGE concerns for a later phase.
|
|
189953
|
+
* IO of any kind: every export is a plain function of its arguments.
|
|
189954
|
+
* Rendering templates (`src/PatternTemplates.ts`), walking git history for
|
|
189955
|
+
* the process trace (`src/Edge.ts`), and emitting the commit/squash scripts
|
|
189956
|
+
* a driver runs (`src/Edge.ts`/`src/Emit.ts`) are all EDGE concerns.
|
|
189957
189957
|
*
|
|
189958
189958
|
* Its one import is `./StateFields.js` — the state-field vocabulary
|
|
189959
189959
|
* (`Actor`, `StateName`, `ContentKind`, `StateMode`, `OnEdge`, `RetryDef`,
|
|
@@ -190005,7 +190005,7 @@ const TRANSITION_SEP = " → ";
|
|
|
190005
190005
|
* state being ENTERED and `<from>` the state the authored changes were made
|
|
190006
190006
|
* in, so the subject reads as what this commit DID, not just where the machine
|
|
190007
190007
|
* is headed. `from` is optional: when it is omitted or equals `to` (a
|
|
190008
|
-
* self-loop, or a manual entry like `gtd
|
|
190008
|
+
* self-loop, or a manual entry like `gtd --entry <state>` that
|
|
190009
190009
|
* has no meaningful source), the subject collapses to the bare
|
|
190010
190010
|
* `gtd(<actor>): <to>` form.
|
|
190011
190011
|
* `resolveState` reads back only `<to>` — the ` → ` prefix is human context.
|
|
@@ -190237,8 +190237,16 @@ const memoryScopeAt = (scopes, state, trace) => {
|
|
|
190237
190237
|
* effect. Refusals: `invoker` isn't `state`'s declared actor (out-of-turn),
|
|
190238
190238
|
* or the tree is dirty and no `on` pattern matches (no-match, naming the
|
|
190239
190239
|
* declared patterns so the CLI can print them). A clean tree with no
|
|
190240
|
-
* matching pattern is a no-op
|
|
190241
|
-
* steps are the default, silent case
|
|
190240
|
+
* matching pattern is a plain no-op at a `script`/`message` rest — the loop
|
|
190241
|
+
* protocol's clean steps are the default, silent case there — but at a
|
|
190242
|
+
* `prompt` rest it is an ATTEMPT instead: the state itself becomes the raw
|
|
190243
|
+
* target, so it falls through the same retry/commit-state tail as a real
|
|
190244
|
+
* match, tagged `attempt: true` on the resulting `"commit"` (never on a
|
|
190245
|
+
* `"squash"`, which a redirect straight into a commit state still produces
|
|
190246
|
+
* unchanged) — see `StepCommit.attempt`'s doc comment for why a fruitless
|
|
190247
|
+
* dispatch is committed at all, and `wouldAttempt` for the "would another
|
|
190248
|
+
* dispatch just repeat this" question a step itself doesn't need to ask. A
|
|
190249
|
+
* match's target (or an attempt's self-target) is retry-redirected
|
|
190242
190250
|
* (`applyRetry`) before being classified: a commit-state target yields a
|
|
190243
190251
|
* `"squash"` decision carrying its `commit` template verbatim; anything
|
|
190244
190252
|
* else yields a `"commit"` decision naming the `gtd(<invoker>): <from> → <to>`
|
|
@@ -190260,20 +190268,23 @@ const step = (def, state, invoker, payload) => {
|
|
|
190260
190268
|
awaits: stateDef.actor
|
|
190261
190269
|
};
|
|
190262
190270
|
const onEdges = stateDef.on ?? [];
|
|
190263
|
-
|
|
190264
|
-
|
|
190265
|
-
|
|
190266
|
-
|
|
190267
|
-
state
|
|
190268
|
-
};
|
|
190269
|
-
return {
|
|
190271
|
+
let target = matchOn(onEdges, payload.changes);
|
|
190272
|
+
let attempt = false;
|
|
190273
|
+
if (target === void 0) {
|
|
190274
|
+
if (payload.changes.length !== 0) return {
|
|
190270
190275
|
kind: "refusal",
|
|
190271
190276
|
reason: "no-match",
|
|
190272
190277
|
state,
|
|
190273
190278
|
patterns: onEdges.map(([pattern]) => pattern)
|
|
190274
190279
|
};
|
|
190280
|
+
if (contentKindOf(stateDef) !== "prompt") return {
|
|
190281
|
+
kind: "noop",
|
|
190282
|
+
state
|
|
190283
|
+
};
|
|
190284
|
+
target = state;
|
|
190285
|
+
attempt = true;
|
|
190275
190286
|
}
|
|
190276
|
-
const finalTarget = applyRetry(def,
|
|
190287
|
+
const finalTarget = applyRetry(def, target, payload.processTrace);
|
|
190277
190288
|
const targetDef = def.states[finalTarget];
|
|
190278
190289
|
if (targetDef === void 0) throw new Error(`step: "${state}" transitions to undefined state "${finalTarget}"`);
|
|
190279
190290
|
if (targetDef.commit !== void 0) return {
|
|
@@ -190287,10 +190298,32 @@ const step = (def, state, invoker, payload) => {
|
|
|
190287
190298
|
subject: stateSubject(invoker, finalTarget, state),
|
|
190288
190299
|
actor: invoker,
|
|
190289
190300
|
from: state,
|
|
190290
|
-
to: finalTarget
|
|
190301
|
+
to: finalTarget,
|
|
190302
|
+
...attempt ? { attempt: true } : {}
|
|
190291
190303
|
};
|
|
190292
190304
|
};
|
|
190293
190305
|
/**
|
|
190306
|
+
* Would a clean step (an empty change set) at `state`, invoked by its own
|
|
190307
|
+
* declared actor, record ANOTHER attempt that stays AT `state` — "another
|
|
190308
|
+
* dispatch would just record another attempt" (see `Edge.ts`'s `stalledAt`,
|
|
190309
|
+
* the derived stall's other half). Runs `step` itself rather than
|
|
190310
|
+
* restating its retry-redirect precedence: a capped `retry` that would
|
|
190311
|
+
* redirect the attempt to `otherwise` (or squash it into a commit state)
|
|
190312
|
+
* makes this `false` — a further dispatch would actually escalate, not
|
|
190313
|
+
* repeat the same fruitless turn. `false` for an unknown/commit state, or
|
|
190314
|
+
* any state whose clean step is a signal (`C`) or a plain no-op
|
|
190315
|
+
* (`script`/`message`).
|
|
190316
|
+
*/
|
|
190317
|
+
const wouldAttempt = (def, state, processTrace) => {
|
|
190318
|
+
const stateDef = def.states[state];
|
|
190319
|
+
if (stateDef?.actor === void 0) return false;
|
|
190320
|
+
const decision = step(def, state, stateDef.actor, {
|
|
190321
|
+
changes: [],
|
|
190322
|
+
processTrace
|
|
190323
|
+
});
|
|
190324
|
+
return decision.kind === "commit" && decision.attempt === true && decision.to === state;
|
|
190325
|
+
};
|
|
190326
|
+
/**
|
|
190294
190327
|
* `entries.default` names a defined, non-commit state. Every entry in
|
|
190295
190328
|
* `entries.manual`, when present, must likewise name a defined, non-commit
|
|
190296
190329
|
* state distinct from `entries.default` — a manual entry is a DELIBERATE,
|
|
@@ -190402,8 +190435,8 @@ const validateRetry = (name, state, names) => {
|
|
|
190402
190435
|
* `retry.otherwise` redirects (a redirect ENTERS its `otherwise` state exactly
|
|
190403
190436
|
* like an `on` match enters its target — see `applyRetry` — so both are real
|
|
190404
190437
|
* edges). The roots are `def.entries` — `default` PLUS every state named in
|
|
190405
|
-
* `entries.manual`: a manual entry is entered directly (`gtd
|
|
190406
|
-
*
|
|
190438
|
+
* `entries.manual`: a manual entry is entered directly (`gtd --entry
|
|
190439
|
+
* <state>`), so a state reachable only from one of them is
|
|
190407
190440
|
* legitimately reachable, not dead config (without seeding them, e.g. a
|
|
190408
190441
|
* manual entry whose only inbound path is `--entry` would be wrongly
|
|
190409
190442
|
* flagged). Plain BFS; targets naming undefined states are skipped here (they
|
|
@@ -191441,7 +191474,7 @@ const isSeededValidateCommand = (mode, command) => command === seededValidateCom
|
|
|
191441
191474
|
* label: <string> # optional, opaque display name — never on a commit state
|
|
191442
191475
|
* file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
|
|
191443
191476
|
* mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
|
|
191444
|
-
* entry: true # optional — an EXTRA reachability root (WorkflowEntries.manual), enterable via `gtd
|
|
191477
|
+
* entry: true # optional — an EXTRA reachability root (WorkflowEntries.manual), enterable via `gtd --entry <this state's qualified name>`; distinct from the top-level `entry:` key above
|
|
191445
191478
|
* <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
|
|
191446
191479
|
* ```
|
|
191447
191480
|
*
|
|
@@ -192154,7 +192187,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192154
192187
|
};
|
|
192155
192188
|
//#endregion
|
|
192156
192189
|
//#region src/workflows/unified.yaml
|
|
192157
|
-
var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See the README's \"How it works\" and \"Driving the loop\" sections for\n# the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this raw source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceModel`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`). Two\n# gate/queue machines below are deliberately IDENTITY-FREE (no `model:`):\n# `entryGate`/`healthGate` are infrastructure, not a persona, and `packageLoop`\n# is a bookkeeping queue, not a builder.\n#\n# * entryGate (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple (`plan-gate`), advanced\n# (`spec-gate`) and review (`review-gate`)\n# entries. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# simple build's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * advancedPlan (encap) — the advanced flow's one design conversation:\n# product Q&A, technical Q&A, and decomposition\n# (`design.*`: product-author/-answer,\n# technical-author/-answer, decompose) all share\n# ONE machine identity, so the agent explores the\n# codebase once, not three times, and the human's\n# answer rationale survives into decomposition.\n# ▸ planner.\n# * planLoop (encap) — the simple flow's plan-iteration loop (`plan.*`).\n# ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `simpleBuild` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — the implementer's OWN\n# follow-through on feedback is a coder action,\n# not a reviewer one, so it lives in\n# `build.addressing` instead.\n# * specReview (encap) — the advanced flow's per-package agentic review,\n# now a SINGLE state (`packages.item.spec.review`)\n# — a machine whose entire job is to be a\n# separate mind from the implementer. ▸ planner\n# — fixing the reviewer's own findings is a coder\n# action, so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the advanced flow's per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the advanced flow's per-package build identity\n# (`packages.item.*`: building, fix-suite,\n# fix-spec, closing), plus its own `health` and\n# `spec` children. ▸ coder.\n# * simpleBuild (encap) — the simple flow's build identity (`build.*`:\n# building, squashing, addressing, fix), plus its\n# own `health` child AND its own `review` child\n# (humanReview, above). ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `simpleBuild` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. The\n# review tail sits between the two coder turns that most need continuity —\n# `build.addressing` (follows through on review feedback) and `build.squashing`\n# (drafts the finale from the range this process built) — so hoisting it back\n# to a root sibling would force both of those turns to re-derive the feature\n# from scratch on every pass through the tail, instead of resuming the session\n# that built it. The reviewer itself is unaffected either way: `build.review.*`\n# is its own scope regardless of where it's nested, so it never resumes the\n# builder's session.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human blocked gate (fix the failures and re-run, exactly like the\n# escalate states), or is what `gtd --entry fix-precheck` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * the configured requirementsFile (default .gtd/REQUIREMENTS.md) ->\n# `spec-gate.check` -> the ADVANCED flow (one design conversation across\n# product + technical Q&A and decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. the configured todoFile, default .gtd/TODO.md) ->\n# `plan-gate.check` -> the SIMPLE flow (one grilling Q&A loop, a monolithic\n# build, no decomposition, no agentic review).\n#\n# A third entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A fourth, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All three converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which inspects\n# the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> build.squashing -> done (the ONLY path\n# to the finale — the whole process collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# build.review.deciding writes REVIEW_FEEDBACK.md -> build.review.collecting\n# implements it (completing any code the human hand-edited, never\n# reverting their lines) -> build.addressing (a coder action, not a\n# reviewer one — see \"Why humanReview is nested\" above) -> back through\n# `build.health.check` (the shared health pair, bound via the\n# `$onFeedback`/local `review` reference) -> build.review.reviewing\n# regenerates an INCREMENTAL REVIEW.md (build.review.deciding carries\n# `reviewBase: true`, so each feedback round's review + checkout window\n# covers only `last-review..HEAD`, i.e. the agent's follow-through, not the\n# human's own edit) -> await-review, looping until sign-off.\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus the caller's\n# own `build.fix`) serves the simple build and the feedback loop — its green\n# edge targets `build.review.reviewing`. The advanced per-package loop keeps\n# its OWN health pair (`packages.item.health.check`/`.escalate`, plus\n# `packages.item.fix-suite`) because its green edge targets the per-package\n# `packages.item.spec.review` gate instead.\n#\n# Every `on` pattern's path below is written as the matching var's Eta\n# template (e.g. `\"A <%= it.vars.feedbackFile %>\"`), not a literal `.gtd/...`\n# path — repointing a var reroutes the machine along with every template that\n# reads/writes it (see the README's \"Variables\" section).\n\nvars:\n testCommand: npm test\n # Where gtd keeps its own scratch/bookkeeping (the check scripts' temp output\n # file, review-deciding's state-file exclusion). Independent of the per-file\n # vars below — relocating a file wholesale means setting both.\n stateDir: .gtd\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/technical-qa/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n\n# `prose` is a format-only steering-file mode this workflow blesses for itself:\n# a curated free-form document with no gtd-side schema (the simple flow's plan\n# file, .gtd/TODO.md — see the `plan.planning` state below). An empty entry is\n# the format-only tier any workflow can declare for a name with no built-in\n# parser; `qa`/`review` need no entry here — src/PatternConfig.ts's compiler\n# seeds those two from the built-in registry (src/SteeringFormats.ts)\n# automatically.\nmodes:\n prose: {}\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts (its script is the shared suite-check\n # wrapper — red -> the configured feedbackFile, green -> remove it); the `on`\n # rules turn that into \"halt at blocked\" vs \"proceed to $onGreen\". The script\n # and blocked message/describe differ per entry, so they ride in as `with:`\n # params. Kept SEPARATE from `healthGate` (below) even though both are a\n # check+something-else shape: this gate runs BEFORE planning, so it must NOT\n # sweep the plan/review steering files healthGate does — a human's fresh\n # sketch legitimately still exists here.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # Anchored here and aliased by `fix-precheck` (`gtd --entry\n # fix-precheck` entry): one suite-check script shared by all four\n # call sites (this machine's ×3 instances plus `fix-precheck`)\n # instead of four source copies. These gates run BEFORE planning, so\n # — unlike `healthGate.check` — they must NOT sweep\n # `todoFile`/`requirementsFile`: the human's fresh sketch still\n # legitimately exists here.\n # `entry: true` on the shared local marks all three `entryGate`\n # instances (plan-gate/spec-gate/review-gate) as manual entries —\n # only `review-gate.check` actually needs the reachability root (it\n # is otherwise unreachable from `entries.default`), but the shared\n # dedup makes marking one mark all three. Accepted as correct: all\n # three ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `plan-gate`/`spec-gate` bind the\n # literal empty string, which compiles away to \"no base\" here — they\n # are reached from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n script: &suiteCheck |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.stateDir %>\"\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": blocked\n \"M <%= it.vars.feedbackFile %>\": blocked\n \"D <%= it.vars.feedbackFile %>\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records the configured feedbackFile and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the simple build's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # This script ALSO sweeps spent steering files (`todoFile`,\n # `reviewRawFile`, `reviewFeedbackFile`) — a mechanic `entryGate.check`\n # must NOT run, since it executes before planning, while a fresh\n # sketch legitimately still exists.\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.stateDir %>\"\n # Hoist every path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a\n # line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep steering files spent by the states that led here: the plan\n # (building), the raw review capture (review.collecting), and the\n # feedback instructions (build.addressing) — each `rm -f` is a\n # no-op when the file is already gone or never existed on this path.\n todo=\"<%~ it.vars.todoFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n reviewFeedback=\"<%~ it.vars.reviewFeedbackFile %>\"\n rm -f \"$todo\" \"$reviewRaw\" \"$reviewFeedback\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": $onRed\n \"M <%= it.vars.feedbackFile %>\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── ENCAPSULATION: the advanced flow's design conversation ─────────────────\n # One planner identity across product Q&A -> technical Q&A -> decomposition:\n # a single codebase exploration instead of three, and the human's answer\n # rationale survives from product into technical and into decomposition (all\n # five states share one memory scope, `design`). Replaces the former\n # `qaLoop ×2` (which split one design conversation into two minds that each\n # re-explored the codebase) plus `simpleBuild`'s own `decompose` (which\n # landed on the coder tier only because it happened to live in the build\n # machine — decomposition is judgement-heavy planning work, not building).\n # ▸ planner identity — `model:` declared once here, stamped onto\n # `product-author`, `technical-author`, and `decompose` (its own prompt\n # states; the `*-answer` states are human turns and carry no prompt).\n advancedPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: product-author\n states:\n product-author:\n actor: agent\n label: Refining the product plan\n file: <%= it.vars.requirementsFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this turn\n touches is `<%= it.vars.requirementsFile %>`; develop it. Do not\n create other files to hold notes or output.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n on:\n \"* **\": product-answer\n\n product-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.requirementsFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: technical-author\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n technical Q&A (**design.technical-author**).\n \"* **\":\n to: product-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.product-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n technical-author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.architectureFile %>` (write it) and\n `<%= it.vars.requirementsFile %>` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n If you developed those requirements earlier in this conversation, work\n from what you already settled; otherwise read\n `<%= it.vars.requirementsFile %>` (the converged product plan) yourself.\n Develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n on:\n \"* **\": technical-answer\n\n technical-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.architectureFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: decompose\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n decomposing the work into packages (**design.decompose**).\n \"* **\":\n to: technical-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.technical-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `<%= it.vars.packagesDir %>/` and\n `<%= it.vars.architectureFile %>`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `<%= it.vars.architectureFile %>` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. Decompose it into an ordered\n set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `planning`\n # (its only prompt state).\n planLoop:\n model: <%= it.vars.plannerModel %>\n params: [onAccept]\n entry: planning\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n mode: prose\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.todoFile %>`; develop it into the plan.\n Do not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. If you wrote that plan earlier in\n this conversation, work from what you already know plus these edits;\n otherwise read the file fresh. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd step agent` yourself; the harness does\n that.\n on:\n \"* **\": await-plan\n\n await-plan:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n mode: prose\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n action: Accept plan\n describe: >-\n change nothing to move on to implementation (**build.building**).\n \"* **\":\n to: planning\n action: Revise plan\n describe: >-\n rewrite a step, add a constraint, or leave an inline comment to\n send it back for another round (**plan.planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). The implementer's OWN\n # follow-through on feedback is a coder action, not a reviewer one — it\n # lives in `build.addressing` (`$onFeedback`), not inside this machine.\n humanReview:\n model: <%= it.vars.plannerModel %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.reviewFile %>`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `deciding`, which decides sign-off vs.\n # feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd step human`:\n\n - **Sign off** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review.deciding** → **build.addressing**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the process start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round other than `.gtd/` or the state's own\n # reviewFile (which is var-configurable and may live outside `.gtd/`, e.g.\n # a root-level REVIEW.md — issue #128). Otherwise it is a clean sign-off.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then turns it into instructions); it does NOT\n # interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n stateDir=\"<%~ it.vars.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n head=$(git rev-parse HEAD)\n mkdir -p \"$(dirname \"$reviewRawFile\")\"\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n # Did the human hand-edit anything this round besides stateDir and the\n # reviewFile itself (var-configurable — may live outside stateDir, so it\n # is excluded by exact path, not just the stateDir prefix)?\n if git diff-tree --no-commit-id --name-only -r HEAD \\\n | grep -v \"^$stateDir/\" | grep -vxF \"$reviewFile\" | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the changes and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions — it names a commit, never inlined content.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit; their hand edits,\"\n echo \"if any, are in that commit's other paths. Run: git show $head\"\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": collecting\n \"M <%= it.vars.reviewRawFile %>\": collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.reviewRawFile %>` (read it, then\n delete it) and `<%= it.vars.reviewFeedbackFile %>` (write it) — do\n NOT change any other file, you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n \"M <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n\n # ── ENCAPSULATION: the advanced flow's per-package agentic review ──────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `<%= it.vars.specFeedbackFile %>`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A <%= it.vars.specFeedbackFile %>\": $onFix\n \"M <%= it.vars.specFeedbackFile %>\": $onFix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the advanced flow's per-package build queue ─────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n # Sweep the advanced flow's spent Q&A steering files: requirementsFile\n # is consumed by technical-qa, architectureFile by decompose — both are\n # gone by the time the package queue starts. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n rm -f \"$requirements\" \"$architecture\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n mkdir -p \"$(dirname \"$nextFile\")\"\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D <%= it.vars.nextFile %>\": $onDrained\n \"* <%= it.vars.nextFile %>\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the advanced flow's per-package build identity ──────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. This turn writes no state\n file: do NOT delete the package file (the spec-review gate reads\n it after you) and never touch `<%= it.vars.nextFile %>` — the\n `picking` state owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: &suiteFixPrompt |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.specFeedbackFile %>`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec review clean) and on the\n # spec review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the simple flow's build identity ────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `squashing`, `addressing`, and `fix` (its own prompt states). Decomposition\n # (`design.decompose`) belongs to the planner tier now, not here — see\n # `advancedPlan` below. `addressing` is the implementer's OWN follow-through\n # on review feedback (a coder action) — reached via `$onFeedback`, bound at\n # its own `review:` reference below, not nested inside the (planner-only)\n # `humanReview` machine. `humanReview` is nested HERE (not at the root) so\n # the review tail's run stays inside the builder's own scope — see the\n # header comment.\n simpleBuild:\n model: <%= it.vars.coderModel %>\n params: [onDone]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state file this turn touches is\n `<%= it.vars.todoFile %>`; delete it once its work is complete. Do not\n create other files to hold notes or output.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `<%= it.vars.commitMsgFile %>`; write\n it. Do not create other files to hold notes or output.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `<%= it.vars.commitMsgFile %>` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `<%= it.vars.commitMsgFile %>` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A <%= it.vars.commitMsgFile %>\": $onDone\n \"M <%= it.vars.commitMsgFile %>\": $onDone\n\n addressing:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The\n # feedback-progress guard (`src/StepGuards.ts`) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. This\n workflow steers itself through its own state files — treat them as\n its private scratchpad, never as project code or documentation. The\n only state file this turn touches is\n `<%= it.vars.reviewFeedbackFile %>`; address it, then delete it. Do\n not create other files to hold notes or output.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": health.check\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: *suiteFixPrompt\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so its run stays\n # inside the builder's own scope across the feedback round-trip — see\n # the header comment. Sign-off -> squashing; feedback -> addressing (the\n # implementer's own coder-identity follow-through, not a reviewer action).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: addressing\n\n # The root machine — the whole \"advanced/simple\" flow tree described in\n # this file's header comment. WIRING ONLY: no identity of its own, just the\n # shared entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd process.\n\n To start one, create a steering file describing what you want built:\n\n - `<%= it.vars.todoFile %>` — a short sketch — starts the SIMPLE flow\n (plan, build, review).\n - `<%= it.vars.requirementsFile %>` — the product requirements — starts\n the ADVANCED flow (product + technical Q&A, package decomposition,\n parallel build, per-package agentic review).\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* <%= it.vars.requirementsFile %>\":\n to: spec-gate\n action: Start advanced flow\n describe: >-\n save the product requirements to `.gtd/REQUIREMENTS.md` — gtd first\n checks the test baseline is green, then the agent develops them\n through product then technical Q&A, decomposes the work into\n packages, and builds each in parallel (**spec-gate.check**).\n \"* **\":\n to: plan-gate\n action: Start simple flow\n describe: >-\n save a short sketch to `.gtd/TODO.md` (or any other change) — gtd\n first checks the test baseline is green, then the agent reads it,\n explores the codebase, and develops it into a concrete\n implementation plan (**plan-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix).\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A <%= it.vars.feedbackFile %>\": build.fix\n \"M <%= it.vars.feedbackFile %>\": build.fix\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n # ── Green-baseline gate: simple flow entry. ────────────────────────────────\n plan-gate:\n machine: entryGate\n with:\n onGreen: plan\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**plan-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: advanced flow entry. ──────────────────────────────\n spec-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**spec-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Advanced flow: the design conversation (design.product-author/.product-answer ─\n # -> design.technical-author/.technical-answer -> design.decompose -> packages). ──\n design:\n machine: advancedPlan\n with:\n onPackages: packages\n\n # ── Simple flow plan-iteration loop (plan.planning/.await-plan -> build.building). ──\n plan:\n machine: planLoop\n with:\n onAccept: build.building\n\n # ── Advanced per-package build queue (packages.* -> build.review). ────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── Simple flow's build identity (build.* -> build.review -> done). ───────\n build:\n machine: simpleBuild\n with:\n onPackages: packages\n onDone: done\n";
|
|
192190
|
+
var unified_default = "# The unified gtd workflow template — gtd's BUILT-IN DEFAULT workflow. When no\n# `workflow:` key is configured anywhere in the cwd→home config chain,\n# src/Config.ts runs this template directly, so a state command works with no\n# configuration at all. `gtd init` seeds only the settings a project tunes\n# (vars.testCommand + a modes: formatting suggestion), never the workflow\n# itself. See the README's \"How it works\" and \"Driving the loop\" sections for\n# the full walkthrough.\n#\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/templates.ts does the compiling). Every content string below\n# is INLINE (no `./`-relative file references): the bundled template ships\n# inside a single-file build (`dist/gtd.bundle.mjs`), so its content must\n# already be resolved at BUILD time. `renderInitConfig` (src/workflows/\n# templates.ts) can MATERIALIZE this raw source into a `.gtdrc.json`\n# `workflow:` value — the way to eject/customize the machine, and the hermetic\n# test fixture — but `gtd init` itself no longer writes the workflow.\n#\n# ── Authored with MACHINES (entry:/machines:) ───────────────────────────────\n#\n# This template is written as a tree of reusable, parameterized MACHINES (see\n# src/Machines.ts) — a gate/loop written once and instantiated several times\n# with different `with:` bindings (dedup), or a complex cluster grouped under\n# one name for source comprehension (encapsulation). Every reference below is\n# an ordinary `{ machine: <name>, with: { ... } }` local, expanded at\n# compile-time into concrete, qualified states before the pure engine ever\n# sees the definition — no runtime footprint.\n#\n# MACHINE BOUNDARIES LINE UP WITH CONVERSATIONAL IDENTITY: a machine that holds\n# an identity (a planner or a coder persona) declares its own `model:` ONCE, at\n# the machine level (stamped onto every one of its own `prompt` states — see\n# src/Machines.ts's `resolveInstanceModel`), instead of repeating `model:` on\n# every state; no state anywhere declares `model:` or `memory:` directly any\n# more (the memory key is computed from the machine tree — see\n# src/PatternMachine.ts's `memoryScopeAt` / src/Edge.ts's `memoryKeyFor`). Two\n# gate/queue machines below are deliberately IDENTITY-FREE (no `model:`):\n# `entryGate`/`healthGate` are infrastructure, not a persona, and `packageLoop`\n# is a bookkeeping queue, not a builder.\n#\n# * entryGate (dedup ×3) — the green-baseline gate (check + human blocked)\n# guarding the simple (`plan-gate`), advanced\n# (`spec-gate`) and review (`review-gate`)\n# entries. Identity-free.\n# * healthGate (dedup ×2) — the check/escalate health pair shared by the\n# simple build's own tail (`build.health.*`) and\n# the per-package build's own tail\n# (`packages.item.health.*`) — each caller's OWN\n# `fix` state (a coder-identity action) lives\n# beside this machine, not inside it, since\n# fixing IS a coder action and this machine has\n# no identity of its own. Identity-free.\n# * advancedPlan (encap) — the advanced flow's one design conversation:\n# product Q&A, technical Q&A, and decomposition\n# (`design.*`: product-author/-answer,\n# technical-author/-answer, decompose) all share\n# ONE machine identity, so the agent explores the\n# codebase once, not three times, and the human's\n# answer rationale survives into decomposition.\n# ▸ planner.\n# * planLoop (encap) — the simple flow's plan-iteration loop (`plan.*`).\n# ▸ planner.\n# * humanReview (encap) — the shared review tail, nested INSIDE\n# `simpleBuild` (`build.review.*`: reviewing →\n# await-review → deciding → collecting) — see\n# \"Why humanReview is nested, not a root sibling\"\n# below. ▸ planner — the implementer's OWN\n# follow-through on feedback is a coder action,\n# not a reviewer one, so it lives in\n# `build.addressing` instead.\n# * specReview (encap) — the advanced flow's per-package agentic review,\n# now a SINGLE state (`packages.item.spec.review`)\n# — a machine whose entire job is to be a\n# separate mind from the implementer. ▸ planner\n# — fixing the reviewer's own findings is a coder\n# action, so it lives in `packages.item.fix-spec`\n# instead.\n# * packageLoop (encap) — the advanced flow's per-package build queue\n# (`packages.picking`). Identity-free — the\n# actual per-package build identity lives in its\n# own `item` child (packageItem, below).\n# * packageItem (encap) — the advanced flow's per-package build identity\n# (`packages.item.*`: building, fix-suite,\n# fix-spec, closing), plus its own `health` and\n# `spec` children. ▸ coder.\n# * simpleBuild (encap) — the simple flow's build identity (`build.*`:\n# building, squashing, addressing, fix), plus its\n# own `health` child AND its own `review` child\n# (humanReview, above). ▸ coder.\n#\n# ── Why humanReview is nested, not a root sibling ───────────────────────────\n#\n# `humanReview` sits UNDER `simpleBuild` (`build.review`) rather than beside it\n# at the root, because a machine's memory scope is its dotted instance path\n# (see src/PatternMachine.ts's `memoryScopeAt`): entering a DESCENDANT scope\n# never breaks the parent's unbroken run, but entering a sibling does. The\n# review tail sits between the two coder turns that most need continuity —\n# `build.addressing` (follows through on review feedback) and `build.squashing`\n# (drafts the finale from the range this process built) — so hoisting it back\n# to a root sibling would force both of those turns to re-derive the feature\n# from scratch on every pass through the tail, instead of resuming the session\n# that built it. The reviewer itself is unaffected either way: `build.review.*`\n# is its own scope regardless of where it's nested, so it never resumes the\n# builder's session.\n#\n# ── Four entry points behind a green-baseline gate, one shared tail ─────────\n#\n# EVERY entry first runs the test suite and only proceeds when it is GREEN — you\n# never start new work (or a review) on top of a red baseline. A red run halts\n# at a human blocked gate (fix the failures and re-run, exactly like the\n# escalate states), or is what `gtd --entry fix-precheck` exists to repair.\n#\n# The initial `idle` state FORKS on which steering file the human creates,\n# routing into that flow's OWN start gate:\n#\n# * the configured requirementsFile (default .gtd/REQUIREMENTS.md) ->\n# `spec-gate.check` -> the ADVANCED flow (one design conversation across\n# product + technical Q&A and decomposition, per-package parallel build +\n# agentic spec-review).\n# * anything else (e.g. the configured todoFile, default .gtd/TODO.md) ->\n# `plan-gate.check` -> the SIMPLE flow (one grilling Q&A loop, a monolithic\n# build, no decomposition, no agentic review).\n#\n# A third entry, `gtd --entry review-gate.check`, jumps there directly and,\n# once green, to `build.review.reviewing` to review a foreign branch through\n# the same shared tail. A fourth, `gtd --entry fix-precheck`, jumps there: a\n# red suite drops straight into the shared `build.fix` loop and out through the\n# review + squash tail; a green suite is a no-op back to `idle`. Both states\n# declare `entry: true` — otherwise unreachable from `entries.default`.\n#\n# All three converge at `build.review.reviewing` -> `build.review.await-review`.\n# Ticking a REVIEW.md box means only \"I reviewed this hunk\"; a COMMENT (not an\n# unticked box) is what asks for changes. Every human step at\n# `build.review.await-review` goes to `build.review.deciding`, which inspects\n# the step and routes:\n# * no comment (every box ticked, no REVIEW.md note, no code edit) -> the diff\n# is a bare REVIEW.md deletion -> build.squashing -> done (the ONLY path\n# to the finale — the whole process collapses into one commit).\n# * any comment — a note in REVIEW.md OR a hand-edit to code — ->\n# build.review.deciding writes REVIEW_FEEDBACK.md -> build.review.collecting\n# implements it (completing any code the human hand-edited, never\n# reverting their lines) -> build.addressing (a coder action, not a\n# reviewer one — see \"Why humanReview is nested\" above) -> back through\n# `build.health.check` (the shared health pair, bound via the\n# `$onFeedback`/local `review` reference) -> build.review.reviewing\n# regenerates an INCREMENTAL REVIEW.md (build.review.deciding carries\n# `reviewBase: true`, so each feedback round's review + checkout window\n# covers only `last-review..HEAD`, i.e. the agent's follow-through, not the\n# human's own edit) -> await-review, looping until sign-off.\n# Two dead ends never commit: the review-signoff guard (src/StepGuards.ts)\n# REFUSES a step that leaves a box unticked with no comment (finish reviewing\n# first) and a deleted REVIEW.md (restore it and tick the boxes).\n#\n# The shared health pair (`build.health.check`/`.escalate`, plus the caller's\n# own `build.fix`) serves the simple build and the feedback loop — its green\n# edge targets `build.review.reviewing`. The advanced per-package loop keeps\n# its OWN health pair (`packages.item.health.check`/`.escalate`, plus\n# `packages.item.fix-suite`) because its green edge targets the per-package\n# `packages.item.spec.review` gate instead.\n#\n# Every `on` pattern's path below is written as the matching var's Eta\n# template (e.g. `\"A <%= it.vars.feedbackFile %>\"`), not a literal `.gtd/...`\n# path — repointing a var reroutes the machine along with every template that\n# reads/writes it (see the README's \"Variables\" section).\n\nvars:\n testCommand: npm test\n # Where gtd keeps its own scratch/bookkeeping (the check scripts' temp output\n # file, review-deciding's state-file exclusion). Independent of the per-file\n # vars below — relocating a file wholesale means setting both.\n stateDir: .gtd\n todoFile: .gtd/TODO.md\n requirementsFile: .gtd/REQUIREMENTS.md\n architectureFile: .gtd/ARCHITECTURE.md\n packagesDir: .gtd/packages\n nextFile: .gtd/NEXT.md\n reviewFile: .gtd/REVIEW.md\n reviewRawFile: .gtd/REVIEW_RAW.md\n reviewFeedbackFile: .gtd/REVIEW_FEEDBACK.md\n feedbackFile: .gtd/FEEDBACK.md\n specFeedbackFile: .gtd/SPEC_FEEDBACK.md\n commitMsgFile: .gtd/COMMIT_MSG.md\n # The two opaque model tiers every agent state draws from — repoint them here\n # (or via a top-level `.gtdrc` `vars:` key / a `GTD_PLANNERMODEL` env var)\n # to change every state on that tier at once. `plannerModel` (default \"smart\")\n # is the heavier tier for the one-shot planning/technical-qa/review turns;\n # `coderModel` (default \"base\") is the tier for the build/fix turns.\n plannerModel: smart\n coderModel: base\n # The fixed diff base for `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` — a plain non-gtd branch reviewed against that\n # commitish rather than the process start. Declared here (rather than left\n # undeclared) so `--var reviewBase=...` passes the CLI's declared-name\n # check; the default empty string renders blank, which the entry command\n # refuses (a review entry always requires an explicit base).\n reviewBase: \"\"\n\n# `prose` is a format-only steering-file mode this workflow blesses for itself:\n# a curated free-form document with no gtd-side schema (the simple flow's plan\n# file, .gtd/TODO.md — see the `plan.planning` state below). An empty entry is\n# the format-only tier any workflow can declare for a name with no built-in\n# parser; `qa`/`review` need no entry here — src/PatternConfig.ts's compiler\n# seeds those two from the built-in registry (src/SteeringFormats.ts)\n# automatically.\nmodes:\n prose: {}\n\nentry:\n default: unified\n\nmachines:\n # ── DEDUP: the green-baseline entry gate (identity-free) ──────────────────\n # Runs the suite before any work starts (its script is the shared suite-check\n # wrapper — red -> the configured feedbackFile, green -> remove it); the `on`\n # rules turn that into \"halt at blocked\" vs \"proceed to $onGreen\". The script\n # and blocked message/describe differ per entry, so they ride in as `with:`\n # params. Kept SEPARATE from `healthGate` (below) even though both are a\n # check+something-else shape: this gate runs BEFORE planning, so it must NOT\n # sweep the plan/review steering files healthGate does — a human's fresh\n # sketch legitimately still exists here.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # Anchored here and aliased by `fix-precheck` (`gtd --entry\n # fix-precheck` entry): one suite-check script shared by all four\n # call sites (this machine's ×3 instances plus `fix-precheck`)\n # instead of four source copies. These gates run BEFORE planning, so\n # — unlike `healthGate.check` — they must NOT sweep\n # `todoFile`/`requirementsFile`: the human's fresh sketch still\n # legitimately exists here.\n # `entry: true` on the shared local marks all three `entryGate`\n # instances (plan-gate/spec-gate/review-gate) as manual entries —\n # only `review-gate.check` actually needs the reachability root (it\n # is otherwise unreachable from `entries.default`), but the shared\n # dedup makes marking one mark all three. Accepted as correct: all\n # three ARE the workflow's entry gates.\n entry: true\n # `reviewBase: $reviewBase` — only `review-gate` binds a real\n # template (`<%= it.vars.reviewBase %>`), fixing the whole process's\n # diff base to whatever `gtd --entry review-gate.check --var\n # reviewBase=<commitish>` supplies. `plan-gate`/`spec-gate` bind the\n # literal empty string, which compiles away to \"no base\" here — they\n # are reached from `idle`, never manually entered with a base.\n reviewBase: $reviewBase\n script: &suiteCheck |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.stateDir %>\"\n # Hoist the feedback path once, at the TOP: Eta's autoTrim eats the\n # newline after an interpolation tag, so no tag may be the last token on\n # a line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": blocked\n \"M <%= it.vars.feedbackFile %>\": blocked\n \"D <%= it.vars.feedbackFile %>\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: <%= it.vars.feedbackFile %>\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # ── DEDUP: the check/escalate health pair (identity-free) ─────────────────\n # Runs the suite; a red run records the configured feedbackFile and routes to\n # the caller's OWN fixer (`$onRed` — a coder-identity action that lives\n # beside this machine, not inside it, since fixing IS a coder action and this\n # machine has no identity); a green run cleans it up and routes to\n # `$onGreen`. Escalation redirects here after the caller's fixer has retried\n # `max` times without success. Instantiated for the simple build's own tail\n # (`build.health.*`, caller's fixer is `build.fix`) and the per-package\n # build's own tail (`packages.item.health.*`, caller's fixer is\n # `packages.item.fix-suite`).\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # This script ALSO sweeps spent steering files (`todoFile`,\n # `reviewRawFile`, `reviewFeedbackFile`) — a mechanic `entryGate.check`\n # must NOT run, since it executes before planning, while a fresh\n # sketch legitimately still exists.\n script: |\n #!/usr/bin/env bash\n # gtd check turn — run the suite; a red run records .gtd/FEEDBACK.md, a\n # green run cleans it up. The `on` rules decide what that means.\n set +e\n mkdir -p \"<%~ it.vars.stateDir %>\"\n # Hoist every path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a\n # line — it would glue the next line's `else`/`fi` onto it and break\n # the script. Below this point everything is plain bash.\n feedback=\"<%~ it.vars.feedbackFile %>\"\n mkdir -p \"$(dirname \"$feedback\")\"\n # Sweep steering files spent by the states that led here: the plan\n # (building), the raw review capture (review.collecting), and the\n # feedback instructions (build.addressing) — each `rm -f` is a\n # no-op when the file is already gone or never existed on this path.\n todo=\"<%~ it.vars.todoFile %>\"\n reviewRaw=\"<%~ it.vars.reviewRawFile %>\"\n reviewFeedback=\"<%~ it.vars.reviewFeedbackFile %>\"\n rm -f \"$todo\" \"$reviewRaw\" \"$reviewFeedback\"\n <%~ it.vars.testCommand %> > <%~ it.vars.stateDir %>/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s <%~ it.vars.stateDir %>/.check-output ]; then\n mv <%~ it.vars.stateDir %>/.check-output \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > \"$feedback\"\n fi\n # A repeat identical failure would otherwise leave FEEDBACK.md\n # byte-identical to its committed copy — no diff, so the check would\n # look GREEN. Stamp each red run with the current HEAD (which always\n # advances between checks) so a still-red re-run always re-registers\n # as an M/A edit and takes the red edge.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> \"$feedback\"\n else\n rm -f <%~ it.vars.stateDir %>/.check-output\n rm -f \"$feedback\"\n fi\n on:\n \"A <%= it.vars.feedbackFile %>\": $onRed\n \"M <%= it.vars.feedbackFile %>\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: <%= it.vars.feedbackFile %>\n message: |\n The agent could not get the check to pass after repeated attempts.\n `<%= it.vars.feedbackFile %>` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # ── ENCAPSULATION: the advanced flow's design conversation ─────────────────\n # One planner identity across product Q&A -> technical Q&A -> decomposition:\n # a single codebase exploration instead of three, and the human's answer\n # rationale survives from product into technical and into decomposition (all\n # five states share one memory scope, `design`). Replaces the former\n # `qaLoop ×2` (which split one design conversation into two minds that each\n # re-explored the codebase) plus `simpleBuild`'s own `decompose` (which\n # landed on the coder tier only because it happened to live in the build\n # machine — decomposition is judgement-heavy planning work, not building).\n # ▸ planner identity — `model:` declared once here, stamped onto\n # `product-author`, `technical-author`, and `decompose` (its own prompt\n # states; the `*-answer` states are human turns and carry no prompt).\n advancedPlan:\n model: <%= it.vars.plannerModel %>\n params: [onPackages]\n entry: product-author\n states:\n product-author:\n actor: agent\n label: Refining the product plan\n file: <%= it.vars.requirementsFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this turn\n touches is `<%= it.vars.requirementsFile %>`; develop it. Do not\n create other files to hold notes or output.\n\n `<%= it.vars.requirementsFile %>` holds the product requirements (or the\n plan you already wrote plus the human's answers on a return lap). Read it,\n explore the codebase, and develop it into a concrete, PRODUCT-level plan\n in this one turn.\n\n Scope: product and user-facing decisions only. Leave implementation\n details (file/module structure, data models, tech-stack choices) for the\n next phase (`<%= it.vars.architectureFile %>`).\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading near the top: one `### <question>` sub-heading\n each, followed by a checkbox list of candidate answers — aim for TWO\n concrete options plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question. The\n last `- [ ] _your answer_` line is the human's own-answer slot; write it\n with the literal `_your answer_` placeholder and leave it for them.\n\n On a RETURN lap the human has ticked one option per question (or replaced\n `_your answer_` with their own text). For each answered question: fold the\n chosen answer into the plan prose, then MOVE the question down into a\n `## Answered Questions` section as `### <question>` followed by the\n resolved answer in plain PROSE (drop the checkboxes). Never re-raise a\n question the human deleted; if the human deleted the whole\n `## Open Questions` section, treat the plan as accepted and surface no new\n questions unless something is genuinely still blocking. Omit\n `## Open Questions` entirely once there are none left.\n\n Leave `<%= it.vars.requirementsFile %>` uncommitted and finish your turn.\n on:\n \"* **\": product-answer\n\n product-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.requirementsFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.requirementsFile %>` holds the product plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: technical-author\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n technical Q&A (**design.technical-author**).\n \"* **\":\n to: product-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.product-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n technical-author:\n actor: agent\n label: Refining the technical plan\n file: <%= it.vars.architectureFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.architectureFile %>` (write it) and\n `<%= it.vars.requirementsFile %>` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n If you developed those requirements earlier in this conversation, work\n from what you already settled; otherwise read\n `<%= it.vars.requirementsFile %>` (the converged product plan) yourself.\n Develop `<%= it.vars.architectureFile %>` from it in this one turn:\n file/module structure, data models, library/tech-stack choices,\n error-handling strategy — the *how*, building on the *what* already\n settled. Do not re-open product-level decisions; treat them as settled.\n\n For every open point you cannot settle yourself, add a question under a\n `## Open Questions` heading exactly as the grilling phase does: one\n `### <question>` each, followed by a checkbox list — TWO concrete options\n plus a final free-text slot, ALL left UNTICKED:\n\n ### <the question>\n\n - [ ] <first option>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself. On a RETURN lap the human has ticked one option\n per question (or replaced `_your answer_` with their own text): fold each\n chosen answer into the plan prose, then MOVE the question into a\n `## Answered Questions` section as `### <question>` + the resolved answer\n in plain PROSE (drop the checkboxes). Never re-raise a deleted question;\n treat a deleted `## Open Questions` section as acceptance. Omit\n `## Open Questions` once there are none left.\n\n Once `<%= it.vars.architectureFile %>` is written, delete\n `<%= it.vars.requirementsFile %>` — its content has been folded in and it\n must not linger. Leave everything uncommitted and finish your turn.\n on:\n \"* **\": technical-answer\n\n technical-answer:\n actor: human\n label: Awaiting your answers\n file: <%= it.vars.architectureFile %>\n mode: qa\n answerGate: true\n message: |\n `<%= it.vars.architectureFile %>` holds the technical plan under\n development. Each question under `## Open Questions` offers a few options\n plus a `- [ ] _your answer_` slot. Answer EVERY question by ticking\n exactly one box (`- [x]`); for your own answer, replace `_your answer_`\n with your text and tick that line. Stepping is refused while any question\n is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: decompose\n action: Accept answers\n describe: >-\n with no open questions left (the agent surfaced none, or you deleted\n the section and it finalized the plan), change nothing to move on to\n decomposing the work into packages (**design.decompose**).\n \"* **\":\n to: technical-author\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace `_your answer_` for\n your own) to send the plan back for the agent to fold your answers in\n (**design.technical-author**). Delete a question to skip it, or delete the whole\n `## Open Questions` section to accept the plan as-is.\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state files this turn touches\n are the package files it writes under `<%= it.vars.packagesDir %>/` and\n `<%= it.vars.architectureFile %>`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `<%= it.vars.architectureFile %>` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. Decompose it into an ordered\n set of WORK PACKAGES, one file each under\n `<%= it.vars.packagesDir %>/` (e.g. `<%= it.vars.packagesDir %>/01-name.md`,\n `<%= it.vars.packagesDir %>/02-name.md`, ...). Order the packages so the\n test suite can pass again after each one lands.\n\n Each package file describes a set of INDEPENDENT tasks that can be\n implemented concurrently without ordering constraints between them — a\n single build turn will fan them out to parallel subagents. Give each task\n a clear description, acceptance criteria as `- [ ]` checkboxes, and the\n relevant paths. Also state the package's overall requirement so it can be\n reviewed against its spec. Do NOT put ordering-dependent tasks in the same\n package — split them across ordered packages instead.\n\n No package file may reference any other `.gtd/` file. Once the package\n files are written, delete `<%= it.vars.architectureFile %>`. Leave\n everything uncommitted and finish your turn.\n on:\n \"* <%= it.vars.packagesDir %>/**\": $onPackages\n\n # ── ENCAPSULATION: the simple flow's plan-iteration loop ───────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `planning`\n # (its only prompt state).\n planLoop:\n model: <%= it.vars.plannerModel %>\n params: [onAccept]\n entry: planning\n states:\n planning:\n actor: agent\n label: Refining the plan\n file: <%= it.vars.todoFile %>\n mode: prose\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.todoFile %>`; develop it into the plan.\n Do not create other files to hold notes or output.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build — or, on a\n return lap, the plan you already wrote plus the human's edits and comments\n on it. Read it, explore the codebase, and develop it into a concrete\n implementation plan: the files to change, the approach, and the steps to\n take.\n\n Do NOT ask questions and do NOT add an \"Open Questions\" section — decide\n every open point yourself and write the plan as a set of concrete\n decisions. Where a decision is genuinely a judgement call, state the\n choice you made and, in one line, why. The human iterates on the plan by\n editing it directly, not by answering questions.\n\n On a return lap, `<%= it.vars.todoFile %>` carries the human's edits and\n inline comments on your previous plan. If you wrote that plan earlier in\n this conversation, work from what you already know plus these edits;\n otherwise read the file fresh. Fold every edit and comment into a\n revised, self-contained plan and remove the addressed comments — the plan\n is always the current, clean proposal, never a diff.\n\n Leave `<%= it.vars.todoFile %>` uncommitted and finish your turn — do\n not commit, and do not run `gtd land` yourself; the harness does\n that.\n on:\n \"* **\": await-plan\n\n await-plan:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.todoFile %>\n mode: prose\n message: |\n `<%= it.vars.todoFile %>` holds the proposed implementation plan. Read it,\n then either accept it as-is or edit it — rewrite a step, add a\n constraint, or drop an inline comment — to send it back for another round.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: $onAccept\n action: Accept plan\n describe: >-\n change nothing to move on to implementation (**build.building**).\n \"* **\":\n to: planning\n action: Revise plan\n describe: >-\n rewrite a step, add a constraint, or leave an inline comment to\n send it back for another round (**plan.planning**).\n\n # ── ENCAPSULATION: the shared review tail ──────────────────────────────────\n # ▸ planner identity — `model:` declared once here, stamped onto `reviewing`\n # and `collecting` (its only prompt states). The implementer's OWN\n # follow-through on feedback is a coder action, not a reviewer one — it\n # lives in `build.addressing` (`$onFeedback`), not inside this machine.\n humanReview:\n model: <%= it.vars.plannerModel %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # `gtd --entry review-gate.check` (a plain non-gtd branch, e.g. a\n # colleague's PR) does NOT enter here directly — it enters\n # `review-gate.check` first, which runs the green-baseline gate and,\n # once green, transitions here. The `Gtd-Review-Base:` trailer rides on that\n # entry commit (the process's oldest), so everything below still reuses\n # the same reviewing/await-review/feedback machinery over <commitish>..HEAD.\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.reviewFile %>`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the changes in\n this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one `- [ ] ./path/to/file.ts#42 — note` file\n pointer per hunk (checkboxes are for the human to tick, not you; every\n path is `./`-relative, with an optional `#line` and an optional\n ` — note`).\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the process rests here, gtd opens a \"review checkout window\": raw HEAD\n # is rewound to the review base (the most-recent `deciding` commit — so a\n # feedback round shows only `last-review..HEAD` — or the process start on\n # the first review) with the working tree untouched, surfacing the diff\n # as ordinary uncommitted changes. It closes on the next invocation.\n #\n # Every human step here routes to `deciding`, which decides sign-off vs.\n # feedback from the step's CONTENT (a comment, not a tick). Ticking a box\n # only records \"I reviewed this hunk\". Two dead ends can't be told apart by a\n # file-pattern edge, so the review-signoff guard (`src/StepGuards.ts`)\n # catches them BEFORE the step commits and refuses: an unfinished\n # review (a box still `- [ ]` with no comment) and a deleted REVIEW.md.\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — tick EVERY box and leave no comment — no note in\n `<%= it.vars.reviewFile %>`, no code edit — to collapse the process into\n one commit (**build.squashing**).\n - **Request changes** — leave a comment: a note on a\n `<%= it.vars.reviewFile %>` line, or a direct code edit — to send it to a\n build + re-review round (**review.deciding** → **build.addressing**). A\n code edit counts as your own fix: the agent completes the follow-through\n it implies and never reverts your lines.\n\n Stepping with a box still unticked and no comment is refused (finish\n reviewing first); so is deleting `<%= it.vars.reviewFile %>`.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: <%= it.vars.reviewFile %>\n mode: review\n # Anchors the INCREMENTAL review base: its most-recent in-process commit is\n # the point the previous review round ended, so the next `reviewing` turn\n # (and the review checkout window) covers only the changes since then. On\n # the first review there is no such commit, so the base is the process start.\n reviewBase: true\n script: |\n #!/usr/bin/env bash\n # gtd check turn — decide the human's review step from its CONTENT. HEAD is\n # the human's `await-review -> deciding` commit; HEAD^ is the `reviewing`\n # commit that wrote the agent's original REVIEW.md (the sign-off gate\n # refuses an incomplete step, so `deciding` is entered exactly once per\n # round — nothing commits in between). It is FEEDBACK when the human left\n # a comment: a note in REVIEW.md (any edit beyond a \"[ ]\"->\"[x]\" tick) OR a\n # hand-edit to any file this round other than `.gtd/` or the state's own\n # reviewFile (which is var-configurable and may live outside `.gtd/`, e.g.\n # a root-level REVIEW.md — issue #128). Otherwise it is a clean sign-off.\n # This turn only CAPTURES the raw material into REVIEW_RAW.md (the\n # `collecting` agent then turns it into instructions); it does NOT\n # interpret it. Either way, remove REVIEW.md; what the resulting diff\n # MEANS is decided by this state's own `on` rules — the A/M REVIEW_RAW.md\n # rows are declared BEFORE the D REVIEW.md row so a feedback round (which\n # also deletes REVIEW.md) is captured as feedback, not mistaken for a\n # sign-off.\n set +e\n # Hoist the paths once, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line — it\n # would glue the next line onto it and break the script. Below this point\n # everything is plain bash.\n stateDir=\"<%~ it.vars.stateDir %>\"\n reviewFile=\"<%~ it.vars.reviewFile %>\"\n reviewRawFile=\"<%~ it.vars.reviewRawFile %>\"\n head=$(git rev-parse HEAD)\n mkdir -p \"$(dirname \"$reviewRawFile\")\"\n # A note is any REVIEW.md change beyond a checkbox flip: normalize every\n # \"[ ]\"/\"[x]\" to a placeholder, then compare the agent's original (HEAD^)\n # against the human's version (HEAD).\n before=$(git show \"HEAD^:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n after=$(git show \"HEAD:$reviewFile\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\n # Did the human hand-edit anything this round besides stateDir and the\n # reviewFile itself (var-configurable — may live outside stateDir, so it\n # is excluded by exact path, not just the stateDir prefix)?\n if git diff-tree --no-commit-id --name-only -r HEAD \\\n | grep -v \"^$stateDir/\" | grep -vxF \"$reviewFile\" | grep -q . \\\n || [ \"$before\" != \"$after\" ]; then\n {\n echo \"Raw review material captured for classification. The human reviewed the changes and left the feedback below; a downstream agent turns it into an instruction list. This file is machine-captured input, not instructions — it names a commit, never inlined content.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in $reviewFile at this commit; their hand edits,\"\n echo \"if any, are in that commit's other paths. Run: git show $head\"\n } > \"$reviewRawFile\"\n fi\n rm -f \"$reviewFile\"\n on:\n \"A <%= it.vars.reviewRawFile %>\": collecting\n \"M <%= it.vars.reviewRawFile %>\": collecting\n \"D <%= it.vars.reviewFile %>\": $onSignoff\n\n collecting:\n actor: agent\n label: Collecting your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n prompt: |\n You are an autonomous coding agent turning raw review feedback into an\n explicit instruction list for a builder. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state files this\n turn touches are `<%= it.vars.reviewRawFile %>` (read it, then\n delete it) and `<%= it.vars.reviewFeedbackFile %>` (write it) — do\n NOT change any other file, you classify, you do not build.\n\n The raw review material is: <%~ it.read(it.vars.reviewRawFile) %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n Classify every piece of it into `<%= it.vars.reviewFeedbackFile %>` as a\n flat, numbered instruction list. Each item is `N. ./path/to/file.ts#line —\n <imperative>`. Apply these rules exactly:\n\n 1. NOTES the human added to REVIEW.md — each note is a MANDATORY change,\n regardless of whether its checkbox is ticked (ticking means only \"I read\n this hunk\", never sign-off). Anchor each to the `./path#line` its review\n item names (or the chunk it sits under). Emit it as a concrete\n imperative.\n 2. Every COMMENT the human ADDED this round (a `+` line that is a code\n comment, in the hand-edited files) is a MANDATORY instruction the human\n is giving the builder — even a plain-prose one, even if its box is\n ticked. Emit an imperative to satisfy it and note that the comment line\n itself must be REMOVED once addressed. If a comment turns out to describe\n behaviour the code already has, the instruction is still \"confirm the\n code matches and remove the comment line\".\n 3. Every NON-comment code line the human hand-edited is an already-committed\n INTENT, NOT a do-then-delete item. Emit an instruction that names those\n files and says: their lines are final — complete ONLY the follow-through\n they imply (callers, tests, types, docs) and NEVER revert or rewrite\n them. Do not turn their own edit into a task.\n\n Split each hand-edited hunk per line: comment `+` lines follow rule 2, the\n surrounding real code follows rule 3.\n\n If — and only if — nothing in the raw material is actionable (e.g. the human\n left only an approving remark), write `<%= it.vars.reviewFeedbackFile %>`\n containing exactly one line: `NOTHING ACTIONABLE — <one-sentence reason>`.\n Never invent work to satisfy a non-actionable remark.\n\n Delete `<%= it.vars.reviewRawFile %>`, leave everything else uncommitted,\n and finish your turn.\n on:\n \"A <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n \"M <%= it.vars.reviewFeedbackFile %>\": $onFeedback\n\n # ── ENCAPSULATION: the advanced flow's per-package agentic review ──────────\n # A single state: a machine whose entire job is to be a separate MIND from\n # the implementer, distinct from the coder identity that builds/fixes the\n # package (`packages.item.*`). ▸ planner identity — `model:` declared once\n # here, stamped onto `review` (its only prompt state). Fixing the reviewer's\n # own findings is a coder action, not a reviewer one — it lives in\n # `packages.item.fix-spec` (`$onFix`), not inside this machine.\n specReview:\n model: <%= it.vars.plannerModel %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # Bounded per process: once entered `max` times, a transition into it is\n # redirected to `$onApproved` (force-close the package and move on — the\n # unresolved concern is deferred to the shared human review at the tail,\n # which sees the whole diff anyway). NOTE the cap is PROCESS-scoped, so it\n # pools across packages — the same documented limitation the shared\n # `build.fix` cap has (see the README's \"Why\" section, \"Bounded, not\n # runaway\").\n retry:\n max: 3\n otherwise: $onApproved\n prompt: |\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec. This workflow steers itself through its own\n state files — treat them as its private scratchpad, never as project\n code or documentation. The only state file this turn touches is\n `<%= it.vars.specFeedbackFile %>`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(it.vars.nextFile) %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write NOTHING and finish your turn — a\n clean turn is your approval.\n - If it does not, write `<%= it.vars.specFeedbackFile %>` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A <%= it.vars.specFeedbackFile %>\": $onFix\n \"M <%= it.vars.specFeedbackFile %>\": $onFix\n \"D <%= it.vars.specFeedbackFile %>\": $onApproved\n \"C\": $onApproved\n\n # ── ENCAPSULATION: the advanced flow's per-package build queue ─────────────\n # Identity-free (a bookkeeping queue, not a builder) — the actual per-package\n # build identity lives in its own `item` child (packageItem, below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package queue arbiter) — the loop driver executes\n # this verbatim, then steps the check actor. Mechanics only: take the\n # first package file (by name) under the packages dir into NEXT.md, or\n # remove NEXT.md when the queue is empty. What NEXT.md's presence/absence\n # MEANS (another package to build vs. the queue closing out to review) is\n # decided by this state's own `on` rules at capture time — never here.\n set +e\n # Hoist the NEXT path once, at the TOP: Eta's autoTrim eats the newline\n # after an interpolation tag, so no tag may be the last token on a line —\n # it would glue the next line's `else`/`fi` onto it and break the script.\n # Below this point everything is plain bash.\n nextFile=\"<%~ it.vars.nextFile %>\"\n # Sweep the advanced flow's spent Q&A steering files: requirementsFile\n # is consumed by technical-qa, architectureFile by decompose — both are\n # gone by the time the package queue starts. No-op if already removed.\n requirements=\"<%~ it.vars.requirementsFile %>\"\n architecture=\"<%~ it.vars.architectureFile %>\"\n rm -f \"$requirements\" \"$architecture\"\n next=$(ls <%~ it.vars.packagesDir %>/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n mkdir -p \"$(dirname \"$nextFile\")\"\n printf '%s' \"$next\" > \"$nextFile\"\n else\n rm -f \"$nextFile\"\n fi\n on:\n \"D <%= it.vars.nextFile %>\": $onDrained\n \"* <%= it.vars.nextFile %>\": item.building\n \"C\": $onDrained\n\n # The per-package build identity — its own coder machine (below), so\n # deleting a package file's own lifecycle stays inside `item` while the\n # queue itself (`packages`) stays a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # ── ENCAPSULATION: the advanced flow's per-package build identity ──────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `fix-suite`, and `fix-spec` (its own prompt states). `closing` ends that\n # package file's own lifecycle; keeping it here (not in the queue) keeps\n # `packages` itself to a single state. `packages.item.closing` -> `picking`\n # is the ONE upward-resolving target in the whole file, so it MUST be a\n # param (`$onNext`, bound by `packages` — see the machine's own reference).\n packageItem:\n model: <%= it.vars.coderModel %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. This turn writes no state\n file: do NOT delete the package file (the spec-review gate reads\n it after you) and never touch `<%= it.vars.nextFile %>` — the\n `picking` state owns it.\n\n The package to implement is: <%~ it.read(it.vars.nextFile) %>\n\n That file describes a set of INDEPENDENT tasks. Implement ALL of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement EXACTLY the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: &suiteFixPrompt |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.feedbackFile %>`; fix it per its\n contents, or delete it if the feedback turns out to be wrong. Do not\n create other files to hold notes or output.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: <%= it.vars.specFeedbackFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation. The only state file this\n turn touches is `<%= it.vars.specFeedbackFile %>`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `<%= it.vars.specFeedbackFile %>` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(it.vars.nextFile) %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `<%= it.vars.specFeedbackFile %>` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env bash\n # gtd check turn (the package closer) — remove the just-reviewed package\n # file (its path is in NEXT.md) plus any leftover spec feedback, so\n # `picking` selects the NEXT package (or falls through to review when the\n # queue is empty). Reached on approval (spec review clean) and on the\n # spec review retry cap (force-close, deferring the concern to the tail).\n set +e\n pkg=$(cat <%~ it.vars.nextFile %> 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f <%~ it.vars.specFeedbackFile %> <%~ it.vars.nextFile %>\n on:\n \"* **\": $onNext\n\n # Per-package health pair: packages.item.health.check/.escalate ->\n # packages.item.spec.review (green) or packages.item.fix-suite (red).\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Per-package agentic review: packages.item.spec.review -> closing\n # (approved), or packages.item.fix-spec (findings — re-verify via the\n # full health check before re-reviewing).\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # ── ENCAPSULATION: the simple flow's build identity ────────────────────────\n # ▸ coder identity — `model:` declared once here, stamped onto `building`,\n # `squashing`, `addressing`, and `fix` (its own prompt states). Decomposition\n # (`design.decompose`) belongs to the planner tier now, not here — see\n # `advancedPlan` below. `addressing` is the implementer's OWN follow-through\n # on review feedback (a coder action) — reached via `$onFeedback`, bound at\n # its own `review:` reference below, not nested inside the (planner-only)\n # `humanReview` machine. `humanReview` is nested HERE (not at the root) so\n # the review tail's run stays inside the builder's own scope — see the\n # header comment.\n simpleBuild:\n model: <%= it.vars.coderModel %>\n params: [onDone]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n file: <%= it.vars.todoFile %>\n prompt: |\n You are an autonomous coding agent. This workflow steers itself through\n its own state files — treat them as its private scratchpad, never as\n project code or documentation. The only state file this turn touches is\n `<%= it.vars.todoFile %>`; delete it once its work is complete. Do not\n create other files to hold notes or output.\n\n The plan to implement is: <%~ it.read(it.vars.todoFile) %>\n\n Read that plan and implement it in full. Use TDD discipline: one test,\n then the implementation that passes it, then the next — never write all\n the tests first.\n\n Delete `<%= it.vars.todoFile %>` once the work it describes is complete\n and verified. Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n squashing:\n actor: agent\n label: Squashing\n file: <%= it.vars.commitMsgFile %>\n prompt: |\n You are an autonomous coding agent. The process is approved and done. This\n workflow steers itself through its own state files — treat them as its\n private scratchpad, never as project code or documentation. The only\n state file this turn touches is `<%= it.vars.commitMsgFile %>`; write\n it. Do not create other files to hold notes or output.\n\n Leave `<%= it.vars.commitMsgFile %>` uncommitted and finish your turn —\n entering this file squashes everything this process retains into one\n commit using its content as the message. This prompt carries no diff —\n if you built that range yourself earlier in this conversation, work from\n what you already know; otherwise read the range yourself. It runs from\n `<%= it.retainedBase %>` to the working tree, and is exactly what that\n commit will contain, the changes THIS process made (its trace/retry\n boundary onward). For a normal build\n process that is the whole feature; for a `gtd --entry review-gate.check`\n process it is only the fixes made during the review — NOT the changeset\n that was under review.\n\n If that range is non-empty, write `<%= it.vars.commitMsgFile %>` with ONE\n conventional-commits message for the changes (subject line\n `type(scope): subject`, imperative mood, <= 72 characters; a body\n explaining the why, trade-offs, and key decisions). Plain text, no\n markdown wrapper.\n\n If that range is empty (a clean review sign-off with no fixes), write\n `<%= it.vars.commitMsgFile %>` with exactly this one line and nothing\n else:\n\n chore: human review\n on:\n \"A <%= it.vars.commitMsgFile %>\": $onDone\n \"M <%= it.vars.commitMsgFile %>\": $onDone\n\n addressing:\n actor: agent\n label: Addressing your feedback\n file: <%= it.vars.reviewFeedbackFile %>\n # Refuse a work-free turn: a step whose only pending change is deleting the\n # instructions file (the original \"captured then discarded\" bug). The\n # feedback-progress guard (`src/StepGuards.ts`) reads this flag and\n # exempts a `NOTHING ACTIONABLE` sentinel (a non-actionable review round,\n # which legitimately makes no code change). The pure engine carries it inert.\n requireProgress: true\n prompt: |\n You are an autonomous coding agent addressing review feedback. This\n workflow steers itself through its own state files — treat them as\n its private scratchpad, never as project code or documentation. The\n only state file this turn touches is\n `<%= it.vars.reviewFeedbackFile %>`; address it, then delete it. Do\n not create other files to hold notes or output.\n\n The instructions to implement are: <%~ it.read(it.vars.reviewFeedbackFile) %>\n\n This is a flat instruction list, not a diff — execute every item exactly as\n written, no Q&A, no re-planning, using TDD discipline:\n\n - An item that says the human's own lines are final: build ON TOP of them to\n complete only the follow-through it names (callers, tests, types, docs).\n NEVER revert or rewrite the lines they already committed.\n - An item derived from a human comment: satisfy it AND delete that comment\n line from the code (the human's review comments are transient — none of\n them survive this round).\n\n If the file's sole content is a `NOTHING ACTIONABLE` line, make no code\n change; just delete the instructions file.\n\n Delete `<%= it.vars.reviewFeedbackFile %>` once every item is addressed.\n Leave everything else uncommitted and finish your turn. Deleting the\n instructions file WITHOUT making the changes it asks for is refused — the\n feedback must actually be addressed.\n on:\n \"* **\": health.check\n\n fix:\n actor: agent\n label: Fixing the check\n file: <%= it.vars.feedbackFile %>\n prompt: *suiteFixPrompt\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n # Shared health pair: build.health.check/.escalate -> build.review.reviewing\n # (green) or build.fix (red).\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # The shared review tail, nested HERE (not at the root) so its run stays\n # inside the builder's own scope across the feedback round-trip — see\n # the header comment. Sign-off -> squashing; feedback -> addressing (the\n # implementer's own coder-identity follow-through, not a reviewer action).\n review:\n machine: humanReview\n with:\n onSignoff: squashing\n onFeedback: addressing\n\n # The root machine — the whole \"advanced/simple\" flow tree described in\n # this file's header comment. WIRING ONLY: no identity of its own, just the\n # shared entry/exit states and the top-level machine tree.\n unified:\n entry: idle\n states:\n # ── Shared entry ────────────────────────────────────────────────────────\n idle:\n actor: human\n label: Idle\n # Every human-gate `message:` below closes with the same rendered \"What\n # each change does next\" list, built from `it.edges` (the resting state's\n # own `on` edges — each edge's `describe:` is the single source of truth).\n message: |\n No active gtd process.\n\n To start one, create a steering file describing what you want built:\n\n - `<%= it.vars.todoFile %>` — a short sketch — starts the SIMPLE flow\n (plan, build, review).\n - `<%= it.vars.requirementsFile %>` — the product requirements — starts\n the ADVANCED flow (product + technical Q&A, package decomposition,\n parallel build, per-package agentic review).\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* <%= it.vars.requirementsFile %>\":\n to: spec-gate\n action: Start advanced flow\n describe: >-\n save the product requirements to `.gtd/REQUIREMENTS.md` — gtd first\n checks the test baseline is green, then the agent develops them\n through product then technical Q&A, decomposes the work into\n packages, and builds each in parallel (**spec-gate.check**).\n \"* **\":\n to: plan-gate\n action: Start simple flow\n describe: >-\n save a short sketch to `.gtd/TODO.md` (or any other change) — gtd\n first checks the test baseline is green, then the agent reads it,\n explores the codebase, and develops it into a concrete\n implementation plan (**plan-gate.check**).\n\n # ── Green-baseline gate: `gtd --entry fix-precheck` entry ─────────────────\n # `gtd --entry fix-precheck` enters HERE: run the suite, then a red run\n # drops straight into the shared `build.fix` loop (-> build.health.check ->\n # review + squash tail); a green run is a no-op back to `idle` (nothing\n # to fix).\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A <%= it.vars.feedbackFile %>\": build.fix\n \"M <%= it.vars.feedbackFile %>\": build.fix\n \"D <%= it.vars.feedbackFile %>\": idle\n \"C\": idle\n\n # ── Shared tail: the finale ─────────────────────────────────────────────\n done:\n commit: |\n <%~ it.read(it.vars.commitMsgFile) %>\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n # ── Green-baseline gate: simple flow entry. ────────────────────────────────\n plan-gate:\n machine: entryGate\n with:\n onGreen: plan\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**plan-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: advanced flow entry. ──────────────────────────────\n spec-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**spec-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Green-baseline gate: `gtd --entry review-gate.check` entry -> build.review.reviewing. ─\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `<%= it.vars.feedbackFile %>` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n # ── Advanced flow: the design conversation (design.product-author/.product-answer ─\n # -> design.technical-author/.technical-answer -> design.decompose -> packages). ──\n design:\n machine: advancedPlan\n with:\n onPackages: packages\n\n # ── Simple flow plan-iteration loop (plan.planning/.await-plan -> build.building). ──\n plan:\n machine: planLoop\n with:\n onAccept: build.building\n\n # ── Advanced per-package build queue (packages.* -> build.review). ────────\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n # ── Simple flow's build identity (build.* -> build.review -> done). ───────\n build:\n machine: simpleBuild\n with:\n onPackages: packages\n onDone: done\n";
|
|
192158
192191
|
//#endregion
|
|
192159
192192
|
//#region src/workflows/templates.ts
|
|
192160
192193
|
/**
|
|
@@ -192798,6 +192831,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
192798
192831
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
192799
192832
|
isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
|
|
192800
192833
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
192834
|
+
gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
|
|
192801
192835
|
commitHistory: (base, head = "HEAD") => {
|
|
192802
192836
|
const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
|
|
192803
192837
|
const args = [
|
|
@@ -192908,6 +192942,79 @@ var CommandRunner = class CommandRunner extends Tag("CommandRunner")() {
|
|
|
192908
192942
|
}));
|
|
192909
192943
|
};
|
|
192910
192944
|
//#endregion
|
|
192945
|
+
//#region src/Sessions.ts
|
|
192946
|
+
/**
|
|
192947
|
+
* A conversation's session id is DERIVED, not remembered: `uuidv5(namespace,
|
|
192948
|
+
* memoryKey)`, where `memoryKey` (`memoryKeyFor` in `src/Edge.ts`) is already
|
|
192949
|
+
* a pure function of history — `<scope>#<anchor7>`, anchored to the commit
|
|
192950
|
+
* the current unbroken scope-run began from. Same scope-run → same key → same
|
|
192951
|
+
* id → the agent resumes its conversation; a new, unbroken entry into the
|
|
192952
|
+
* scope changes the anchor, hence the key, hence the id → a fresh
|
|
192953
|
+
* conversation. Nothing is written anywhere, so a peek (`gtd next --json`)
|
|
192954
|
+
* and a dispatch derive the exact same id — there is no table to keep in
|
|
192955
|
+
* sync, no write to race, no file to go stale.
|
|
192956
|
+
*
|
|
192957
|
+
* A prompt rest with no memory key (its state is absent from `stateScopes` —
|
|
192958
|
+
* possible only for a hand-built definition) keeps the old ephemeral
|
|
192959
|
+
* behaviour instead: a random id, `resume: false` — deriving something
|
|
192960
|
+
* stable there would resume forever across unrelated processes, and there is
|
|
192961
|
+
* no history to anchor a key to anyway.
|
|
192962
|
+
*
|
|
192963
|
+
* The crash edge: an agent turn that creates session X but lands no commit
|
|
192964
|
+
* (a crash, a killed driver) re-derives X on the next lap with the SAME
|
|
192965
|
+
* `resume: false` a fresh scope-run would report, so `claude --session-id X`
|
|
192966
|
+
* hits "id already in use" the second time around. `resolveSession` can't see
|
|
192967
|
+
* that from `memoryKey`/`resume` alone — the fix lives in the driver, which
|
|
192968
|
+
* must treat `resume` as a HINT rather than a contract: try the flag `resume`
|
|
192969
|
+
* points at first, and fall back to the other on failure —
|
|
192970
|
+
*
|
|
192971
|
+
* if [ "$resume" = true ]
|
|
192972
|
+
* then agent_turn --resume || agent_turn --session-id
|
|
192973
|
+
* else agent_turn --session-id || agent_turn --resume
|
|
192974
|
+
* fi
|
|
192975
|
+
*
|
|
192976
|
+
* — which also makes the inverse mismatch (`resume: true`, id gone: retention
|
|
192977
|
+
* expired, `~/.claude/projects` wiped) recover on its own, with no file to
|
|
192978
|
+
* delete.
|
|
192979
|
+
*/
|
|
192980
|
+
/** Fixed forever — every derived session id descends from this one constant. */
|
|
192981
|
+
const GTD_SESSION_NAMESPACE = "ca4be249-805e-41c1-8b6a-75a11c011e25";
|
|
192982
|
+
const toBytes = (uuid) => Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
192983
|
+
const toUuidString = (bytes) => {
|
|
192984
|
+
const hex = bytes.toString("hex");
|
|
192985
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
192986
|
+
};
|
|
192987
|
+
/**
|
|
192988
|
+
* A UUIDv5 (RFC 4122 §4.3): `sha1(namespaceBytes ‖ nameBytes)`, the first 16
|
|
192989
|
+
* bytes, with the version nibble forced to `5` and the variant bits forced to
|
|
192990
|
+
* `10xx`. Hand-rolled over `node:crypto`'s SHA-1 rather than a dependency, so
|
|
192991
|
+
* its correctness is checked directly against the RFC's own DNS-namespace
|
|
192992
|
+
* test vector in `Sessions.test.ts` — not against this implementation's own
|
|
192993
|
+
* output.
|
|
192994
|
+
*/
|
|
192995
|
+
const uuidv5 = (namespace, name) => {
|
|
192996
|
+
const bytes = createHash("sha1").update(toBytes(namespace)).update(name, "utf8").digest().subarray(0, 16);
|
|
192997
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
192998
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
192999
|
+
return toUuidString(bytes);
|
|
193000
|
+
};
|
|
193001
|
+
/**
|
|
193002
|
+
* Resolve the session id a `prompt` rest's turn should use: `memoryKey ===
|
|
193003
|
+
* undefined` mints an ephemeral id via `mint` (defaults to `randomUUID`) and
|
|
193004
|
+
* forces `resume: false` — there is no history to derive a stable id from.
|
|
193005
|
+
* Otherwise the id is `uuidv5(GTD_SESSION_NAMESPACE, memoryKey)` and `resume`
|
|
193006
|
+
* is passed through verbatim (the caller — `src/Edge.ts`'s
|
|
193007
|
+
* `memoryResumedFor` — already computed it from the same trace the key came
|
|
193008
|
+
* from).
|
|
193009
|
+
*/
|
|
193010
|
+
const resolveSession = (memoryKey, resume, mint = randomUUID) => memoryKey === void 0 ? {
|
|
193011
|
+
sessionId: mint(),
|
|
193012
|
+
resume: false
|
|
193013
|
+
} : {
|
|
193014
|
+
sessionId: uuidv5(GTD_SESSION_NAMESPACE, memoryKey),
|
|
193015
|
+
resume
|
|
193016
|
+
};
|
|
193017
|
+
//#endregion
|
|
192911
193018
|
//#region node_modules/eta/dist/index.mjs
|
|
192912
193019
|
var EtaError = class extends Error {
|
|
192913
193020
|
constructor(message) {
|
|
@@ -193473,13 +193580,6 @@ const withHistoryTrailer = (subject, hash) => `${subject}\n\n${HISTORY_TRAILER_P
|
|
|
193473
193580
|
* by a later `gtd restore` command to recover the pre-squash tip.
|
|
193474
193581
|
*/
|
|
193475
193582
|
const parseHistoryTrailer = (message) => HISTORY_TRAILER_RE.exec(message)?.[1];
|
|
193476
|
-
/**
|
|
193477
|
-
* Record the pre-squash tip so a later `gtd restore` can find it. A no-op
|
|
193478
|
-
* (the git call is skipped entirely) when `tipHash === startParentHash`: an
|
|
193479
|
-
* empty process — no turns were ever committed — has no turn chain worth
|
|
193480
|
-
* keeping.
|
|
193481
|
-
*/
|
|
193482
|
-
const retainHistory = (git, tipHash, startParentHash) => tipHash === startParentHash ? _void : git.updateRef(HISTORY_REF, tipHash);
|
|
193483
193583
|
/** A thin wrapper over `git.readRefOption(HISTORY_REF)`. */
|
|
193484
193584
|
const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
|
|
193485
193585
|
/**
|
|
@@ -193514,9 +193614,10 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
|
|
|
193514
193614
|
/**
|
|
193515
193615
|
* Bash-script builders mirroring `GitWriterOperations` (`src/Git.ts`) — one
|
|
193516
193616
|
* function per writer method, each returning the equivalent bash as a
|
|
193517
|
-
* `string` instead of executing it.
|
|
193518
|
-
*
|
|
193519
|
-
* `
|
|
193617
|
+
* `string` instead of executing it. THE emission path: every git write gtd
|
|
193618
|
+
* decides reaches the repository as one of these strings, assembled by
|
|
193619
|
+
* `src/Emit.ts` and run by an external driver — gtd itself never executes
|
|
193620
|
+
* them.
|
|
193520
193621
|
*
|
|
193521
193622
|
* Pure, like `src/PatternMachine.ts`: no git, no filesystem, no `Effect`, no
|
|
193522
193623
|
* IO of any kind. Every export is a plain, total function of its arguments.
|
|
@@ -193607,6 +193708,143 @@ const restoreStagedFrom = (source, paths) => {
|
|
|
193607
193708
|
].join("\n");
|
|
193608
193709
|
};
|
|
193609
193710
|
//#endregion
|
|
193711
|
+
//#region src/OutcomeScript.ts
|
|
193712
|
+
/**
|
|
193713
|
+
* The human-facing wording for what a `gtd`-emitted script just landed —
|
|
193714
|
+
* printed by the DRIVER's bash when it runs `required`/`optional`, authored
|
|
193715
|
+
* once here in TS. Pure, like `src/GitScript.ts`: no git, no filesystem, no
|
|
193716
|
+
* `Effect`. `OUTCOME_PREAMBLE` defines the bash functions
|
|
193717
|
+
* (`gtd_report_transition`/`gtd_report_commit`/`gtd_report_note`/
|
|
193718
|
+
* `gtd_report_abandoned`/`gtd_report_restored`) a script calls; the builders
|
|
193719
|
+
* below emit the one-line calls, every argument routed through
|
|
193720
|
+
* `src/GitScript.ts`'s `shellQuote`.
|
|
193721
|
+
*
|
|
193722
|
+
* `noopText`/`abandonNoopText` render the shared no-op wording; both reach the
|
|
193723
|
+
* driver only through an emitted script now (`noteOutcome(noopText(state))`
|
|
193724
|
+
* for a genuine `gtd land` no-op, `abandonNoopOutcome` for `gtd abandon` with
|
|
193725
|
+
* nothing underway) — a no-op's script always prints itself, exactly like
|
|
193726
|
+
* every other outcome, rather than `src/program.ts` special-casing an empty
|
|
193727
|
+
* script. Their format strings live once here (`FMT_*`, with
|
|
193728
|
+
* `%s` placeholders) and reach both sides through `renderFormat` (the plain
|
|
193729
|
+
* path) and `printfLine` (the bash `printf` call embedded in
|
|
193730
|
+
* `OUTCOME_PREAMBLE`), so the two can never drift.
|
|
193731
|
+
*/
|
|
193732
|
+
/** Substitute `args` into `fmt`'s `%s` placeholders, in order — the plain-text twin of `printfLine`. */
|
|
193733
|
+
const renderFormat = (fmt, ...args) => {
|
|
193734
|
+
let i = 0;
|
|
193735
|
+
return fmt.replace(/%s/g, () => args[i++] ?? "");
|
|
193736
|
+
};
|
|
193737
|
+
/**
|
|
193738
|
+
* A `printf '<fmt>' <args...>` bash statement — `args` are already-valid bash
|
|
193739
|
+
* tokens (a variable reference like `"$from"`), never re-quoted here: a value
|
|
193740
|
+
* reaches the script as a printf ARGUMENT, never interpolated into the format
|
|
193741
|
+
* itself, so a subject or state name containing `%` can never be read as a
|
|
193742
|
+
* conversion spec.
|
|
193743
|
+
*/
|
|
193744
|
+
const printfLine = (fmt, args) => `printf ${shellQuote(fmt)} ${args.join(" ")}`;
|
|
193745
|
+
const FMT_NOOP = "nothing to do at \"%s\"\n";
|
|
193746
|
+
const FMT_ABANDON_NOOP = "no gtd process is underway (resting at \"%s\") — nothing to abandon\n";
|
|
193747
|
+
const FMT_ABANDONED = "abandoned the process resting at \"%s\" — HEAD is back at %s (\"%s\"), resting at \"%s\".\nEverything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n";
|
|
193748
|
+
const FMT_RESTORED = "restored the retained history — HEAD is back at %s (\"%s\"), resting at \"%s\". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n";
|
|
193749
|
+
/** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's `gtd_report_note` carries. */
|
|
193750
|
+
const noopText = (state) => renderFormat(FMT_NOOP, state);
|
|
193751
|
+
/** The initial-state collapse's own line — no commit landed, HEAD was rewound
|
|
193752
|
+
* to the process's start parent. Trailing newline, exactly like `noopText`:
|
|
193753
|
+
* printed verbatim by `program.ts` and via `noteOutcome`/`gtd_report_note` by
|
|
193754
|
+
* the emitted script. */
|
|
193755
|
+
const COLLAPSED_TEXT = "nothing to retain — rewound to the commit before the process started\n";
|
|
193756
|
+
/** `no gtd process is underway (resting at "<initial>") — nothing to abandon` — `gtd abandon`'s no-op plain-text line. */
|
|
193757
|
+
const abandonNoopText = (initial) => renderFormat(FMT_ABANDON_NOOP, initial);
|
|
193758
|
+
/**
|
|
193759
|
+
* The bash preamble every outcome-carrying script includes, ONE block (no
|
|
193760
|
+
* blank lines, like `src/Emit.ts`'s `RETRY_HELPER`) so `assembleScript`'s
|
|
193761
|
+
* blank-line-joined sections stay intact. First line is a literal comment
|
|
193762
|
+
* naming this module, so a reader (or the recognizer) can tell at a glance
|
|
193763
|
+
* where the block comes from.
|
|
193764
|
+
*
|
|
193765
|
+
* The palette/marker detection follows the `NO_COLOR` convention
|
|
193766
|
+
* (`[ -t 1 ] && [ -z "${NO_COLOR:-}" ]`) with the same plain-mode fallback
|
|
193767
|
+
* strings (`->`/`[commit]`/`...`) the now-deleted bash `bin/gtd` used to
|
|
193768
|
+
* render, so existing `@live` assertions on plain output kept matching once
|
|
193769
|
+
* this preamble took over printing it. `gtd_files` is the diff-tree read
|
|
193770
|
+
* (with `--root` so a repository's very first commit shows its files too)
|
|
193771
|
+
* plus the 3-row cap, fused into one function, since a script-side caller
|
|
193772
|
+
* always has the commit, never a pre-fetched file list.
|
|
193773
|
+
*/
|
|
193774
|
+
const OUTCOME_PREAMBLE = [
|
|
193775
|
+
"# gtd: human-facing outcome rendering (see src/OutcomeScript.ts)",
|
|
193776
|
+
"if [ -t 1 ] && [ -z \"${NO_COLOR:-}\" ]; then",
|
|
193777
|
+
" gtd_c_reset=$'\\e[0m'; gtd_c_bold=$'\\e[1m'; gtd_c_dim=$'\\e[2m'",
|
|
193778
|
+
" gtd_c_cyan=$'\\e[36m'; gtd_c_green=$'\\e[32m'",
|
|
193779
|
+
" gtd_m_trans=\"➡️\"; gtd_m_commit=\"✅\"; gtd_m_ellipsis=\"…\"",
|
|
193780
|
+
"else",
|
|
193781
|
+
" gtd_c_reset=\"\"; gtd_c_bold=\"\"; gtd_c_dim=\"\"",
|
|
193782
|
+
" gtd_c_cyan=\"\"; gtd_c_green=\"\"",
|
|
193783
|
+
" gtd_m_trans=\"->\"; gtd_m_commit=\"[commit]\"; gtd_m_ellipsis=\"...\"",
|
|
193784
|
+
"fi",
|
|
193785
|
+
"gtd_files() {",
|
|
193786
|
+
" local sha=\"$1\" f shown=0 total=0 files",
|
|
193787
|
+
" files=\"$(git diff-tree --no-commit-id --name-only -r --root \"$sha\" 2>/dev/null || true)\"",
|
|
193788
|
+
" [ -z \"$files\" ] && return 0",
|
|
193789
|
+
" while IFS= read -r f; do [ -n \"$f\" ] && total=$((total + 1)); done < <(printf '%s\\n' \"$files\")",
|
|
193790
|
+
" while IFS= read -r f; do",
|
|
193791
|
+
" [ -z \"$f\" ] && continue",
|
|
193792
|
+
" if [ \"$shown\" -ge 3 ]; then",
|
|
193793
|
+
" printf ' %s%s (%d more)%s\\n' \"$gtd_c_dim\" \"$gtd_m_ellipsis\" \"$((total - 3))\" \"$gtd_c_reset\"",
|
|
193794
|
+
" break",
|
|
193795
|
+
" fi",
|
|
193796
|
+
" printf ' %s%s%s\\n' \"$gtd_c_dim\" \"$f\" \"$gtd_c_reset\"",
|
|
193797
|
+
" shown=$((shown + 1))",
|
|
193798
|
+
" done < <(printf '%s\\n' \"$files\")",
|
|
193799
|
+
"}",
|
|
193800
|
+
"gtd_report_transition() {",
|
|
193801
|
+
" local from=\"$1\" to=\"$2\"",
|
|
193802
|
+
" printf '%s %s%s → %s%s\\n' \"$gtd_m_trans\" \"${gtd_c_bold}${gtd_c_cyan}\" \"$from\" \"$to\" \"$gtd_c_reset\"",
|
|
193803
|
+
" gtd_files HEAD",
|
|
193804
|
+
"}",
|
|
193805
|
+
"gtd_report_commit() {",
|
|
193806
|
+
" local subject=\"$1\"",
|
|
193807
|
+
" printf '%s %s%s%s\\n' \"$gtd_m_commit\" \"$gtd_c_green\" \"$subject\" \"$gtd_c_reset\"",
|
|
193808
|
+
" gtd_files HEAD",
|
|
193809
|
+
"}",
|
|
193810
|
+
"gtd_report_note() {",
|
|
193811
|
+
" printf '%s\\n' \"$1\"",
|
|
193812
|
+
"}",
|
|
193813
|
+
"gtd_report_abandoned() {",
|
|
193814
|
+
" local from=\"$1\" head=\"$2\" state=\"$3\" short subject",
|
|
193815
|
+
" short=\"$(git rev-parse --short \"$head\")\"",
|
|
193816
|
+
" subject=\"$(git log -1 --format=%s \"$head\")\"",
|
|
193817
|
+
` ${printfLine(FMT_ABANDONED, [
|
|
193818
|
+
"\"$from\"",
|
|
193819
|
+
"\"$short\"",
|
|
193820
|
+
"\"$subject\"",
|
|
193821
|
+
"\"$state\""
|
|
193822
|
+
])}`,
|
|
193823
|
+
"}",
|
|
193824
|
+
"gtd_report_restored() {",
|
|
193825
|
+
" local to=\"$1\" state=\"$2\" short subject",
|
|
193826
|
+
" short=\"$(git rev-parse --short \"$to\")\"",
|
|
193827
|
+
" subject=\"$(git log -1 --format=%s \"$to\")\"",
|
|
193828
|
+
` ${printfLine(FMT_RESTORED, [
|
|
193829
|
+
"\"$short\"",
|
|
193830
|
+
"\"$subject\"",
|
|
193831
|
+
"\"$state\""
|
|
193832
|
+
])}`,
|
|
193833
|
+
"}"
|
|
193834
|
+
].join("\n");
|
|
193835
|
+
/** `gtd_report_transition <from> <to>` — a landed self-loop/target-change transition, with its changed-file rows. */
|
|
193836
|
+
const transitionOutcome = (from, to) => `gtd_report_transition ${shellQuote(from)} ${shellQuote(to)}`;
|
|
193837
|
+
/** `gtd_report_commit <subject>` — a bare capture or a squash's final commit, with its changed-file rows. */
|
|
193838
|
+
const commitOutcome = (subject) => `gtd_report_commit ${shellQuote(subject)}`;
|
|
193839
|
+
/** `gtd_report_note <text>` — one already-rendered plain line, no marker. */
|
|
193840
|
+
const noteOutcome = (text) => `gtd_report_note ${shellQuote(text)}`;
|
|
193841
|
+
/** `gtd_report_abandoned <from> <head> <state>` — resolves the post-hoc short hash/subject from `head` (a commitish) in-script. */
|
|
193842
|
+
const abandonedOutcome = (from, head, state) => `gtd_report_abandoned ${shellQuote(from)} ${shellQuote(head)} ${shellQuote(state)}`;
|
|
193843
|
+
/** `gtd abandon`'s no-op outcome — the same wording `abandonNoopText` renders, printed via `gtd_report_note`. */
|
|
193844
|
+
const abandonNoopOutcome = (initial) => noteOutcome(abandonNoopText(initial));
|
|
193845
|
+
/** `gtd_report_restored <to> <state>` — resolves the post-hoc short hash/subject from `to` (a commitish) in-script. */
|
|
193846
|
+
const restoredOutcome = (to, state) => `gtd_report_restored ${shellQuote(to)} ${shellQuote(state)}`;
|
|
193847
|
+
//#endregion
|
|
193610
193848
|
//#region src/Emit.ts
|
|
193611
193849
|
/**
|
|
193612
193850
|
* Assembles the two scripts a `gtd next`-time decision hands the external
|
|
@@ -193616,7 +193854,11 @@ const restoreStagedFrom = (source, paths) => {
|
|
|
193616
193854
|
* nothing but that view). Pure, like `src/GitScript.ts`: no git, no
|
|
193617
193855
|
* filesystem, no `Effect`. Every input string (a `GitScript.ts` builder's
|
|
193618
193856
|
* output, a steering mode's already-rendered `format:`/`validate:` command)
|
|
193619
|
-
* arrives already rendered — this module only concatenates and wraps
|
|
193857
|
+
* arrives already rendered — this module only concatenates and wraps: once
|
|
193858
|
+
* for a `gitWrite` step's index-lock retry (`gtd_retry`), and once for a
|
|
193859
|
+
* `command` step's `onFailure` fix prompt (`failurePromptWrapper`), which
|
|
193860
|
+
* prints a ready-to-send instruction plus the command's captured output
|
|
193861
|
+
* ahead of propagating its exit code.
|
|
193620
193862
|
*
|
|
193621
193863
|
* Both halves are meant to stand alone: a driver may run either, both, or
|
|
193622
193864
|
* only `required`, and a person could paste either into a terminal. That's
|
|
@@ -193668,6 +193910,29 @@ const reviewWindowAssertion = (ref, expectedHash) => {
|
|
|
193668
193910
|
return `[ "$(git rev-parse --verify --quiet ${refQ} 2>/dev/null)" = ${hashQ} ] || { printf 'gtd: review window ref %s changed since this script was generated (expected %s) — re-run gtd\\n' ${refQ} ${hashQ} >&2; exit 1; }`;
|
|
193669
193911
|
};
|
|
193670
193912
|
/**
|
|
193913
|
+
* Wraps `command` so that a non-zero exit prints `prompt` (the ready-to-send
|
|
193914
|
+
* fix instruction), a blank line, then the command's own captured combined
|
|
193915
|
+
* output, before propagating the command's exit code — rather than letting
|
|
193916
|
+
* that output escape raw. The `{ … }` GROUP (not a bare command
|
|
193917
|
+
* substitution) is what lets a MULTI-LINE `command` be inlined verbatim
|
|
193918
|
+
* inside it; the assignment sits on the left of `||`, so `set -e` never trips
|
|
193919
|
+
* on the failing command itself. Exported (alongside `headAssertion`) solely
|
|
193920
|
+
* so `src/testing/EmittedScriptRecognizer.ts` can re-derive and
|
|
193921
|
+
* string-compare this exact shape.
|
|
193922
|
+
*/
|
|
193923
|
+
const failurePromptWrapper = (command, prompt) => {
|
|
193924
|
+
return [
|
|
193925
|
+
`gtd_validate_status=0`,
|
|
193926
|
+
`gtd_validate_out="$( {`,
|
|
193927
|
+
command,
|
|
193928
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
193929
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
193930
|
+
` printf '%s\\n\\n%s\\n' ${shellQuote(prompt)} "$gtd_validate_out"`,
|
|
193931
|
+
` exit "$gtd_validate_status"`,
|
|
193932
|
+
`fi`
|
|
193933
|
+
].join("\n");
|
|
193934
|
+
};
|
|
193935
|
+
/**
|
|
193671
193936
|
* `gtd_retry` takes ONE already-`shellQuote`d command string and `eval`s it,
|
|
193672
193937
|
* mirroring `src/Git.ts`'s `withIndexLockRetry`: retry ONLY on the same two
|
|
193673
193938
|
* substrings `isIndexLockError` matches, with jittered exponential backoff
|
|
@@ -193706,13 +193971,18 @@ const RETRY_HELPER = [
|
|
|
193706
193971
|
` done`,
|
|
193707
193972
|
`}`
|
|
193708
193973
|
].join("\n");
|
|
193709
|
-
const renderStep = (step) =>
|
|
193974
|
+
const renderStep = (step) => {
|
|
193975
|
+
if (step.kind === "gitWrite") return `gtd_retry ${shellQuote(step.command)}`;
|
|
193976
|
+
if (step.kind === "command" && step.onFailure !== void 0) return failurePromptWrapper(step.command, step.onFailure);
|
|
193977
|
+
return step.command;
|
|
193978
|
+
};
|
|
193710
193979
|
const assembleScript = (preconditions, steps) => {
|
|
193711
193980
|
if (steps.length === 0) return "";
|
|
193712
193981
|
const sections = ["set -euo pipefail"];
|
|
193713
193982
|
if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
|
|
193714
193983
|
if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
|
|
193715
193984
|
if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
|
|
193985
|
+
if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
|
|
193716
193986
|
sections.push(...steps.map(renderStep));
|
|
193717
193987
|
return sections.join("\n\n");
|
|
193718
193988
|
};
|
|
@@ -193728,6 +193998,32 @@ const emitScripts = (preconditions, required = [], optional = []) => ({
|
|
|
193728
193998
|
required: assembleScript(preconditions, required),
|
|
193729
193999
|
optional: assembleScript(preconditions, optional)
|
|
193730
194000
|
});
|
|
194001
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — pipe it into `bash` to land the turn";
|
|
194002
|
+
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
194003
|
+
const PRESENTATION_FAILURE_WARNING = "echo \"gtd: presentation-only follow-up failed — continuing\" >&2";
|
|
194004
|
+
/**
|
|
194005
|
+
* A plain-text (no `--json`) write command's single pasteable script: `gtd
|
|
194006
|
+
* land | bash` lands the turn without ever separating `required` from
|
|
194007
|
+
* `optional`. `required` runs verbatim (its own `set -euo pipefail` aborts
|
|
194008
|
+
* before `optional` ever starts if it fails); `optional`, when non-empty, is
|
|
194009
|
+
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
194010
|
+
* must never turn a landed turn into a non-zero exit. Empty when `required`
|
|
194011
|
+
* is — unreachable in practice today (every caller's `required` is always
|
|
194012
|
+
* non-empty, even a genuine no-op's print-only note), kept as the function's
|
|
194013
|
+
* own total behavior rather than assumed by every caller.
|
|
194014
|
+
*/
|
|
194015
|
+
const combinedScript = (required, optional) => {
|
|
194016
|
+
if (required.length === 0) return "";
|
|
194017
|
+
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`;
|
|
194018
|
+
return [
|
|
194019
|
+
DID_NOT_RUN_COMMENT,
|
|
194020
|
+
"",
|
|
194021
|
+
required,
|
|
194022
|
+
"",
|
|
194023
|
+
PRESENTATION_ONLY_COMMENT,
|
|
194024
|
+
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`
|
|
194025
|
+
].join("\n");
|
|
194026
|
+
};
|
|
193731
194027
|
//#endregion
|
|
193732
194028
|
//#region src/Edge.ts
|
|
193733
194029
|
/**
|
|
@@ -193810,8 +194106,8 @@ const parseCostTrailers = (messages) => {
|
|
|
193810
194106
|
const REVIEW_BASE_TRAILER_PREFIX = "Gtd-Review-Base: ";
|
|
193811
194107
|
const REVIEW_BASE_TRAILER_RE = /^Gtd-Review-Base:[ \t]*(\S+)[ \t]*$/m;
|
|
193812
194108
|
/**
|
|
193813
|
-
* The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd
|
|
193814
|
-
*
|
|
194109
|
+
* The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd --entry <state>`
|
|
194110
|
+
* entry commit (see `withEntryTrailers`), or `undefined`
|
|
193815
194111
|
* when `message` carries none. Read back by `computeProcessRun` — ONLY off
|
|
193816
194112
|
* the process's first (oldest) commit — to override the run's diff base.
|
|
193817
194113
|
*/
|
|
@@ -193906,6 +194202,27 @@ const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries
|
|
|
193906
194202
|
path: e.path
|
|
193907
194203
|
}))));
|
|
193908
194204
|
/**
|
|
194205
|
+
* `ProcessRun.headTurn` from an already-fetched `commitHistory` array
|
|
194206
|
+
* (oldest→newest) — HEAD is its last entry. `undefined` for an empty
|
|
194207
|
+
* history, an unparseable/foreign subject, or a subject naming a state the
|
|
194208
|
+
* ACTIVE definition doesn't declare (removed by a workflow change) or that
|
|
194209
|
+
* IS a commit state (never a real rest — see `resolveState`'s own doc
|
|
194210
|
+
* comment on why a commit-state subject is never trusted at face value).
|
|
194211
|
+
*/
|
|
194212
|
+
const headTurnFrom = (def, history) => {
|
|
194213
|
+
if (history.length === 0) return void 0;
|
|
194214
|
+
const head = history[history.length - 1];
|
|
194215
|
+
const parsed = parseStateSubject(subjectOf(head.message));
|
|
194216
|
+
if (parsed === void 0) return void 0;
|
|
194217
|
+
const stateDef = def.states[parsed.state];
|
|
194218
|
+
if (stateDef === void 0 || isCommitState(stateDef)) return void 0;
|
|
194219
|
+
return {
|
|
194220
|
+
state: parsed.state,
|
|
194221
|
+
actor: parsed.actor,
|
|
194222
|
+
empty: head.touched.length === 0
|
|
194223
|
+
};
|
|
194224
|
+
};
|
|
194225
|
+
/**
|
|
193909
194226
|
* The `Gtd-Review-Base:`/`Gtd-Var:` overrides carried by the process's OLDEST
|
|
193910
194227
|
* commit (its entry commit, when `planEntry` started this process) — a later
|
|
193911
194228
|
* turn's message is never mistaken for it. `{reviewBase: undefined, vars:
|
|
@@ -193958,7 +194275,8 @@ const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
|
|
|
193958
194275
|
diffBase: EMPTY_TREE$2,
|
|
193959
194276
|
trace: [],
|
|
193960
194277
|
costEntries: [],
|
|
193961
|
-
entryVars: {}
|
|
194278
|
+
entryVars: {},
|
|
194279
|
+
headTurn: void 0
|
|
193962
194280
|
};
|
|
193963
194281
|
const initialState = initialStateOf(def);
|
|
193964
194282
|
const history = yield* git.commitHistory(void 0, head);
|
|
@@ -193984,7 +194302,8 @@ const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
|
|
|
193984
194302
|
diffBase: reviewBaseOverride ?? startParentHash,
|
|
193985
194303
|
trace,
|
|
193986
194304
|
costEntries,
|
|
193987
|
-
entryVars
|
|
194305
|
+
entryVars,
|
|
194306
|
+
headTurn: headTurnFrom(def, history)
|
|
193988
194307
|
};
|
|
193989
194308
|
});
|
|
193990
194309
|
/**
|
|
@@ -194053,6 +194372,35 @@ const memoryKeyFor = (scopes, rest, run) => {
|
|
|
194053
194372
|
const token = entryIndex <= 0 ? run.startParentHash : run.trace[entryIndex - 1].hash;
|
|
194054
194373
|
return `${scope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`;
|
|
194055
194374
|
};
|
|
194375
|
+
/**
|
|
194376
|
+
* True iff a `prompt` rest in the CURRENT unbroken scope-run has already been
|
|
194377
|
+
* passed — the derivation `src/Sessions.ts`'s `resolveSession` takes its
|
|
194378
|
+
* `resume` flag from. `false` for any non-`prompt` rest, or when `rest.state`
|
|
194379
|
+
* resolves no scope at all (mirrors `memoryKeyFor`'s own two escape hatches).
|
|
194380
|
+
*
|
|
194381
|
+
* Computed over the process's rests, oldest→newest, with the process's own
|
|
194382
|
+
* starting state (`initialStateOf(def)`) prefixed: `run.trace`'s last entry is
|
|
194383
|
+
* already the current rest (a `Rest` is a snapshot of a fully-resolved state,
|
|
194384
|
+
* never an in-flight turn), so prefixing the ONE state before the trace began
|
|
194385
|
+
* is all that's missing. The prefix matters for a workflow whose
|
|
194386
|
+
* `entries.default` IS a prompt state — without it, the very first turn there
|
|
194387
|
+
* would have no predecessor to look back at.
|
|
194388
|
+
*
|
|
194389
|
+
* The root scope (`scope === ""`) matches every state, so a workflow like
|
|
194390
|
+
* `idle → working` puts `idle` inside `working`'s scope-run — the very first
|
|
194391
|
+
* agent beat of a process would otherwise claim `resume: true` for an id
|
|
194392
|
+
* nobody ever created. Filtering the "prior rests" slice down to `prompt`-kind
|
|
194393
|
+
* states (not merely non-empty) closes that hole: a message state like `idle`
|
|
194394
|
+
* never counts as a prior conversation turn.
|
|
194395
|
+
*/
|
|
194396
|
+
const memoryResumedFor = (def, scopes, rest, run) => {
|
|
194397
|
+
if (contentKindOf(rest.stateDef) !== "prompt") return false;
|
|
194398
|
+
const rests = [initialStateOf(def), ...run.trace.map((entry) => entry.state)];
|
|
194399
|
+
const resolved = memoryScopeAt(scopes, rest.state, rests);
|
|
194400
|
+
if (resolved === void 0) return false;
|
|
194401
|
+
const { entryIndex } = resolved;
|
|
194402
|
+
return rests.slice(Math.max(entryIndex, 0), rests.length - 1).some((state) => contentKindOf(def.states[state]) === "prompt");
|
|
194403
|
+
};
|
|
194056
194404
|
const PREFIX = "GTD_";
|
|
194057
194405
|
/**
|
|
194058
194406
|
* Assemble the merged `it.vars` map every template sees, from four layers
|
|
@@ -194060,7 +194408,7 @@ const PREFIX = "GTD_";
|
|
|
194060
194408
|
* (`ConfigOperations.workflowVars`), the top-level `.gtdrc` `vars:` key
|
|
194061
194409
|
* (`ConfigOperations.rcVars`), the current process's entry commit's
|
|
194062
194410
|
* `Gtd-Var:` trailers (`ProcessRun.entryVars` — fixed overrides recorded at
|
|
194063
|
-
* the moment a process like `gtd
|
|
194411
|
+
* the moment a process like `gtd --entry <state>` started it),
|
|
194064
194412
|
* and — for each name declared by any of those three layers — a
|
|
194065
194413
|
* `GTD_<UPPERCASE-name>` environment variable, if defined. Unlike the first
|
|
194066
194414
|
* three layers, the environment can only OVERRIDE a name some earlier layer
|
|
@@ -194238,6 +194586,7 @@ const restAt = (ref) => gen(function* () {
|
|
|
194238
194586
|
const changes = yield* pendingChanges(git, windowHead);
|
|
194239
194587
|
const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, hasCommits);
|
|
194240
194588
|
const memory = memoryKeyFor(config.stateScopes, resolved, run);
|
|
194589
|
+
const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
|
|
194241
194590
|
const hints = yield* renderHints(resolved.stateDef, context);
|
|
194242
194591
|
return {
|
|
194243
194592
|
...resolved,
|
|
@@ -194247,6 +194596,7 @@ const restAt = (ref) => gen(function* () {
|
|
|
194247
194596
|
stepDef,
|
|
194248
194597
|
changes,
|
|
194249
194598
|
memory,
|
|
194599
|
+
memoryResumed,
|
|
194250
194600
|
hints,
|
|
194251
194601
|
context
|
|
194252
194602
|
};
|
|
@@ -194305,11 +194655,39 @@ const renderRest = (rest) => gen(function* () {
|
|
|
194305
194655
|
content,
|
|
194306
194656
|
...rest.hints,
|
|
194307
194657
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
194658
|
+
memoryResumed: rest.memoryResumed,
|
|
194308
194659
|
edges: rest.context.edges
|
|
194309
194660
|
};
|
|
194310
194661
|
});
|
|
194311
|
-
/**
|
|
194312
|
-
|
|
194662
|
+
/**
|
|
194663
|
+
* Derived stall detection: PURE, no `Effect` — a plain fold over `rest`'s
|
|
194664
|
+
* already-resolved snapshot, restart-proof by construction (any process that
|
|
194665
|
+
* re-resolves the same HEAD reaches the same verdict). `true` iff ALL of:
|
|
194666
|
+
*
|
|
194667
|
+
* - the working tree is clean (a dirty tree is always an in-progress turn,
|
|
194668
|
+
* never a stall — this is the recovery story: a human fixing the cause out
|
|
194669
|
+
* of band clears the stall by touching the repo);
|
|
194670
|
+
* - HEAD's own commit already rests AT `rest.state` (`run.headTurn.state ===
|
|
194671
|
+
* rest.state` — the previous dispatch's attempt landed here) and that
|
|
194672
|
+
* commit's diff was EMPTY (`run.headTurn.empty`);
|
|
194673
|
+
* - ANOTHER dispatch right now would just repeat the same fruitless attempt
|
|
194674
|
+
* (`wouldAttempt` — false once a retry cap would redirect it elsewhere
|
|
194675
|
+
* instead, which is the escalation path OUT of a stall).
|
|
194676
|
+
*
|
|
194677
|
+
* Sticky by design (decision 4): resolving `true` twice in a row is the
|
|
194678
|
+
* correct, restart-proof answer, not a bug — the marker it replaces used to
|
|
194679
|
+
* consume itself on report; this doesn't, because there is no longer any
|
|
194680
|
+
* mutation to gate a "one-shot" report on.
|
|
194681
|
+
*/
|
|
194682
|
+
const stalledAt = (rest) => rest.changes.length === 0 && rest.run.headTurn?.state === rest.state && rest.run.headTurn.empty && rest.run.headTurn.actor === rest.actor && wouldAttempt(rest.stepDef, rest.state, rest.run.trace.map((entry) => entry.state));
|
|
194683
|
+
/**
|
|
194684
|
+
* The user-facing message for a `land` refusal — out-of-turn names the
|
|
194685
|
+
* awaited actor, no-match names every declared pattern. `land` derives its
|
|
194686
|
+
* invoker from `rest.actor` itself (see `planStep`), so out-of-turn is
|
|
194687
|
+
* unreachable by construction there; this branch stays reachable only via
|
|
194688
|
+
* `PatternMachine.step`'s own tests (a defensive message beats a lie).
|
|
194689
|
+
*/
|
|
194690
|
+
const formatStepRefusal = (refusal) => refusal.reason === "out-of-turn" ? `gtd land: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd land: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
|
|
194313
194691
|
/**
|
|
194314
194692
|
* Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
|
|
194315
194693
|
* own resting one, with a step's `cost`/`model` folded in. A squash's commit
|
|
@@ -194317,7 +194695,7 @@ const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn"
|
|
|
194317
194695
|
* `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
|
|
194318
194696
|
* that one is pinned to the resting state and carries `cost: 0`, so rendering
|
|
194319
194697
|
* `it.processCost` against it silently omits the squashing step's own
|
|
194320
|
-
* `--cost`. Exported for `program.ts`'s `
|
|
194698
|
+
* `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
|
|
194321
194699
|
* same script by hand and must pick the same context.
|
|
194322
194700
|
*/
|
|
194323
194701
|
const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
|
|
@@ -194328,117 +194706,129 @@ const contextAt = (rest, targetState, invoker, cost, model) => gen(function* ()
|
|
|
194328
194706
|
return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), hasCommits);
|
|
194329
194707
|
});
|
|
194330
194708
|
/**
|
|
194331
|
-
* Execute a `"commit"`/`"squash"` decision: a `"commit"` decision stages and
|
|
194332
|
-
* commits everything pending under the decided subject, with an optional
|
|
194333
|
-
* `Gtd-Cost: <cost>[ <model>]` trailer; a `"squash"` decision renders the
|
|
194334
|
-
* commit-state template against the PENDING tree — a render failure REFUSES
|
|
194335
|
-
* the step, touching nothing — then records the pre-squash tip on the
|
|
194336
|
-
* retained-history ref (`retainHistory`, a no-op for an empty process) before
|
|
194337
|
-
* soft-resetting to the process's start parent, writes ONE commit with the
|
|
194338
|
-
* rendered message plus a `Gtd-History: <hash>` trailer pointing at that tip,
|
|
194339
|
-
* and discards everything left pending (the template file included). The
|
|
194340
|
-
* whole-process total and per-model breakdown reach the message through
|
|
194341
|
-
* `it.processCost`/`it.processCostByModel` in the rendered template.
|
|
194342
|
-
*/
|
|
194343
|
-
const executeDecision = (git, run, decision, context, cost, model) => gen(function* () {
|
|
194344
|
-
switch (decision.kind) {
|
|
194345
|
-
case "commit":
|
|
194346
|
-
yield* git.commitAllWithPrefix(withCostTrailer(decision.subject, cost, model));
|
|
194347
|
-
return {
|
|
194348
|
-
kind: "commit",
|
|
194349
|
-
subject: decision.subject
|
|
194350
|
-
};
|
|
194351
|
-
case "squash": {
|
|
194352
|
-
const message = yield* try_({
|
|
194353
|
-
try: () => renderStateTemplate(decision.template, context),
|
|
194354
|
-
catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
|
|
194355
|
-
});
|
|
194356
|
-
const tip = yield* git.resolveRef("HEAD");
|
|
194357
|
-
yield* retainHistory(git, tip, run.startParentHash);
|
|
194358
|
-
yield* git.softResetTo(run.startParentHash);
|
|
194359
|
-
yield* git.commitAsIs(withHistoryTrailer(message, tip));
|
|
194360
|
-
yield* git.discardPending();
|
|
194361
|
-
return {
|
|
194362
|
-
kind: "squash",
|
|
194363
|
-
subject: subjectOf(message)
|
|
194364
|
-
};
|
|
194365
|
-
}
|
|
194366
|
-
}
|
|
194367
|
-
});
|
|
194368
|
-
/**
|
|
194369
194709
|
* Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
|
|
194370
|
-
* driver
|
|
194371
|
-
*
|
|
194372
|
-
*
|
|
194373
|
-
*
|
|
194374
|
-
* assembly (`buildStepScripts`) and `Edge.test.ts`'s render/perform
|
|
194375
|
-
* equivalence tests are its two callers.
|
|
194710
|
+
* driver runs to produce its git effect — the ONE place a decision becomes
|
|
194711
|
+
* git commands. Every git call here is a READ (`git.resolveRef("HEAD")` for
|
|
194712
|
+
* the squash branch's pre-squash tip); nothing is written — gtd itself never
|
|
194713
|
+
* writes git, only a driven script does.
|
|
194376
194714
|
*
|
|
194377
194715
|
* The squash branch renders the commit-state template against the PENDING
|
|
194378
|
-
* tree — a render failure fails this Effect
|
|
194379
|
-
*
|
|
194380
|
-
*
|
|
194381
|
-
*
|
|
194382
|
-
*
|
|
194383
|
-
*
|
|
194384
|
-
*
|
|
194385
|
-
*
|
|
194386
|
-
*
|
|
194716
|
+
* tree — a render failure fails this Effect, touching no git. On success it
|
|
194717
|
+
* emits, in order: retain-history (`updateRef(HISTORY_REF, tip)`, OMITTED
|
|
194718
|
+
* when `tip === run.startParentHash` — an empty process retains nothing,
|
|
194719
|
+
* mirroring `retainHistory`'s no-op check, decided here since `tip` is
|
|
194720
|
+
* already known), soft-reset to the process start, commit-as-is with the
|
|
194721
|
+
* rendered message plus its `Gtd-History:` trailer, discard-pending, then a
|
|
194722
|
+
* trailing `commitOutcome` naming the rendered message's bare subject line —
|
|
194723
|
+
* the script's own report, printed as it runs.
|
|
194724
|
+
*
|
|
194725
|
+
* The commit branch carries one non-obvious case too: a
|
|
194387
194726
|
* commit whose target is the workflow's INITIAL state, from a process that
|
|
194388
194727
|
* retained nothing, is a no-op probe (`gtd --entry fix-precheck` against a
|
|
194389
194728
|
* green suite is the shipped example) and must leave no trace at all. It
|
|
194390
194729
|
* emits retain-history + a mixed reset to the process start instead of a
|
|
194391
194730
|
* commit, so the entry commit and the probe collapse away together rather
|
|
194392
|
-
* than dirtying the log with a round trip to `idle
|
|
194393
|
-
*
|
|
194394
|
-
*
|
|
194395
|
-
* the
|
|
194731
|
+
* than dirtying the log with a round trip to `idle` — this COLLAPSE case
|
|
194732
|
+
* prints `COLLAPSED_TEXT` via a `gtd_report_note`, never a `gtd_report_commit`:
|
|
194733
|
+
* no commit lands, so there is nothing to report by reading `HEAD`'s files —
|
|
194734
|
+
* that read would list the pre-process commit's unrelated files instead.
|
|
194735
|
+
* Deciding it needs the whole `rest` (its definition for the initial state,
|
|
194736
|
+
* its pending changes for the "retained nothing" test), which is why this
|
|
194737
|
+
* takes a `Rest` rather than the `ProcessRun` alone. The ordinary commit
|
|
194738
|
+
* path's own trailing outcome is a `transitionOutcome` when the decided
|
|
194739
|
+
* subject moves to a DIFFERENT state than it started from, else a
|
|
194740
|
+
* `commitOutcome` naming the bare (never cost-trailer'd) subject.
|
|
194741
|
+
*/
|
|
194742
|
+
/** `updateRef(HISTORY_REF, tip)` as a single-element (or empty) step list — OMITTED when `tip === startParentHash`, mirroring `retainHistory`'s own no-op check. Shared by the squash branch and the commit branch's collapse case below. */
|
|
194743
|
+
const retainHistoryStep = (tip, startParentHash) => tip === startParentHash ? [] : [{
|
|
194744
|
+
kind: "gitWrite",
|
|
194745
|
+
command: updateRef(HISTORY_REF, tip)
|
|
194746
|
+
}];
|
|
194747
|
+
/** The commit branch's own trailing outcome: a bare `commitOutcome` for a self-loop (`from === to`), else a `transitionOutcome` naming both states. */
|
|
194748
|
+
const commitDecisionOutcome = (decision) => ({
|
|
194749
|
+
kind: "outcome",
|
|
194750
|
+
command: decision.from === decision.to ? commitOutcome(decision.subject) : transitionOutcome(decision.from, decision.to)
|
|
194751
|
+
});
|
|
194752
|
+
/**
|
|
194753
|
+
* True for a `"commit"` decision that is the "green re-entry into the initial
|
|
194754
|
+
* state retaining nothing" collapse — the process's target is the workflow's
|
|
194755
|
+
* initial state, it started somewhere other than the empty tree, and nothing
|
|
194756
|
+
* pending or already-committed since its start parent survives
|
|
194757
|
+
* (`retainsNothing`). Always `false` for a `"squash"` decision (a squash's
|
|
194758
|
+
* target is never the initial state by construction) and for an ATTEMPT
|
|
194759
|
+
* (`decision.attempt === true`, per decision 5): an agent that did nothing
|
|
194760
|
+
* must never rewind a process — the collapse is for a green re-entry that
|
|
194761
|
+
* retained nothing, not for a fruitless turn, and an attempt landing at the
|
|
194762
|
+
* initial state (a prompt state that also happens to be where the workflow
|
|
194763
|
+
* starts) would otherwise satisfy every OTHER criterion here identically to a
|
|
194764
|
+
* genuine collapse. The ONE predicate `renderDecision`'s emitted-script
|
|
194765
|
+
* branch calls — `program.ts`'s `planLanding` asks the same question,
|
|
194766
|
+
* through the exported `collapsesToInitialState` below, at the same moment,
|
|
194767
|
+
* so the two can never decide differently about the same run.
|
|
194768
|
+
*/
|
|
194769
|
+
const collapsesWith = (git, rest, decision) => gen(function* () {
|
|
194770
|
+
if (decision.kind === "squash" || decision.attempt === true) return false;
|
|
194771
|
+
return parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes));
|
|
194772
|
+
});
|
|
194773
|
+
/**
|
|
194774
|
+
* The service-requiring twin of `collapsesWith`, for `program.ts` to ask
|
|
194775
|
+
* instead of reaching into `GitService` itself — the boundary AGENTS.md pins.
|
|
194776
|
+
* `planLanding` calls this to fill `LandResult.settled` for a `"commit"`/
|
|
194777
|
+
* `"squash"` plan, off the same `rest`/`decision` `renderDecision` already
|
|
194778
|
+
* decided against, so the two git reads (`changedPathsSince` + `resolveRef`)
|
|
194779
|
+
* can never disagree with each other.
|
|
194396
194780
|
*/
|
|
194781
|
+
const collapsesToInitialState = (rest, decision) => gen(function* () {
|
|
194782
|
+
const git = yield* GitService;
|
|
194783
|
+
return yield* collapsesWith(git, rest, decision);
|
|
194784
|
+
});
|
|
194397
194785
|
const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
|
|
194398
194786
|
const run = rest.run;
|
|
194399
194787
|
switch (decision.kind) {
|
|
194400
194788
|
case "commit":
|
|
194401
|
-
if (
|
|
194789
|
+
if (yield* collapsesWith(git, rest, decision)) {
|
|
194402
194790
|
const tip = yield* git.resolveRef("HEAD");
|
|
194403
|
-
|
|
194404
|
-
|
|
194405
|
-
|
|
194406
|
-
|
|
194407
|
-
|
|
194408
|
-
|
|
194409
|
-
|
|
194410
|
-
|
|
194411
|
-
|
|
194412
|
-
|
|
194791
|
+
return [
|
|
194792
|
+
...retainHistoryStep(tip, run.startParentHash),
|
|
194793
|
+
{
|
|
194794
|
+
kind: "gitWrite",
|
|
194795
|
+
command: mixedResetTo(run.startParentHash)
|
|
194796
|
+
},
|
|
194797
|
+
{
|
|
194798
|
+
kind: "outcome",
|
|
194799
|
+
command: noteOutcome(COLLAPSED_TEXT)
|
|
194800
|
+
}
|
|
194801
|
+
];
|
|
194413
194802
|
}
|
|
194414
194803
|
return [{
|
|
194415
194804
|
kind: "gitWrite",
|
|
194416
194805
|
command: commitAll(withCostTrailer(decision.subject, cost, model))
|
|
194417
|
-
}];
|
|
194806
|
+
}, commitDecisionOutcome(decision)];
|
|
194418
194807
|
case "squash": {
|
|
194419
194808
|
const message = yield* try_({
|
|
194420
194809
|
try: () => renderStateTemplate(decision.template, context),
|
|
194421
194810
|
catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
|
|
194422
194811
|
});
|
|
194423
194812
|
const tip = yield* git.resolveRef("HEAD");
|
|
194424
|
-
|
|
194425
|
-
|
|
194426
|
-
|
|
194427
|
-
|
|
194428
|
-
|
|
194429
|
-
|
|
194430
|
-
|
|
194431
|
-
|
|
194432
|
-
|
|
194433
|
-
|
|
194434
|
-
|
|
194435
|
-
|
|
194436
|
-
|
|
194437
|
-
|
|
194438
|
-
|
|
194439
|
-
|
|
194440
|
-
|
|
194441
|
-
|
|
194813
|
+
return [
|
|
194814
|
+
...retainHistoryStep(tip, run.startParentHash),
|
|
194815
|
+
{
|
|
194816
|
+
kind: "gitWrite",
|
|
194817
|
+
command: softResetTo(run.startParentHash)
|
|
194818
|
+
},
|
|
194819
|
+
{
|
|
194820
|
+
kind: "gitWrite",
|
|
194821
|
+
command: commitAsIs(withHistoryTrailer(message, tip))
|
|
194822
|
+
},
|
|
194823
|
+
{
|
|
194824
|
+
kind: "gitWrite",
|
|
194825
|
+
command: discardPending()
|
|
194826
|
+
},
|
|
194827
|
+
{
|
|
194828
|
+
kind: "outcome",
|
|
194829
|
+
command: commitOutcome(subjectOf(message))
|
|
194830
|
+
}
|
|
194831
|
+
];
|
|
194442
194832
|
}
|
|
194443
194833
|
}
|
|
194444
194834
|
});
|
|
@@ -194479,64 +194869,57 @@ const buildStepScripts = (git, rest, decision, context, cost, model) => gen(func
|
|
|
194479
194869
|
return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
|
|
194480
194870
|
});
|
|
194481
194871
|
/**
|
|
194482
|
-
*
|
|
194483
|
-
*
|
|
194484
|
-
*
|
|
194485
|
-
* `
|
|
194486
|
-
*
|
|
194487
|
-
*
|
|
194488
|
-
* `
|
|
194489
|
-
*
|
|
194490
|
-
*
|
|
194491
|
-
*
|
|
194492
|
-
*
|
|
194493
|
-
|
|
194494
|
-
|
|
194495
|
-
|
|
194496
|
-
|
|
194872
|
+
* A no-op is TERMINAL only at a `script` rest: gtd rendered the script, the
|
|
194873
|
+
* driver ran it, it left nothing any pattern claims, and re-running it can't
|
|
194874
|
+
* change that — the loop should exit rather than spin (`gtd land`'s exit-3
|
|
194875
|
+
* `settled` signal). A `prompt` rest can no longer produce a no-op at all (a
|
|
194876
|
+
* clean tree with no `C` row there is an ATTEMPT commit instead — see
|
|
194877
|
+
* `PatternMachine.step`'s doc comment — and `stalledAt` is its own, separately
|
|
194878
|
+
* reported signal). A no-op at a `message` rest is a human gate the loop
|
|
194879
|
+
* already halts on (`kind: "message"`), so it needs no signal either. This is
|
|
194880
|
+
* one of TWO settled shapes — the other is the initial-state collapse
|
|
194881
|
+
* (`collapsesWith`/`collapsesToInitialState`), decided independently by
|
|
194882
|
+
* `program.ts`'s `planLanding` for a `"commit"`/`"squash"` plan.
|
|
194883
|
+
*/
|
|
194884
|
+
const noOpSettles = (rest) => contentKindOf(rest.stateDef) === "script";
|
|
194885
|
+
/**
|
|
194886
|
+
* Decide what landing at `rest` does (`PatternMachine.step` over
|
|
194887
|
+
* `rest.stepDef`/`rest.changes`/`rest.run.trace`, authenticated as
|
|
194888
|
+
* `rest.actor` — the state's own declared actor, so out-of-turn is
|
|
194889
|
+
* unreachable by construction), and — for a `"commit"`/`"squash"` decision —
|
|
194890
|
+
* assemble the emitted `scripts` a driver runs to land it (against
|
|
194891
|
+
* `rest.context` for a commit, or a freshly built `contextAt` for a squash,
|
|
194892
|
+
* which renders a DIFFERENT state's template with the step's cost folded
|
|
194893
|
+
* in). `opts.cost`/`opts.model` ride along for the `Gtd-Cost:` trailer and
|
|
194894
|
+
* the squash template's folded `it.processCost`/`it.processCostByModel`.
|
|
194895
|
+
*/
|
|
194896
|
+
const planStep = (rest, opts = {}) => gen(function* () {
|
|
194897
|
+
const decision = step(rest.stepDef, rest.state, rest.actor, {
|
|
194497
194898
|
changes: rest.changes,
|
|
194498
194899
|
processTrace: rest.run.trace.map((entry) => entry.state)
|
|
194499
194900
|
});
|
|
194500
194901
|
if (decision.kind === "refusal") return {
|
|
194501
194902
|
kind: "refusal",
|
|
194502
|
-
message: formatStepRefusal(
|
|
194903
|
+
message: formatStepRefusal(decision)
|
|
194503
194904
|
};
|
|
194504
194905
|
if (decision.kind === "noop") return {
|
|
194505
194906
|
kind: "noop",
|
|
194506
|
-
state: decision.state
|
|
194907
|
+
state: decision.state,
|
|
194908
|
+
settled: noOpSettles(rest)
|
|
194507
194909
|
};
|
|
194508
194910
|
const { cost, model } = opts;
|
|
194509
|
-
const perform = gen(function* () {
|
|
194510
|
-
const git = yield* GitService;
|
|
194511
|
-
if (decision.kind === "commit") {
|
|
194512
|
-
const target = parseStateSubject(decision.subject)?.state;
|
|
194513
|
-
if (target === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes))) {
|
|
194514
|
-
yield* retainHistory(git, yield* git.resolveRef("HEAD"), rest.run.startParentHash);
|
|
194515
|
-
yield* git.mixedResetTo(rest.run.startParentHash);
|
|
194516
|
-
return {
|
|
194517
|
-
kind: "reset",
|
|
194518
|
-
state: target
|
|
194519
|
-
};
|
|
194520
|
-
}
|
|
194521
|
-
return yield* executeDecision(git, rest.run, decision, rest.context, cost, model);
|
|
194522
|
-
}
|
|
194523
|
-
const context = yield* contextAt(rest, decision.state, invoker, cost, model);
|
|
194524
|
-
return yield* executeDecision(git, rest.run, decision, context, cost, model);
|
|
194525
|
-
});
|
|
194526
194911
|
const git = yield* GitService;
|
|
194527
|
-
const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state,
|
|
194912
|
+
const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
|
|
194528
194913
|
const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
|
|
194529
194914
|
return {
|
|
194530
194915
|
kind: decision.kind,
|
|
194531
194916
|
state: rest.state,
|
|
194532
194917
|
decision,
|
|
194533
|
-
perform,
|
|
194534
194918
|
scripts
|
|
194535
194919
|
};
|
|
194536
194920
|
});
|
|
194537
194921
|
/**
|
|
194538
|
-
* `gtd
|
|
194539
|
-
* --entry <state>`): start a brand NEW process at `entry.state` — any
|
|
194922
|
+
* `gtd --entry <state>`: start a brand NEW process at `entry.state` — any
|
|
194540
194923
|
* declared, non-commit state (see `enterableStates`) — writing an ordinary
|
|
194541
194924
|
* turn commit (`gtd(<actor>): <state>`) carrying zero or more `Gtd-Var:
|
|
194542
194925
|
* <name>=<value>` trailers for each `entry.vars` override, plus — when
|
|
@@ -194544,7 +194927,7 @@ const planStep = (rest, invoker, opts = {}) => gen(function* () {
|
|
|
194544
194927
|
* trailer pinning the new process's diff base (rendered from that template
|
|
194545
194928
|
* against the merged `it.vars`, resolved to a commit, and checked sane).
|
|
194546
194929
|
* Commits via `commitAllWithPrefix` — capturing whatever the working tree
|
|
194547
|
-
* carries at the moment of entry, exactly like an ordinary
|
|
194930
|
+
* carries at the moment of entry, exactly like an ordinary land capture,
|
|
194548
194931
|
* rather than demanding a clean tree first.
|
|
194549
194932
|
*
|
|
194550
194933
|
* All validation is a REFUSAL, not an Effect failure — checked in order:
|
|
@@ -194617,23 +195000,17 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194617
195000
|
...base !== void 0 ? { base } : {},
|
|
194618
195001
|
vars: varOverrides
|
|
194619
195002
|
});
|
|
194620
|
-
const perform = gen(function* () {
|
|
194621
|
-
yield* (yield* GitService).commitAllWithPrefix(message);
|
|
194622
|
-
return {
|
|
194623
|
-
kind: "entry",
|
|
194624
|
-
state: entryState,
|
|
194625
|
-
subject
|
|
194626
|
-
};
|
|
194627
|
-
});
|
|
194628
195003
|
const scriptsGit = yield* GitService;
|
|
194629
195004
|
return {
|
|
194630
195005
|
kind: "entry",
|
|
194631
195006
|
state: entryState,
|
|
194632
195007
|
subject,
|
|
194633
|
-
perform,
|
|
194634
195008
|
scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
|
|
194635
195009
|
kind: "gitWrite",
|
|
194636
195010
|
command: commitAll(message)
|
|
195011
|
+
}, {
|
|
195012
|
+
kind: "outcome",
|
|
195013
|
+
command: commitOutcome(subject)
|
|
194637
195014
|
}])
|
|
194638
195015
|
};
|
|
194639
195016
|
});
|
|
@@ -205942,7 +206319,7 @@ const renderSteeringCommands = (resolved, file, context) => gen(function* () {
|
|
|
205942
206319
|
* mode with no built-in format at all (a workflow-only name, or a declared
|
|
205943
206320
|
* `{}` entry) dispatches to no symbols, no actions, and no diagnostics. Either
|
|
205944
206321
|
* way the file is still formatted and validated by `gtd validate` and the
|
|
205945
|
-
* `gtd
|
|
206322
|
+
* `gtd land` capture gate — just not live in the editor.
|
|
205946
206323
|
*
|
|
205947
206324
|
* `gtd.openSteeringFile` (an `executeCommand`) resolves the CURRENT state
|
|
205948
206325
|
* exactly like the CLI (`resolveRest`/`computeProcessRun`/
|
|
@@ -206619,15 +206996,16 @@ const stepGuards = [
|
|
|
206619
206996
|
}
|
|
206620
206997
|
];
|
|
206621
206998
|
/**
|
|
206622
|
-
* The `gtd
|
|
206999
|
+
* The `gtd land` capture gate: run every guard the resolved rest's state
|
|
206623
207000
|
* applies to against ONE sample of the current committed/working bytes — see
|
|
206624
207001
|
* the module docstring — and fail with the first refusal's reason, prefixed
|
|
206625
|
-
* `gtd
|
|
206626
|
-
* `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision,
|
|
206627
|
-
*
|
|
207002
|
+
* `gtd land: ` (the one place that prefix, and the `cliErrorLine`
|
|
207003
|
+
* `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision, an
|
|
207004
|
+
* ATTEMPT commit (`input.attempt` — see its own doc comment), a state with no
|
|
207005
|
+
* `file:`, or a state no guard applies to.
|
|
206628
207006
|
*/
|
|
206629
207007
|
const enforceStepGuards = (input) => gen(function* () {
|
|
206630
|
-
if (input.kind !== "commit") return;
|
|
207008
|
+
if (input.kind !== "commit" || input.attempt) return;
|
|
206631
207009
|
const file = input.file;
|
|
206632
207010
|
if (file === void 0) return;
|
|
206633
207011
|
const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
|
|
@@ -206652,10 +207030,349 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206652
207030
|
};
|
|
206653
207031
|
for (const g of applicable) {
|
|
206654
207032
|
const refusal = yield* g.check(ctx);
|
|
206655
|
-
if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd
|
|
207033
|
+
if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd land: ${refusal}`));
|
|
206656
207034
|
}
|
|
206657
207035
|
});
|
|
206658
207036
|
//#endregion
|
|
207037
|
+
//#region src/Beat.ts
|
|
207038
|
+
/**
|
|
207039
|
+
* The beat kind for a rest, in precedence order:
|
|
207040
|
+
*
|
|
207041
|
+
* 1. `stalled` — `Edge.ts`'s `stalledAt` verdict, passed in as a plain
|
|
207042
|
+
* boolean (this module never resolves it itself). `stalledAt` already
|
|
207043
|
+
* requires a clean tree, so this can never collide with `capture` below.
|
|
207044
|
+
* 2. `capture` — a `message` rest with a DIRTY tree: the human already
|
|
207045
|
+
* acted; a driver lands it immediately.
|
|
207046
|
+
* 3. Otherwise, the content kind verbatim (`script`/`prompt`, or `message`
|
|
207047
|
+
* on a clean tree).
|
|
207048
|
+
*/
|
|
207049
|
+
const beatKindOf = (input) => {
|
|
207050
|
+
if (input.stalled) return "stalled";
|
|
207051
|
+
if (input.contentKind === "message" && input.dirty) return "capture";
|
|
207052
|
+
return input.contentKind;
|
|
207053
|
+
};
|
|
207054
|
+
/**
|
|
207055
|
+
* The `stalled` beat's own content: a diagnosis naming the stuck state and
|
|
207056
|
+
* the three ways out. The first line is shaped so a stderr grep for
|
|
207057
|
+
* `stalled at "<state>"` stays a stable substring (see
|
|
207058
|
+
* `tests/integration/features/readme-driver.feature`).
|
|
207059
|
+
*/
|
|
207060
|
+
const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nThree ways out:\n - sharpen the state's prompt so the turn has something concrete to author\n - give the state a retry: cap, redirecting to an escalation state after\n N fruitless attempts\n - declare a "C" pattern on the state, if it can legitimately finish with\n nothing to change\n`;
|
|
207061
|
+
/**
|
|
207062
|
+
* Assemble one beat document line — the newline-terminated JSON `gtd next
|
|
207063
|
+
* --json` prints. Key order is the emitted order: `kind`, `content`,
|
|
207064
|
+
* `session`, `model`, `validate`, `log`, `state`, `actor`, `label`,
|
|
207065
|
+
* `memory`, `file`, `mode`, `edges`.
|
|
207066
|
+
*
|
|
207067
|
+
* `session`/`validate` are the DISPATCH BLOCK: dropped unless `kind ===
|
|
207068
|
+
* "prompt"`, even if the caller passed one in — the gate lives here (not
|
|
207069
|
+
* only in the caller that computes them) so it can never drift. `content` is
|
|
207070
|
+
* the rendered rest's own content, except at `kind === "stalled"`, where
|
|
207071
|
+
* it's `stallDiagnosis`'s text instead. `model`/`memory`/`file`/`mode`/
|
|
207072
|
+
* `label`/`edges` are plain facts about the resting state and are emitted at
|
|
207073
|
+
* EVERY kind — the same discipline the old `nextJsonOutput` had: omitted
|
|
207074
|
+
* (never `null`) when their source is unset.
|
|
207075
|
+
*/
|
|
207076
|
+
const beatDocument = (input) => {
|
|
207077
|
+
const { rendered, kind, log, session, validate } = input;
|
|
207078
|
+
const dispatchable = kind === "prompt";
|
|
207079
|
+
return JSON.stringify({
|
|
207080
|
+
kind,
|
|
207081
|
+
content: kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content,
|
|
207082
|
+
...dispatchable && session !== void 0 ? { session: {
|
|
207083
|
+
id: session.id,
|
|
207084
|
+
resume: session.resume
|
|
207085
|
+
} } : {},
|
|
207086
|
+
...rendered.model !== void 0 ? { model: rendered.model } : {},
|
|
207087
|
+
...dispatchable && validate !== void 0 ? { validate } : {},
|
|
207088
|
+
log,
|
|
207089
|
+
state: rendered.state,
|
|
207090
|
+
actor: rendered.actor,
|
|
207091
|
+
...rendered.label !== void 0 ? { label: rendered.label } : {},
|
|
207092
|
+
...rendered.memory !== void 0 ? { memory: rendered.memory } : {},
|
|
207093
|
+
...rendered.file !== void 0 ? { file: rendered.file } : {},
|
|
207094
|
+
...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
|
|
207095
|
+
...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
|
|
207096
|
+
}) + "\n";
|
|
207097
|
+
};
|
|
207098
|
+
//#endregion
|
|
207099
|
+
//#region src/WorktreeState.ts
|
|
207100
|
+
/**
|
|
207101
|
+
* The per-worktree, untracked state directory (the git dir) and the
|
|
207102
|
+
* driver-scoped paths gtd keeps in it — IO-shaped, so it sits at the edge
|
|
207103
|
+
* alongside `Edge.ts`/`RepoFiles.ts`; `PatternMachine.ts` stays pure and
|
|
207104
|
+
* never sees it.
|
|
207105
|
+
*
|
|
207106
|
+
* `worktreeGitDir` is resolved from the FILESYSTEM, never from a `git`
|
|
207107
|
+
* subprocess: `<root>/.git` is either a directory (a plain repo — the git dir
|
|
207108
|
+
* IS `.git`) or a `gitdir: <path>` pointer file (a linked worktree, or
|
|
207109
|
+
* `--separate-git-dir`). Reading it directly needs no `stat` (a failed read
|
|
207110
|
+
* because `.git` is a directory reads the same as a missing `.git` — both
|
|
207111
|
+
* fall back to `".git"`) and, unlike `git rev-parse --git-dir`, cannot be
|
|
207112
|
+
* diverted by a stray `GIT_DIR`/`GIT_WORK_TREE` in the ambient environment —
|
|
207113
|
+
* there is no subprocess for those to divert. `loopLogPath` is the ONE path
|
|
207114
|
+
* resolved here: gtd keeps no other per-worktree state (sessions are derived
|
|
207115
|
+
* from history, stall is history itself), and gtd never creates or truncates
|
|
207116
|
+
* even this file — the driver owns it; gtd only names it.
|
|
207117
|
+
*/
|
|
207118
|
+
const GITDIR_POINTER = /^gitdir:\s*(.+)$/m;
|
|
207119
|
+
/**
|
|
207120
|
+
* The worktree's own git dir — byte-identical to `git rev-parse --git-dir`
|
|
207121
|
+
* for a plain repo, a linked worktree, or `--separate-git-dir`. Never fails:
|
|
207122
|
+
* a read failure (no `.git`, or `.git` is a real directory) or a `.git` file
|
|
207123
|
+
* with no `gitdir:` line both fall back to the literal `".git"`, which is
|
|
207124
|
+
* also the right answer against the in-memory test tier (whose fake
|
|
207125
|
+
* filesystem has no `.git` entry at all).
|
|
207126
|
+
*/
|
|
207127
|
+
const worktreeGitDir = gen(function* () {
|
|
207128
|
+
const { root } = yield* Cwd;
|
|
207129
|
+
const content = yield* (yield* FileSystem).readFileString(join(root, ".git")).pipe(catchAll(() => succeed$3(void 0)));
|
|
207130
|
+
const pointer = content === void 0 ? null : GITDIR_POINTER.exec(content);
|
|
207131
|
+
if (pointer === null) return ".git";
|
|
207132
|
+
const gitdir = pointer[1].trim();
|
|
207133
|
+
return isAbsolute(gitdir) ? gitdir : join(root, gitdir);
|
|
207134
|
+
});
|
|
207135
|
+
/**
|
|
207136
|
+
* The per-worktree loop log path `gtd next --json` reports as `log`.
|
|
207137
|
+
* `$GTD_LOOP_LOG` (non-empty) wins verbatim — no join, no normalization — so
|
|
207138
|
+
* a `gtd` invoked from inside a check script inherits the enclosing run's
|
|
207139
|
+
* log path unchanged. Otherwise `<git-dir>/gtd-loop.log`, isolating two
|
|
207140
|
+
* worktrees looping concurrently from each other's log by construction (each
|
|
207141
|
+
* has its own git dir). Advisory metadata only — gtd neither creates nor
|
|
207142
|
+
* truncates the file.
|
|
207143
|
+
*/
|
|
207144
|
+
const loopLogPath = gen(function* () {
|
|
207145
|
+
const { all } = yield* EnvVars;
|
|
207146
|
+
const override = all.GTD_LOOP_LOG;
|
|
207147
|
+
if (override !== void 0 && override !== "") return override;
|
|
207148
|
+
return join(yield* worktreeGitDir, "gtd-loop.log");
|
|
207149
|
+
});
|
|
207150
|
+
//#endregion
|
|
207151
|
+
//#region src/Install.ts
|
|
207152
|
+
/**
|
|
207153
|
+
* The `gtd install` briefing DOCUMENT — string data plus `renderBriefing()`.
|
|
207154
|
+
* This module holds no protocol logic and nothing derives behaviour from it;
|
|
207155
|
+
* it exists purely so the binary can print, on demand, a complete and
|
|
207156
|
+
* self-contained explanation of how to build a gtd driver in any shell
|
|
207157
|
+
* or runtime — the self-serve version of README's "Writing your own driver"
|
|
207158
|
+
* chapter. `MINIMAL_DRIVER` is pinned equal to that chapter's own fenced
|
|
207159
|
+
* bash block by `Install.test.ts`, so the two can never drift.
|
|
207160
|
+
*/
|
|
207161
|
+
const GTD_VERSION$1 = createRequire(import.meta.url)("../package.json").version;
|
|
207162
|
+
const MINIMAL_DRIVER = `#!/usr/bin/env bash
|
|
207163
|
+
set -euo pipefail
|
|
207164
|
+
|
|
207165
|
+
# The bundle plans; this executes. Emitted scripts print their own outcomes.
|
|
207166
|
+
# Exit 3 means SETTLED — nothing owed — so this exits the whole driver at 0.
|
|
207167
|
+
gtd_land() {
|
|
207168
|
+
local json code=0
|
|
207169
|
+
json="$(gtd land --json)" || code=$?
|
|
207170
|
+
[ "$code" = 0 ] || [ "$code" = 3 ] || return "$code"
|
|
207171
|
+
bash -c "$(jq -r '.required // empty' <<<"$json")" || return $?
|
|
207172
|
+
bash -c "$(jq -r '.optional // empty' <<<"$json")" ||
|
|
207173
|
+
echo "warn: presentation follow-up failed — continuing" >&2
|
|
207174
|
+
[ "$code" = 3 ] && exit 0
|
|
207175
|
+
return 0
|
|
207176
|
+
}
|
|
207177
|
+
|
|
207178
|
+
while :; do
|
|
207179
|
+
next="$(gtd next --json)" || exit 1
|
|
207180
|
+
kind="$(jq -r .kind <<<"$next")"
|
|
207181
|
+
log="$(jq -r .log <<<"$next")"
|
|
207182
|
+
case "$kind" in
|
|
207183
|
+
stalled) jq -r .content <<<"$next" >&2; exit 1 ;;
|
|
207184
|
+
message) jq -r .content <<<"$next"; exit 0 ;;
|
|
207185
|
+
capture) ;; # the human already acted — just land it
|
|
207186
|
+
script) bash -c "$(jq -r .content <<<"$next")" >>"$log" 2>&1 || true ;;
|
|
207187
|
+
prompt)
|
|
207188
|
+
sid="$(jq -r '.session.id // empty' <<<"$next")"
|
|
207189
|
+
c="$(jq -r .content <<<"$next")" model="$(jq -r '.model // empty' <<<"$next")"
|
|
207190
|
+
agent_turn() { claude -p "$c" "$1" "$sid" \${model:+--model "$model"} \\
|
|
207191
|
+
--dangerously-skip-permissions >>"$log" 2>&1; }
|
|
207192
|
+
if [ "$(jq -r '.session.resume // false' <<<"$next")" = true ]
|
|
207193
|
+
then agent_turn --resume || agent_turn --session-id
|
|
207194
|
+
else agent_turn --session-id || agent_turn --resume
|
|
207195
|
+
fi
|
|
207196
|
+
v="$(jq -r '.validate // empty' <<<"$next")" n=0
|
|
207197
|
+
while [ -n "$v" ] && ! out="$(bash -c "$v" 2>&1)"; do
|
|
207198
|
+
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$out" >&2; exit 1; }
|
|
207199
|
+
claude -p "$out" --resume "$sid" --dangerously-skip-permissions \\
|
|
207200
|
+
>>"$log" 2>&1 # $out IS the fix prompt, verbatim
|
|
207201
|
+
done ;;
|
|
207202
|
+
esac
|
|
207203
|
+
gtd_land || exit 1
|
|
207204
|
+
done`;
|
|
207205
|
+
const HEADER = () => `gtd ${GTD_VERSION$1} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, and never runs a check script. The one subprocess gtd ever spawns\nitself is a steering mode's own format:/validate: command, during a\nland-capture guard.\n`;
|
|
207206
|
+
const BEAT_PROTOCOL = `
|
|
207207
|
+
## The beat protocol
|
|
207208
|
+
|
|
207209
|
+
Two commands, one repeated beat: ask -> act -> land.
|
|
207210
|
+
|
|
207211
|
+
gtd next --json what now - a pure read; poll it, peek it, no writes
|
|
207212
|
+
gtd land --json how to record what happened - run the script it emits
|
|
207213
|
+
|
|
207214
|
+
Every beat that acts, lands. Every beat that rests, exits.
|
|
207215
|
+
|
|
207216
|
+
\`gtd next --json\` is a read-only PEEK — every field (\`.session.id\`/
|
|
207217
|
+
\`.session.resume\` included) is DERIVED from history, never stored, so looking
|
|
207218
|
+
is free: nothing distinguishes a peek from a dispatch, and calling it twice in
|
|
207219
|
+
a row is always safe. There is no separate claiming form.
|
|
207220
|
+
|
|
207221
|
+
The \`kind\` field selects what to do:
|
|
207222
|
+
|
|
207223
|
+
- \`stalled\` — print \`.content\` (a diagnosis) to stderr and exit non-zero.
|
|
207224
|
+
Terminal: another dispatch would just repeat the same fruitless turn.
|
|
207225
|
+
- \`message\` — print \`.content\` and exit 0. This is a human gate.
|
|
207226
|
+
- \`capture\` — a human gate the human already acted on (the tree is dirty).
|
|
207227
|
+
Land it immediately, no display needed.
|
|
207228
|
+
- \`script\` — run \`bash -c .content\`, appending its output to \`.log\`, and
|
|
207229
|
+
ignore its exit code — the outcome lives in the tree, not in the exit code.
|
|
207230
|
+
- \`prompt\` — send \`.content\` to your agent CLI, using the embedded
|
|
207231
|
+
\`.session\`/\`.model\`/\`.validate\` fields (see below).
|
|
207232
|
+
`;
|
|
207233
|
+
const JSON_FIELD_REFERENCE = `
|
|
207234
|
+
## JSON field reference
|
|
207235
|
+
|
|
207236
|
+
Every field below is always present unless marked "when set".
|
|
207237
|
+
|
|
207238
|
+
### \`gtd next --json\` — the beat document
|
|
207239
|
+
|
|
207240
|
+
- Always: \`kind\` (\`capture\`|\`message\`|\`script\`|\`prompt\`|\`stalled\`),
|
|
207241
|
+
\`content\`, \`log\`, \`state\`, \`actor\`
|
|
207242
|
+
- When set: \`model\`, \`memory\`, \`label\`, \`file\`, \`mode\`, \`edges\` — plain
|
|
207243
|
+
facts about the resting state, present at every kind
|
|
207244
|
+
- Only at \`kind: "prompt"\` (the DISPATCH BLOCK — absent at every other kind,
|
|
207245
|
+
a \`stalled\` beat included, by construction): \`session\` (\`{id, resume}\`,
|
|
207246
|
+
both DERIVED from history — a hash of the resting state's memory scope —
|
|
207247
|
+
never stored, so a plain peek is exactly as safe to call as a dispatch would
|
|
207248
|
+
be) and, when the state declares a validatable steering file, \`validate\`
|
|
207249
|
+
(the same script \`gtd validate --json\`'s \`.script\` emits, embedded)
|
|
207250
|
+
|
|
207251
|
+
### \`gtd land --json\` (also \`--entry\`)
|
|
207252
|
+
|
|
207253
|
+
\`state\`, \`subject\` (\`null\` on a no-op), \`script\` (the combined form —
|
|
207254
|
+
\`required\` verbatim, \`optional\` wrapped non-fatally, so a
|
|
207255
|
+
\`jq -r .script | bash\` driver needs one \`bash\` call, not two), \`required\`,
|
|
207256
|
+
\`optional\`, \`settled\`; \`cost\`/\`model\` when recorded. \`--entry\` omits
|
|
207257
|
+
\`settled\` — read it as \`.settled // false\`.
|
|
207258
|
+
|
|
207259
|
+
### \`gtd abandon\`/\`gtd restore --json\`
|
|
207260
|
+
|
|
207261
|
+
\`required\`, \`optional\` (always \`""\`).
|
|
207262
|
+
|
|
207263
|
+
### \`gtd validate --json\`
|
|
207264
|
+
|
|
207265
|
+
\`state\`, \`script\` (\`""\` means nothing to validate); \`file\`/\`mode\` when the
|
|
207266
|
+
resolved rest declares them. On a non-zero run the script's own output IS the
|
|
207267
|
+
fix prompt, verbatim.
|
|
207268
|
+
|
|
207269
|
+
### The error envelope
|
|
207270
|
+
|
|
207271
|
+
\`{"state":"error","prompt":"<message>"}\` on stdout, plus a single \`gtd: \`
|
|
207272
|
+
line on stderr, exit 1 — this covers usage errors and defects too.
|
|
207273
|
+
|
|
207274
|
+
### Exit codes (\`gtd land\` only — every other command is plain 0/1)
|
|
207275
|
+
|
|
207276
|
+
| code | meaning |
|
|
207277
|
+
| ---- | --------------------------------------------------------------------- |
|
|
207278
|
+
| 0 | a script was emitted (capture, turn, attempt, squash), or a benign no-op at a clean \`message\` rest |
|
|
207279
|
+
| 3 | SETTLED — nothing owed: a no-op at a \`script\` rest, or the initial-state collapse. stdout still carries a script (a print-only note, or the collapse's real retain+rewind) — run it |
|
|
207280
|
+
| 1 | refusal or usage error — nothing was emitted |
|
|
207281
|
+
|
|
207282
|
+
With \`set -o pipefail\`, \`gtd land | bash\` propagates gtd's own exit code
|
|
207283
|
+
through the pipe.
|
|
207284
|
+
`;
|
|
207285
|
+
const DRIVER_OBLIGATIONS = `
|
|
207286
|
+
## Driver obligations, in order
|
|
207287
|
+
|
|
207288
|
+
1. Read exactly one \`gtd next --json\` beat document per iteration. There is
|
|
207289
|
+
no opening move: a human's pending edit arrives as a \`kind: "capture"\`
|
|
207290
|
+
beat, which you land immediately without executing anything.
|
|
207291
|
+
2. A \`kind: "stalled"\` beat halts the driver with a non-zero exit; a
|
|
207292
|
+
\`kind: "message"\` beat prints \`content\` and exits 0.
|
|
207293
|
+
3. Run scripts with their output appended to \`.log\` — gtd never creates or
|
|
207294
|
+
truncates that file itself; truncate it once per run. \`$GTD_LOOP_LOG\`
|
|
207295
|
+
overrides its path.
|
|
207296
|
+
4. Map \`.session.id\`/\`.session.resume\` onto the agent CLI's own session
|
|
207297
|
+
flags — try \`resume\`'s hinted flag first and fall back to the other on
|
|
207298
|
+
failure (\`resume\` is a hint, not a contract: nothing is stored, so a
|
|
207299
|
+
crashed prior turn or an expired agent session recovers by itself).
|
|
207300
|
+
5. After a \`prompt\` beat, run the document's own \`.validate\` script (when
|
|
207301
|
+
present) and re-prompt its output verbatim on failure — the DRIVER owns
|
|
207302
|
+
the retry cap, not gtd.
|
|
207303
|
+
6. Land only a beat you acted on (\`capture\`/\`script\`/\`prompt\`) — a stray
|
|
207304
|
+
\`gtd land\` at a clean \`prompt\` rest authors an empty attempt on purpose
|
|
207305
|
+
(that IS the stall bookkeeping), so don't land beats you didn't dispatch.
|
|
207306
|
+
7. Run \`gtd land --json\`, then execute its \`required\` (always) and
|
|
207307
|
+
\`optional\` (presentation only — its failure is a warning, not a halt).
|
|
207308
|
+
8. Exit 3 (SETTLED) means there is nothing left to do — run the script first,
|
|
207309
|
+
then stop; exit 0 continues the loop.
|
|
207310
|
+
`;
|
|
207311
|
+
const RECOVERY = `
|
|
207312
|
+
## Recovery
|
|
207313
|
+
|
|
207314
|
+
gtd exiting 1 means nothing was attempted — a refusal or a usage error. Exit
|
|
207315
|
+
3 is NOT a failure: it means SETTLED (nothing owed), and stdout still carries
|
|
207316
|
+
a script to run. An emitted script exiting non-zero when YOU run it means
|
|
207317
|
+
something MAY have partially happened. All three recover the same way:
|
|
207318
|
+
re-invoke gtd. It re-reads the real repository state fresh every time, and
|
|
207319
|
+
every emitted script asserts its own HEAD precondition, so a script generated
|
|
207320
|
+
against a repository state that has since moved refuses loudly instead of
|
|
207321
|
+
corrupting anything. Scripts are re-runnable and assert their own HEAD
|
|
207322
|
+
precondition — a driver needs no retry logic beyond "ask gtd again".
|
|
207323
|
+
`;
|
|
207324
|
+
const referenceImplementation = () => `
|
|
207325
|
+
## Building the user's driver: interview first, then adapt
|
|
207326
|
+
|
|
207327
|
+
The obligations above are the contract; the bash block below is one WORKED
|
|
207328
|
+
EXAMPLE of it. Do not copy it blindly, and do not guess the user's setup —
|
|
207329
|
+
INTERVIEW them, then build a driver shaped by their answers. Ask (offering
|
|
207330
|
+
the default when they have no preference):
|
|
207331
|
+
|
|
207332
|
+
1. **Which agent should run the turns?** Whatever coding-agent CLI they
|
|
207333
|
+
already use (default: \`claude\`). Map \`.session.id\`/\`.session.resume\`
|
|
207334
|
+
onto THAT agent's continuation mechanism (obligation 4); an agent with no
|
|
207335
|
+
session concept just ignores them — memory is an optimization, every
|
|
207336
|
+
prompt is self-contained.
|
|
207337
|
+
2. **Under which permission model?** Fully autonomous turns (e.g.
|
|
207338
|
+
\`--dangerously-skip-permissions\`), a sandbox, or the agent's default
|
|
207339
|
+
prompting — their risk tolerance, their call. Also whether the workflow's
|
|
207340
|
+
own \`.model\` hints should be honored (default: yes).
|
|
207341
|
+
3. **How do they want to invoke it?** A command on PATH (default:
|
|
207342
|
+
\`~/.local/bin/gtd-loop\`), a project task-runner entry, a CI job step —
|
|
207343
|
+
or no artifact at all: YOU drive the beats yourself, following the
|
|
207344
|
+
obligations directly. Pick the runtime to match: bash, their language of
|
|
207345
|
+
choice, anything that parses JSON and spawns subprocesses.
|
|
207346
|
+
4. **What should happen at the boundaries?** Where the log goes (obligation
|
|
207347
|
+
3), and whether halting at a human gate should do anything richer than
|
|
207348
|
+
print — desktop notification, terminal-multiplexer status, editor focus —
|
|
207349
|
+
which belongs in their wrapper, never in gtd.
|
|
207350
|
+
|
|
207351
|
+
Then build it, and verify safely before the first real drive:
|
|
207352
|
+
\`gtd next --json\` is a pure read — parse it, check your kind dispatch
|
|
207353
|
+
against the table above, call it as often as you like. Nothing happens until
|
|
207354
|
+
you run an emitted script.
|
|
207355
|
+
|
|
207356
|
+
The reference rendering in bash (requires \`jq\`; the \`claude\` lines are
|
|
207357
|
+
what answers 1–2 replace):
|
|
207358
|
+
|
|
207359
|
+
\`\`\`bash
|
|
207360
|
+
${MINIMAL_DRIVER}
|
|
207361
|
+
\`\`\`
|
|
207362
|
+
`;
|
|
207363
|
+
const PREREQUISITES = `
|
|
207364
|
+
## Prerequisites and portability
|
|
207365
|
+
|
|
207366
|
+
- \`jq\` — to pull \`required\`/\`optional\`/the rest of the JSON fields above
|
|
207367
|
+
back out of gtd's \`--json\` output.
|
|
207368
|
+
- \`gtd\` on \`PATH\` — a seeded mode \`validate:\` command is literally the
|
|
207369
|
+
string \`gtd check <mode> '<file>'\`, resolved by NAME at script-run time.
|
|
207370
|
+
Keep one \`gtd\` on \`PATH\`, consistently.
|
|
207371
|
+
|
|
207372
|
+
Any runtime works: the contract is JSON in, subprocesses out.
|
|
207373
|
+
`;
|
|
207374
|
+
const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + referenceImplementation() + PREREQUISITES;
|
|
207375
|
+
//#endregion
|
|
206659
207376
|
//#region src/program.ts
|
|
206660
207377
|
/**
|
|
206661
207378
|
* `gtd lsp`: start the LSP server for `.gtd/` steering files over stdio. Its
|
|
@@ -206667,6 +207384,23 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206667
207384
|
*/
|
|
206668
207385
|
const runLspCommand = () => startLspServer();
|
|
206669
207386
|
/**
|
|
207387
|
+
* `gtd install`: print the driver-building briefing (`src/Install.ts`'s
|
|
207388
|
+
* `renderBriefing()`) — a complete, self-contained explanation of how to
|
|
207389
|
+
* build a gtd driver in any shell or runtime, emitted by the binary
|
|
207390
|
+
* itself so it is always exactly as current as the gtd that prints it.
|
|
207391
|
+
* Writes nothing: "install" means installing knowledge into the calling
|
|
207392
|
+
* agent's context. Its `needs: "none"` (see `Cli.ts`'s `needsOf`, same as
|
|
207393
|
+
* `gtd lsp`) means it skips the repo-root guard entirely — it resolves no
|
|
207394
|
+
* workflow state and reads no config, so it runs from any directory, in or
|
|
207395
|
+
* out of a repository. `--json` and extra arguments are already rejected by
|
|
207396
|
+
* `Cli.ts` before a `Command` value ever reaches here (`--json` itself is
|
|
207397
|
+
* accepted, wrapping the same text under a `briefing` key).
|
|
207398
|
+
*/
|
|
207399
|
+
const runInstallCommand = (json, write) => sync$1(() => {
|
|
207400
|
+
const briefing = renderBriefing();
|
|
207401
|
+
write(json ? JSON.stringify({ briefing }) + "\n" : briefing.endsWith("\n") ? briefing : briefing + "\n");
|
|
207402
|
+
});
|
|
207403
|
+
/**
|
|
206670
207404
|
* `gtd init`: scaffold a MINIMAL `.gtdrc.json` seeding the default variables a
|
|
206671
207405
|
* fresh project is most likely to change — the test command (`vars.testCommand`)
|
|
206672
207406
|
* and a ready-to-edit Prettier formatting suggestion (`modes:`). It writes NO
|
|
@@ -206680,7 +207414,7 @@ const runLspCommand = () => startLspServer();
|
|
|
206680
207414
|
* instead (`assertInitLocation`, which also allows a directory outside any
|
|
206681
207415
|
* repository) since it writes `.gtdrc.json` at the root and refuses to
|
|
206682
207416
|
* clobber an existing config. The file is left UNCOMMITTED, so the message
|
|
206683
|
-
* warns to commit it before the first `gtd
|
|
207417
|
+
* warns to commit it before the first `gtd land` (an uncommitted config
|
|
206684
207418
|
* counts as a pending change the initial state's `* **` edge would otherwise
|
|
206685
207419
|
* capture).
|
|
206686
207420
|
*/
|
|
@@ -206697,8 +207431,10 @@ const runInitCommand = (json, write) => gen(function* () {
|
|
|
206697
207431
|
written: ".gtdrc.json",
|
|
206698
207432
|
inRepo
|
|
206699
207433
|
}) + "\n");
|
|
206700
|
-
else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first
|
|
207434
|
+
else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first landing. Once\ncommitted, run `gtd land` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd land` from such a repo to\nbegin.\n"));
|
|
206701
207435
|
});
|
|
207436
|
+
/** `gtd land`'s exit code for the two SETTLED shapes (`LandResult.settled`) — nothing owed, but stdout still carries a script a piping driver must run. Every other successful landing exits 0. */
|
|
207437
|
+
const EXIT_SETTLED = 3;
|
|
206702
207438
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
206703
207439
|
/** The `EmitPreconditions` every script this file assembles asserts against: the resolved HEAD hash the snapshot driving it was taken at — `""` when the repo has no commits yet, `headAssertion`'s own convention for that case. */
|
|
206704
207440
|
const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
|
|
@@ -206742,6 +207478,8 @@ const openWindowScript = (rest, targetState) => {
|
|
|
206742
207478
|
})
|
|
206743
207479
|
}]).optional;
|
|
206744
207480
|
};
|
|
207481
|
+
/** True for a `"commit"` decision that is an ATTEMPT (`PatternMachine.StepCommit.attempt`) — the one flag both `enforceStepGuards` and `buildRequiredScript` bypass their own steps for (see each call site). */
|
|
207482
|
+
const isAttemptDecision = (decision) => decision.kind === "commit" && decision.attempt === true;
|
|
206745
207483
|
/**
|
|
206746
207484
|
* The subject the required script WILL produce. A `"commit"` decision's is
|
|
206747
207485
|
* fully deterministic already; a `"squash"` decision's is only known once its
|
|
@@ -206754,33 +207492,35 @@ const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$
|
|
|
206754
207492
|
* The `required` half: everything that decides what lands in git, in order —
|
|
206755
207493
|
* the review-window CLOSE (when one is open), the resting state's own
|
|
206756
207494
|
* steering-mode format/validate commands, then the commit/squash steps
|
|
206757
|
-
* themselves.
|
|
206758
|
-
|
|
206759
|
-
|
|
207495
|
+
* themselves. The steering-mode step is SKIPPED for an attempt commit
|
|
207496
|
+
* (decision 6): there is nothing to format/validate in an empty diff, and a
|
|
207497
|
+
* `format:` command running ahead of the commit could dirty the tree and
|
|
207498
|
+
* turn an "empty" attempt non-empty, breaking the derivation `stalledAt`
|
|
207499
|
+
* relies on (`Edge.ts`). The review-window close still runs regardless —
|
|
207500
|
+
* committing with a window open would land the attempt on the review base.
|
|
207501
|
+
*/
|
|
207502
|
+
const buildRequiredScript = (rest, decision, cost, model) => gen(function* () {
|
|
206760
207503
|
const git = yield* GitService;
|
|
206761
207504
|
const closeDecision = yield* decideCloseWindow;
|
|
206762
|
-
const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state,
|
|
207505
|
+
const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
|
|
207506
|
+
const isAttempt = isAttemptDecision(decision);
|
|
206763
207507
|
const steps = [
|
|
206764
207508
|
...closeDecision.shouldClose ? [{
|
|
206765
207509
|
kind: "gitWrite",
|
|
206766
207510
|
command: buildCloseWindowScript(closeDecision.refs)
|
|
206767
207511
|
}] : [],
|
|
206768
|
-
...yield* steeringModeSteps(rest),
|
|
207512
|
+
...isAttempt ? [] : yield* steeringModeSteps(rest),
|
|
206769
207513
|
...yield* renderDecision(git, rest, decision, commitContext, cost, model)
|
|
206770
207514
|
];
|
|
206771
207515
|
return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
|
|
206772
207516
|
});
|
|
206773
|
-
/** A step's `--cost`/`--model` as `planStep` options — each key OMITTED (never `undefined`-valued) when the flag was not given. */
|
|
206774
|
-
const stepOptions = (cost, model) => ({
|
|
206775
|
-
...cost !== void 0 ? { cost } : {},
|
|
206776
|
-
...model !== void 0 ? { model } : {}
|
|
206777
|
-
});
|
|
206778
207517
|
/**
|
|
206779
|
-
*
|
|
206780
|
-
*
|
|
206781
|
-
*
|
|
206782
|
-
*
|
|
206783
|
-
*
|
|
207518
|
+
* Decide the one resulting transition (commit or squash) for `gtd land` —
|
|
207519
|
+
* WITHOUT performing it. `currentRest` → `planStep` decides, authenticating
|
|
207520
|
+
* as `rest.actor` (the resolved rest's own declared actor — landing derives
|
|
207521
|
+
* who acts, it never takes an actor argument); the step-capture guards
|
|
207522
|
+
* (`src/StepGuards.ts`) then refuse before anything is emitted. The decision
|
|
207523
|
+
* is assembled by hand into a
|
|
206784
207524
|
* `required` script (the review-window CLOSE, when one is open; the resting
|
|
206785
207525
|
* state's steering-mode format/validate commands, when it declares
|
|
206786
207526
|
* `file:`+`mode:`; then the commit/squash steps themselves, via
|
|
@@ -206796,17 +207536,21 @@ const stepOptions = (cost, model) => ({
|
|
|
206796
207536
|
* not the real head — and it contains no window close/open steps at all. This
|
|
206797
207537
|
* function builds its own required/optional pair to sidestep that trap.
|
|
206798
207538
|
*/
|
|
206799
|
-
const
|
|
207539
|
+
const planLanding = (opts = {}) => gen(function* () {
|
|
206800
207540
|
const rest = yield* currentRest;
|
|
206801
|
-
const plan = yield* planStep(rest,
|
|
207541
|
+
const plan = yield* planStep(rest, opts);
|
|
206802
207542
|
if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
|
|
206803
207543
|
if (plan.kind === "noop") return {
|
|
206804
207544
|
state: plan.state,
|
|
206805
207545
|
subject: null,
|
|
206806
207546
|
cost: null,
|
|
206807
207547
|
model: null,
|
|
206808
|
-
required:
|
|
206809
|
-
|
|
207548
|
+
required: emitScripts({}, [{
|
|
207549
|
+
kind: "outcome",
|
|
207550
|
+
command: noteOutcome(noopText(plan.state))
|
|
207551
|
+
}]).required,
|
|
207552
|
+
optional: "",
|
|
207553
|
+
settled: plan.settled
|
|
206810
207554
|
};
|
|
206811
207555
|
const decision = plan.decision;
|
|
206812
207556
|
if (decision.kind !== "commit" && decision.kind !== "squash") return yield* fail$2(/* @__PURE__ */ new Error(`gtd: internal error — plan kind "${plan.kind}" but decision kind "${decision.kind}"`));
|
|
@@ -206815,48 +207559,57 @@ const stepAsActor = (invoker, cost, model) => gen(function* () {
|
|
|
206815
207559
|
context: rest.context,
|
|
206816
207560
|
file: rest.hints.file,
|
|
206817
207561
|
changes: rest.changes,
|
|
206818
|
-
|
|
206819
|
-
|
|
207562
|
+
kind: decision.kind,
|
|
207563
|
+
attempt: isAttemptDecision(decision)
|
|
206820
207564
|
});
|
|
206821
207565
|
const targetState = decision.kind === "commit" ? decision.to : decision.state;
|
|
206822
207566
|
return {
|
|
206823
207567
|
state: rest.state,
|
|
206824
207568
|
subject: yield* previewSubject(decision, rest),
|
|
206825
|
-
cost: cost ?? null,
|
|
206826
|
-
model: model ?? null,
|
|
206827
|
-
required: yield* buildRequiredScript(rest, decision,
|
|
206828
|
-
optional: openWindowScript(rest, targetState)
|
|
207569
|
+
cost: opts.cost ?? null,
|
|
207570
|
+
model: opts.model ?? null,
|
|
207571
|
+
required: yield* buildRequiredScript(rest, decision, opts.cost, opts.model),
|
|
207572
|
+
optional: openWindowScript(rest, targetState),
|
|
207573
|
+
settled: yield* collapsesToInitialState(rest, decision)
|
|
206829
207574
|
};
|
|
206830
207575
|
});
|
|
206831
|
-
/** Renders `
|
|
206832
|
-
const
|
|
207576
|
+
/** Renders `planLanding`'s result for `gtd land`. Plain text prints the combined script exactly as before (its own outcome rendering, including a genuine no-op's "nothing to do at …" note and the initial-state collapse's `COLLAPSED_TEXT` note, is already baked in — see `renderDecision`'s collapse branch); `--json` additionally carries `required`/`optional`/`script`/`settled`. */
|
|
207577
|
+
const reportLanding = (result, json, write) => {
|
|
207578
|
+
const script = combinedScript(result.required, result.optional);
|
|
206833
207579
|
if (json) write(JSON.stringify({
|
|
206834
207580
|
state: result.state,
|
|
206835
207581
|
subject: result.subject,
|
|
206836
207582
|
...result.cost !== null ? { cost: result.cost } : {},
|
|
206837
207583
|
...result.model !== null ? { model: result.model } : {},
|
|
207584
|
+
script,
|
|
206838
207585
|
required: result.required,
|
|
206839
|
-
optional: result.optional
|
|
207586
|
+
optional: result.optional,
|
|
207587
|
+
settled: result.settled
|
|
206840
207588
|
}) + "\n");
|
|
206841
|
-
else write(
|
|
206842
|
-
};
|
|
206843
|
-
/**
|
|
206844
|
-
* `gtd
|
|
206845
|
-
*
|
|
206846
|
-
*
|
|
206847
|
-
*
|
|
206848
|
-
*
|
|
206849
|
-
*
|
|
206850
|
-
* matched
|
|
206851
|
-
|
|
206852
|
-
|
|
206853
|
-
|
|
206854
|
-
|
|
207589
|
+
else write(script);
|
|
207590
|
+
};
|
|
207591
|
+
/**
|
|
207592
|
+
* `gtd land [--cost=<n>] [--model=<name>]`: land whatever the tree now shows
|
|
207593
|
+
* at the currently resolved rest — authenticated as the rest's own actor,
|
|
207594
|
+
* never a caller-supplied one — recording `--cost`/`--model` as a `Gtd-Cost:`
|
|
207595
|
+
* trailer. `--entry <state>` no longer nests inside this handler — `Cli.ts`'s
|
|
207596
|
+
* `--entry` selector resolves that combination to its own `"entry"` command
|
|
207597
|
+
* kind (see `runEntryCommand`) before `runCommand` ever dispatches here, so a
|
|
207598
|
+
* `land` `Command` is always the ordinary pattern-matched landing.
|
|
207599
|
+
*
|
|
207600
|
+
* Returns the exit code `runCommand`/`Cli.ts`'s `runCli` should use: `0` for
|
|
207601
|
+
* an ordinary landing, `EXIT_SETTLED` (3) when `result.settled` — nothing
|
|
207602
|
+
* owed, but stdout still carries a script a piping driver must run (see
|
|
207603
|
+
* `EXIT_SETTLED`'s own doc comment).
|
|
207604
|
+
*/
|
|
207605
|
+
const runLandCommand = (opts, json, write) => gen(function* () {
|
|
207606
|
+
const result = yield* planLanding(opts);
|
|
207607
|
+
reportLanding(result, json, write);
|
|
207608
|
+
return result.settled ? EXIT_SETTLED : 0;
|
|
206855
207609
|
});
|
|
206856
207610
|
/**
|
|
206857
|
-
* `gtd
|
|
206858
|
-
*
|
|
206859
|
-
* `human` there — see `Cli.ts`'s `--entry` selector): start a brand NEW process at `<state>`
|
|
207611
|
+
* `gtd --entry <state> [--var <name>=<value> ...]` (`actor` always `"human"`
|
|
207612
|
+
* — see `Cli.ts`'s `--entry` selector): start a brand NEW process at `<state>`
|
|
206860
207613
|
* — any declared, non-commit state (see `PatternMachine.enterableStates`) —
|
|
206861
207614
|
* replacing the two former named commands `gtd review <commitish>`/`gtd fix`
|
|
206862
207615
|
* with one generic mechanism. Writes an ordinary turn commit
|
|
@@ -206867,7 +207620,7 @@ const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
|
|
|
206867
207620
|
* the merged `it.vars`, resolved to a commit, and checked sane). Unlike the
|
|
206868
207621
|
* old commands (which required a clean tree and used `commitAsIs`), this
|
|
206869
207622
|
* commits via `commitAllWithPrefix` — capturing whatever the working tree
|
|
206870
|
-
* carries at the moment of entry, exactly like an ordinary `gtd
|
|
207623
|
+
* carries at the moment of entry, exactly like an ordinary `gtd land`
|
|
206871
207624
|
* capture, rather than demanding a clean tree first.
|
|
206872
207625
|
*
|
|
206873
207626
|
* Any failure below is a plain refusal: nothing is written. Checked in order:
|
|
@@ -206900,7 +207653,7 @@ const runEntryCommand = (actor, entryState, varOverrides, json, write, commandLa
|
|
|
206900
207653
|
required: plan.scripts.required,
|
|
206901
207654
|
optional: plan.scripts.optional
|
|
206902
207655
|
}) + "\n");
|
|
206903
|
-
else write(
|
|
207656
|
+
else write(combinedScript(plan.scripts.required, plan.scripts.optional));
|
|
206904
207657
|
});
|
|
206905
207658
|
/**
|
|
206906
207659
|
* `gtd abandon`: end the process currently underway WITHOUT completing it,
|
|
@@ -206947,18 +207700,23 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206947
207700
|
const initial = initialStateOf(def);
|
|
206948
207701
|
const run = yield* currentRun;
|
|
206949
207702
|
if (run.trace.length === 0) {
|
|
207703
|
+
const required = emitScripts({}, [{
|
|
207704
|
+
kind: "outcome",
|
|
207705
|
+
command: abandonNoopOutcome(initial)
|
|
207706
|
+
}]).required;
|
|
206950
207707
|
if (json) write(JSON.stringify({
|
|
206951
207708
|
state: initial,
|
|
206952
|
-
abandoned: false
|
|
207709
|
+
abandoned: false,
|
|
207710
|
+
required,
|
|
207711
|
+
optional: ""
|
|
206953
207712
|
}) + "\n");
|
|
206954
|
-
else write(
|
|
207713
|
+
else write(combinedScript(required, ""));
|
|
206955
207714
|
return;
|
|
206956
207715
|
}
|
|
206957
207716
|
const restState = run.trace[run.trace.length - 1].state;
|
|
206958
207717
|
if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
|
|
206959
207718
|
const closeDecision = yield* decideCloseWindow;
|
|
206960
207719
|
const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
|
|
206961
|
-
const subject = yield* git.lastCommitSubject(run.startParentHash);
|
|
206962
207720
|
const steps = [
|
|
206963
207721
|
...closeDecision.shouldClose ? [{
|
|
206964
207722
|
kind: "gitWrite",
|
|
@@ -206971,6 +207729,10 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206971
207729
|
{
|
|
206972
207730
|
kind: "gitWrite",
|
|
206973
207731
|
command: mixedResetTo(run.startParentHash)
|
|
207732
|
+
},
|
|
207733
|
+
{
|
|
207734
|
+
kind: "outcome",
|
|
207735
|
+
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
206974
207736
|
}
|
|
206975
207737
|
];
|
|
206976
207738
|
const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
|
|
@@ -206982,7 +207744,7 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206982
207744
|
required,
|
|
206983
207745
|
optional: ""
|
|
206984
207746
|
}) + "\n");
|
|
206985
|
-
else write(
|
|
207747
|
+
else write(combinedScript(required, ""));
|
|
206986
207748
|
});
|
|
206987
207749
|
/**
|
|
206988
207750
|
* `gtd restore`: hard-reset HEAD back to the pre-squash tip a squash or
|
|
@@ -207010,14 +207772,20 @@ const runRestoreCommand = (json, write) => gen(function* () {
|
|
|
207010
207772
|
const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
|
|
207011
207773
|
if (!check.ok) return yield* fail$2(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
|
|
207012
207774
|
const after = yield* restAt(tip);
|
|
207013
|
-
const
|
|
207014
|
-
|
|
207015
|
-
|
|
207016
|
-
|
|
207017
|
-
|
|
207018
|
-
|
|
207019
|
-
|
|
207020
|
-
|
|
207775
|
+
const steps = [
|
|
207776
|
+
{
|
|
207777
|
+
kind: "gitWrite",
|
|
207778
|
+
command: hardResetTo(tip)
|
|
207779
|
+
},
|
|
207780
|
+
{
|
|
207781
|
+
kind: "gitWrite",
|
|
207782
|
+
command: deleteRef(HISTORY_REF)
|
|
207783
|
+
},
|
|
207784
|
+
{
|
|
207785
|
+
kind: "outcome",
|
|
207786
|
+
command: restoredOutcome(tip, after.state)
|
|
207787
|
+
}
|
|
207788
|
+
];
|
|
207021
207789
|
const required = emitScripts(headPreconditions(headHash), steps).required;
|
|
207022
207790
|
if (json) write(JSON.stringify({
|
|
207023
207791
|
state: after.state,
|
|
@@ -207027,7 +207795,7 @@ const runRestoreCommand = (json, write) => gen(function* () {
|
|
|
207027
207795
|
required,
|
|
207028
207796
|
optional: ""
|
|
207029
207797
|
}) + "\n");
|
|
207030
|
-
else write(
|
|
207798
|
+
else write(combinedScript(required, ""));
|
|
207031
207799
|
});
|
|
207032
207800
|
/**
|
|
207033
207801
|
* The mode's own resolved VALIDATE command, rendered against `file` — the
|
|
@@ -207066,50 +207834,135 @@ const resolveSelfValidateCommand = (def, mode, file, context) => gen(function* (
|
|
|
207066
207834
|
* human or a simple driver who reads the prompt and hands it to an agent, so
|
|
207067
207835
|
* the agent self-validates); withheld from `gtd next --json`, where the
|
|
207068
207836
|
* driving loop instead runs `gtd validate` after the turn and re-prompts on
|
|
207069
|
-
* findings (see the
|
|
207070
|
-
*
|
|
207071
|
-
*
|
|
207072
|
-
*
|
|
207837
|
+
* findings (see the README's minimal driver, and `fixPromptInstruction` below
|
|
207838
|
+
* — its driver-side counterpart). This is advisory: `gtd land` embeds that same
|
|
207839
|
+
* command ahead of its own commit and REFUSES a turn whose steering file is
|
|
207840
|
+
* invalid (see `planLanding`), so a malformed file is never captured whether
|
|
207841
|
+
* or not this instruction was followed.
|
|
207073
207842
|
*/
|
|
207074
207843
|
const selfValidateInstruction = (command, file) => `\nBefore finishing your turn, run \`${command}\` — it checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
|
|
207844
|
+
/**
|
|
207845
|
+
* The driver-side counterpart of `selfValidateInstruction` above — the same
|
|
207846
|
+
* gate, expressed for a loop that RUNS the validate script itself and
|
|
207847
|
+
* re-prompts the same agent session on a non-zero exit, rather than for an
|
|
207848
|
+
* agent that self-validates before finishing. Wrapped onto the emitted
|
|
207849
|
+
* validate script's last command via `Emit.ts`'s `onFailure` (see
|
|
207850
|
+
* `runValidateCommand`), so a driver's fix re-prompt is exactly this text
|
|
207851
|
+
* plus the script's own captured findings — never hand-composed in bash.
|
|
207852
|
+
*/
|
|
207853
|
+
const fixPromptInstruction = (file) => `Your last turn does not pass its own validation script. Fix these format violations in ${file}, then finish:`;
|
|
207075
207854
|
/** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
|
|
207076
207855
|
const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
|
|
207077
|
-
/**
|
|
207078
|
-
|
|
207079
|
-
|
|
207080
|
-
|
|
207081
|
-
|
|
207082
|
-
|
|
207083
|
-
|
|
207084
|
-
|
|
207085
|
-
|
|
207086
|
-
|
|
207087
|
-
|
|
207088
|
-
|
|
207089
|
-
|
|
207090
|
-
|
|
207856
|
+
/**
|
|
207857
|
+
* Resolve the resting state's own steering-file validate script — the SAME
|
|
207858
|
+
* script both `gtd validate --json` (`runValidateCommand`, its thin caller
|
|
207859
|
+
* now) and `gtd next --json`'s embedded `validate` field emit, from one
|
|
207860
|
+
* shared resolver so the two surfaces can't drift. `undefined` = nothing to
|
|
207861
|
+
* validate: no `file:`+`mode:` declared, or the declared file is absent from
|
|
207862
|
+
* the working tree. An unknown `mode:` FAILS this Effect (exactly as
|
|
207863
|
+
* `runValidateCommand` always has) — `runNextCommand` is the one caller that
|
|
207864
|
+
* degrades that failure to omitting `validate`, mirroring how the plain-text
|
|
207865
|
+
* self-validation instruction already degrades on the same failure.
|
|
207866
|
+
*/
|
|
207867
|
+
const resolveValidateScript = (rest) => gen(function* () {
|
|
207868
|
+
const file = rest.hints.file;
|
|
207869
|
+
const mode = rest.stateDef.mode;
|
|
207870
|
+
if (file === void 0 || mode === void 0) return void 0;
|
|
207871
|
+
const fs = yield* FileSystem;
|
|
207872
|
+
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
207873
|
+
if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) return void 0;
|
|
207874
|
+
const resolved = resolveSteeringMode(rest.def, mode);
|
|
207875
|
+
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
207876
|
+
const commands = yield* renderSteeringCommands(resolved, file, rest.context);
|
|
207877
|
+
const lastIndex = commands.length - 1;
|
|
207878
|
+
const steps = commands.map((command, index) => ({
|
|
207879
|
+
kind: "command",
|
|
207880
|
+
command,
|
|
207881
|
+
...index === lastIndex && resolved.validate?.kind === "command" ? { onFailure: fixPromptInstruction(file) } : {}
|
|
207882
|
+
}));
|
|
207883
|
+
return {
|
|
207884
|
+
file,
|
|
207885
|
+
mode,
|
|
207886
|
+
script: emitScripts(headPreconditions(rest.context.currentCommit), steps).required
|
|
207887
|
+
};
|
|
207888
|
+
});
|
|
207091
207889
|
/** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction (naming `selfValidateCommand`, when resolved) when the rest is a validatable prompt. */
|
|
207092
207890
|
const nextPlainOutput = (rendered, selfValidateCommand) => {
|
|
207093
207891
|
const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
|
|
207094
207892
|
return emitsValidatablePrompt(rendered) && selfValidateCommand !== void 0 ? base + selfValidateInstruction(selfValidateCommand, rendered.file) : base;
|
|
207095
207893
|
};
|
|
207894
|
+
/**
|
|
207895
|
+
* `gtd next --json`'s output: the whole BEAT DOCUMENT (`src/Beat.ts`'s
|
|
207896
|
+
* `beatDocument`) for the resolved rest — one self-describing, POLLABLE line.
|
|
207897
|
+
* `stalledAt` (a pure read off history) and the dirty-tree test both feed
|
|
207898
|
+
* `beatKindOf`, which picks the driver-facing `kind`; the dispatch block
|
|
207899
|
+
* (`session`/`validate`) is resolved only when `kind === "prompt"`, since
|
|
207900
|
+
* `beatDocument` drops it at every other kind regardless. Nothing is written
|
|
207901
|
+
* by any of this — `resolveSession` derives the same id for a peek as it
|
|
207902
|
+
* would for a dispatch, and `resolveValidateScript` only READS the file/mode
|
|
207903
|
+
* it inspects — so there is nothing left for a separate claiming form to
|
|
207904
|
+
* protect (see `runNextCommand` below).
|
|
207905
|
+
*/
|
|
207906
|
+
const nextBeatOutput = (rest, rendered) => gen(function* () {
|
|
207907
|
+
const kind = beatKindOf({
|
|
207908
|
+
contentKind: rendered.kind,
|
|
207909
|
+
dirty: rest.changes.length > 0,
|
|
207910
|
+
stalled: stalledAt(rest)
|
|
207911
|
+
});
|
|
207912
|
+
const session = kind === "prompt" ? resolveSession(rendered.memory, rendered.memoryResumed) : void 0;
|
|
207913
|
+
const resolvedValidate = kind === "prompt" ? yield* resolveValidateScript(rest).pipe(catchAll(() => succeed$3(void 0))) : void 0;
|
|
207914
|
+
const validate = resolvedValidate !== void 0 && resolvedValidate.script.length > 0 ? resolvedValidate.script : void 0;
|
|
207915
|
+
return beatDocument({
|
|
207916
|
+
rendered,
|
|
207917
|
+
kind,
|
|
207918
|
+
log: yield* loopLogPath,
|
|
207919
|
+
...session !== void 0 ? { session: {
|
|
207920
|
+
id: session.sessionId,
|
|
207921
|
+
resume: session.resume
|
|
207922
|
+
} } : {},
|
|
207923
|
+
...validate !== void 0 ? { validate } : {}
|
|
207924
|
+
});
|
|
207925
|
+
});
|
|
207926
|
+
/**
|
|
207927
|
+
* `gtd next`: pure emitter of the resolved rest's rendered content — no
|
|
207928
|
+
* mutation at all. Plain-text output is the human rendering, unchanged: the
|
|
207929
|
+
* rendered content plus (when resolvable) the self-validation instruction;
|
|
207930
|
+
* `--json` hands off to `nextBeatOutput` above. Nothing is written either
|
|
207931
|
+
* way, so a peek and a would-be dispatch are the same call — there is no
|
|
207932
|
+
* separate claiming form any more.
|
|
207933
|
+
*/
|
|
207096
207934
|
const runNextCommand = (json, write) => gen(function* () {
|
|
207097
207935
|
const rest = yield* currentRest;
|
|
207098
207936
|
const rendered = yield* renderRest(rest);
|
|
207099
|
-
|
|
207100
|
-
|
|
207937
|
+
if (json) {
|
|
207938
|
+
write(yield* nextBeatOutput(rest, rendered));
|
|
207939
|
+
return;
|
|
207940
|
+
}
|
|
207941
|
+
if (rendered.kind === "prompt" && rest.changes.length === 0 && stalledAt(rest)) {
|
|
207942
|
+
write(stallDiagnosis(rendered.state, rendered.actor));
|
|
207943
|
+
return;
|
|
207944
|
+
}
|
|
207945
|
+
const selfValidateCommand = emitsValidatablePrompt(rendered) ? yield* resolveSelfValidateCommand(rest.def, rendered.mode, rendered.file, rest.context).pipe(catchAll(() => succeed$3(void 0))) : void 0;
|
|
207946
|
+
write(nextPlainOutput(rendered, selfValidateCommand));
|
|
207101
207947
|
});
|
|
207102
207948
|
/**
|
|
207103
207949
|
* `gtd validate [--json]`: emit the script that would format (in place) then
|
|
207104
207950
|
* validate the steering file the resolved rest declares (`file:` rendered,
|
|
207105
|
-
* `mode:` selecting how) — the SAME commands `gtd
|
|
207106
|
-
* embeds ahead of its own commit (see `
|
|
207951
|
+
* `mode:` selecting how) — the SAME commands `gtd land`'s capture guard now
|
|
207952
|
+
* embeds ahead of its own commit (see `planLanding`), rendered here for a
|
|
207107
207953
|
* human or agent to run directly. gtd itself reads no file and executes
|
|
207108
207954
|
* nothing: a state with no `file:`/`mode:`, or a file absent from the working
|
|
207109
207955
|
* tree, has nothing to validate (`script: ""`, exit 0 either way) — the
|
|
207110
207956
|
* verdict now lives in the emitted script's own future exit code, not this
|
|
207111
207957
|
* command's, so this never fails on a bad file. `RepoFiles`/`FileSystem` is
|
|
207112
207958
|
* used only to check the file's PRESENCE, never to read or judge its content.
|
|
207959
|
+
* When the resolved mode's validate half is a `command` (never a `"builtin"`
|
|
207960
|
+
* validator, which renders no shell command at all), the LAST rendered
|
|
207961
|
+
* command carries `onFailure: fixPromptInstruction(file)` (`Emit.ts`'s
|
|
207962
|
+
* `failurePromptWrapper`) — so a non-zero exit from the script prints the
|
|
207963
|
+
* COMPLETE ready-to-send fix prompt (instruction + findings) rather than bare
|
|
207964
|
+
* findings, letting a driver treat the script's captured output as opaque
|
|
207965
|
+
* prompt text (see the README's minimal driver).
|
|
207113
207966
|
*/
|
|
207114
207967
|
const runValidateCommand = (json, write) => gen(function* () {
|
|
207115
207968
|
const rest = yield* currentRest;
|
|
@@ -207124,30 +207977,16 @@ const runValidateCommand = (json, write) => gen(function* () {
|
|
|
207124
207977
|
}) + "\n");
|
|
207125
207978
|
else write(`nothing to validate at "${rest.state}"\n`);
|
|
207126
207979
|
};
|
|
207127
|
-
|
|
207980
|
+
const resolved = yield* resolveValidateScript(rest);
|
|
207981
|
+
if (resolved === void 0) {
|
|
207128
207982
|
emitNothingToValidate();
|
|
207129
207983
|
return;
|
|
207130
207984
|
}
|
|
207131
|
-
const fs = yield* FileSystem;
|
|
207132
|
-
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
207133
|
-
if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) {
|
|
207134
|
-
emitNothingToValidate();
|
|
207135
|
-
return;
|
|
207136
|
-
}
|
|
207137
|
-
const resolved = resolveSteeringMode(rest.def, mode);
|
|
207138
|
-
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
207139
|
-
const commands = yield* renderSteeringCommands(resolved, file, rest.context);
|
|
207140
|
-
const script = emitScripts(headPreconditions(rest.context.currentCommit), commands.map((command) => ({
|
|
207141
|
-
kind: "command",
|
|
207142
|
-
command
|
|
207143
|
-
}))).required;
|
|
207144
207985
|
if (json) write(JSON.stringify({
|
|
207145
207986
|
state: rest.state,
|
|
207146
|
-
|
|
207147
|
-
mode,
|
|
207148
|
-
script
|
|
207987
|
+
...resolved
|
|
207149
207988
|
}) + "\n");
|
|
207150
|
-
else write(script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
207989
|
+
else write(resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
207151
207990
|
});
|
|
207152
207991
|
/**
|
|
207153
207992
|
* `gtd check <mode> <file>`: read `<file>` and run the BUILT-IN steering
|
|
@@ -207199,7 +208038,7 @@ const runCheckCommand = (mode, file, json, write) => gen(function* () {
|
|
|
207199
208038
|
else write(errors.join("\n") + "\n");
|
|
207200
208039
|
return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s))`));
|
|
207201
208040
|
});
|
|
207202
|
-
/** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd
|
|
208041
|
+
/** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd land` would match against. */
|
|
207203
208042
|
const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
|
|
207204
208043
|
const matchedRow = onEdges.find(([patternStr]) => {
|
|
207205
208044
|
const parsed = parsePattern(patternStr);
|
|
@@ -207403,20 +208242,20 @@ const assertInitLocation = (git, fs) => gen(function* () {
|
|
|
207403
208242
|
*/
|
|
207404
208243
|
const needsOf = (kind) => {
|
|
207405
208244
|
switch (kind) {
|
|
207406
|
-
case "lsp":
|
|
208245
|
+
case "lsp":
|
|
208246
|
+
case "install": return "none";
|
|
207407
208247
|
case "init":
|
|
207408
208248
|
case "check": return "fs";
|
|
207409
208249
|
case "visualize": return "config";
|
|
207410
208250
|
default: return "state";
|
|
207411
208251
|
}
|
|
207412
208252
|
};
|
|
207413
|
-
/** Dispatches to the named `run*Command` handler for every `Command.kind` — the counterpart of `Cli.ts`'s `parseArgv`, which has already validated every field a handler receives. */
|
|
207414
|
-
const
|
|
208253
|
+
/** Dispatches to the named `run*Command` handler for every `Command.kind` EXCEPT `"land"` (which alone returns a non-zero exit code — see `dispatchCommand`) — the counterpart of `Cli.ts`'s `parseArgv`, which has already validated every field a handler receives. */
|
|
208254
|
+
const dispatchVoidCommand = (command, json, write) => {
|
|
207415
208255
|
switch (command.kind) {
|
|
207416
208256
|
case "lsp": return runLspCommand();
|
|
207417
208257
|
case "init": return runInitCommand(json, write);
|
|
207418
208258
|
case "visualize": return runVisualizeCommand(command.port, command.open, json, write);
|
|
207419
|
-
case "step": return runStepCommand(command.actor, command.cost, command.model, json, write);
|
|
207420
208259
|
case "entry": return runEntryCommand(command.actor, command.state, command.vars, json, write, command.label);
|
|
207421
208260
|
case "abandon": return runAbandonCommand(json, write);
|
|
207422
208261
|
case "restore": return runRestoreCommand(json, write);
|
|
@@ -207424,18 +208263,35 @@ const dispatchCommand = (command, json, write) => {
|
|
|
207424
208263
|
case "status": return runStatusCommand(json, write);
|
|
207425
208264
|
case "validate": return runValidateCommand(json, write);
|
|
207426
208265
|
case "check": return runCheckCommand(command.mode, command.file, json, write);
|
|
208266
|
+
case "install": return runInstallCommand(json, write);
|
|
207427
208267
|
}
|
|
207428
208268
|
};
|
|
207429
208269
|
/**
|
|
208270
|
+
* Dispatches every `Command` to its exit code — `"land"` alone can return
|
|
208271
|
+
* non-zero (`EXIT_SETTLED`, via `runLandCommand`); every other kind always
|
|
208272
|
+
* succeeds at `0` (`dispatchVoidCommand`, piped through `Effect.as(0)`).
|
|
208273
|
+
* Splitting the exhaustive switch out to `dispatchVoidCommand` keeps this
|
|
208274
|
+
* function's own branching to the one kind that actually varies.
|
|
208275
|
+
*/
|
|
208276
|
+
const dispatchCommand = (command, json, write) => {
|
|
208277
|
+
if (command.kind === "land") return runLandCommand({
|
|
208278
|
+
...command.cost !== void 0 ? { cost: command.cost } : {},
|
|
208279
|
+
...command.model !== void 0 ? { model: command.model } : {}
|
|
208280
|
+
}, json, write);
|
|
208281
|
+
return dispatchVoidCommand(command, json, write).pipe(as(0));
|
|
208282
|
+
};
|
|
208283
|
+
/**
|
|
207430
208284
|
* The one entry point `Cli.ts`'s `runCli` calls for a resolved `Command`:
|
|
207431
208285
|
* dispatches to the matching `run*Command` handler (see `dispatchCommand`),
|
|
207432
208286
|
* wrapped in the repo-root guard exactly when `needsOf(command.kind) ===
|
|
207433
208287
|
* "state"` — `lsp`/`init`/`visualize`/`check` (the `standaloneKinds`) run
|
|
207434
208288
|
* bare. `Cli.ts` has already validated every field on `command` (arity, flag
|
|
207435
|
-
* scope, decoding), so nothing here re-parses argv.
|
|
208289
|
+
* scope, decoding), so nothing here re-parses argv. Returns the exit code
|
|
208290
|
+
* `Cli.ts`'s `runCli` should use — `0` for every command except a `land` that
|
|
208291
|
+
* settles (`EXIT_SETTLED`).
|
|
207436
208292
|
*
|
|
207437
208293
|
* No review-window close/open bracket runs here any more: every state-command
|
|
207438
|
-
* handler (see `
|
|
208294
|
+
* handler (see `planLanding`) now decides for itself, off `ReviewWindow.ts`'s
|
|
207439
208295
|
* pure `decideCloseWindow`/`decideOpenWindow`, whether a close/open belongs in
|
|
207440
208296
|
* the script it emits — there is no in-process HEAD to rewind ahead of time
|
|
207441
208297
|
* (gtd itself no longer writes git for a state command at all).
|
|
@@ -207447,7 +208303,7 @@ const runCommand = (command, json, write) => {
|
|
|
207447
208303
|
const git = yield* GitService;
|
|
207448
208304
|
const fs = yield* FileSystem;
|
|
207449
208305
|
yield* assertRunningFromRepoRoot(git, fs);
|
|
207450
|
-
yield* dispatch;
|
|
208306
|
+
return yield* dispatch;
|
|
207451
208307
|
});
|
|
207452
208308
|
};
|
|
207453
208309
|
//#endregion
|
|
@@ -207506,21 +208362,21 @@ const FLAGS = [
|
|
|
207506
208362
|
name: "--cost",
|
|
207507
208363
|
arity: 1,
|
|
207508
208364
|
repeatable: false,
|
|
207509
|
-
scope: (kind) => kind === "
|
|
208365
|
+
scope: (kind) => kind === "land",
|
|
207510
208366
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
207511
|
-
scopeError: "gtd: --cost is only valid for `gtd
|
|
208367
|
+
scopeError: "gtd: --cost is only valid for `gtd land` — an entry is not a metered agent turn",
|
|
207512
208368
|
valueHint: "<n>",
|
|
207513
|
-
help: ["(gtd
|
|
208369
|
+
help: ["(gtd land only) record the invocation's token cost"]
|
|
207514
208370
|
},
|
|
207515
208371
|
{
|
|
207516
208372
|
name: "--model",
|
|
207517
208373
|
arity: 1,
|
|
207518
208374
|
repeatable: false,
|
|
207519
|
-
scope: (kind) => kind === "
|
|
208375
|
+
scope: (kind) => kind === "land",
|
|
207520
208376
|
decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --model must be a non-empty, single-line value") : right(raw),
|
|
207521
|
-
scopeError: "gtd: --model is only valid for `gtd
|
|
208377
|
+
scopeError: "gtd: --model is only valid for `gtd land` — an entry is not a metered agent turn",
|
|
207522
208378
|
valueHint: "<name>",
|
|
207523
|
-
help: ["(gtd
|
|
208379
|
+
help: ["(gtd land only, with --cost) tag that cost's model"]
|
|
207524
208380
|
},
|
|
207525
208381
|
{
|
|
207526
208382
|
name: "--entry",
|
|
@@ -207528,14 +208384,12 @@ const FLAGS = [
|
|
|
207528
208384
|
repeatable: false,
|
|
207529
208385
|
scope: (kind) => kind === "entry",
|
|
207530
208386
|
decode: ([raw]) => right(raw ?? ""),
|
|
207531
|
-
scopeError: "gtd: --entry is only valid
|
|
208387
|
+
scopeError: "gtd: --entry is only valid with no other command — use the bare `gtd --entry <state>` form; landing and entering are different verbs",
|
|
207532
208388
|
valueHint: "<state>",
|
|
207533
208389
|
help: [
|
|
207534
|
-
"(
|
|
207535
|
-
"
|
|
207536
|
-
"
|
|
207537
|
-
"combinable with --cost/--model (an entry is not a metered",
|
|
207538
|
-
"agent turn)"
|
|
208390
|
+
"(with no command at all) start a brand new process at",
|
|
208391
|
+
"<state> — any declared, non-commit state — authenticated",
|
|
208392
|
+
"as human"
|
|
207539
208393
|
]
|
|
207540
208394
|
},
|
|
207541
208395
|
{
|
|
@@ -207589,20 +208443,22 @@ const COMMAND_ROWS = [
|
|
|
207589
208443
|
]
|
|
207590
208444
|
},
|
|
207591
208445
|
{
|
|
207592
|
-
token: "
|
|
207593
|
-
kind: "
|
|
207594
|
-
arity:
|
|
208446
|
+
token: "land",
|
|
208447
|
+
kind: "land",
|
|
208448
|
+
arity: "none",
|
|
207595
208449
|
details: [
|
|
207596
|
-
"
|
|
207597
|
-
"
|
|
207598
|
-
"(
|
|
207599
|
-
"
|
|
207600
|
-
"
|
|
207601
|
-
"
|
|
207602
|
-
"
|
|
207603
|
-
"
|
|
207604
|
-
"
|
|
207605
|
-
"
|
|
208450
|
+
"Land whatever the tree now shows at the currently",
|
|
208451
|
+
"resolved rest — a human capture, an agent/check turn, an",
|
|
208452
|
+
"empty attempt (a fruitless prompt turn), or a squash — and",
|
|
208453
|
+
"print the script that records it; a driver runs the",
|
|
208454
|
+
"script, e.g. `gtd land | bash`. Pass --cost=<n>",
|
|
208455
|
+
"(optionally --model=<name>) to record the just-finished",
|
|
208456
|
+
"invocation's token cost and model on the turn commit",
|
|
208457
|
+
"(summed into it.processCost/processCostByModel). Exits 0",
|
|
208458
|
+
"when a script is emitted (or a benign no-op at a clean",
|
|
208459
|
+
"message rest), 3 when SETTLED — nothing owed: a no-op at a",
|
|
208460
|
+
"script rest, or the initial-state collapse (stdout still",
|
|
208461
|
+
"carries the script) — 1 on any refusal"
|
|
207606
208462
|
]
|
|
207607
208463
|
},
|
|
207608
208464
|
{
|
|
@@ -207634,7 +208490,14 @@ const COMMAND_ROWS = [
|
|
|
207634
208490
|
token: "next",
|
|
207635
208491
|
kind: "next",
|
|
207636
208492
|
arity: "none",
|
|
207637
|
-
details: [
|
|
208493
|
+
details: [
|
|
208494
|
+
"Print the resolved rest's rendered script/prompt/message",
|
|
208495
|
+
"(no mutation, safe to poll). --json emits the whole beat",
|
|
208496
|
+
"document instead: kind (capture|message|script|prompt|",
|
|
208497
|
+
"stalled) selects what a driver does, content is what it",
|
|
208498
|
+
"runs or shows, plus the prompt session, model, validate",
|
|
208499
|
+
"script, log path and the resting state's own fields"
|
|
208500
|
+
]
|
|
207638
208501
|
},
|
|
207639
208502
|
{
|
|
207640
208503
|
token: "status",
|
|
@@ -207654,7 +208517,10 @@ const COMMAND_ROWS = [
|
|
|
207654
208517
|
"own exit code/output. Always exits 0; --json emits",
|
|
207655
208518
|
"{state, file?, mode?, script} (script is \"\" when there is",
|
|
207656
208519
|
"nothing to validate; plain text prints \"nothing to",
|
|
207657
|
-
"validate\" in that case)"
|
|
208520
|
+
"validate\" in that case). On a non-zero validate exit",
|
|
208521
|
+
"the emitted script prints a ready-to-send fix prompt",
|
|
208522
|
+
"(instruction + findings) and exits with the",
|
|
208523
|
+
"validator's own code"
|
|
207658
208524
|
]
|
|
207659
208525
|
},
|
|
207660
208526
|
{
|
|
@@ -207686,6 +208552,19 @@ const COMMAND_ROWS = [
|
|
|
207686
208552
|
"with <mode>/<file> given explicitly. This is what a",
|
|
207687
208553
|
"workflow's emitted validation script invokes as a leaf step"
|
|
207688
208554
|
]
|
|
208555
|
+
},
|
|
208556
|
+
{
|
|
208557
|
+
token: "install",
|
|
208558
|
+
kind: "install",
|
|
208559
|
+
arity: "none",
|
|
208560
|
+
details: [
|
|
208561
|
+
"Print a complete, self-contained briefing that teaches an",
|
|
208562
|
+
"agent (or a human) to build a gtd driver in any shell or",
|
|
208563
|
+
"runtime — the self-serve version of README's 'Writing",
|
|
208564
|
+
"your own driver'. Writes nothing: this installs knowledge",
|
|
208565
|
+
"into the calling agent's context, not files on disk. Runs",
|
|
208566
|
+
"from any directory, in or out of a repository"
|
|
208567
|
+
]
|
|
207689
208568
|
}
|
|
207690
208569
|
];
|
|
207691
208570
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
@@ -207698,8 +208577,10 @@ const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
|
207698
208577
|
* enterable states.
|
|
207699
208578
|
*/
|
|
207700
208579
|
const REMOVED = {
|
|
207701
|
-
|
|
207702
|
-
|
|
208580
|
+
step: "gtd: `gtd step <actor>` is gone — landing is actorless; run `gtd land` instead (`gtd --entry <state>` for entries)",
|
|
208581
|
+
review: "gtd: `gtd review <commitish>` is gone — this workflow's own state names aren't known to gtd; run `gtd --entry <review-state> --var <name>=<value> ...` instead — run it with an unknown <review-state> to see this workflow's own enterable states",
|
|
208582
|
+
fix: "gtd: `gtd fix` is gone — this workflow's own state names aren't known to gtd; run `gtd --entry <fix-state>` instead — run it with an unknown <fix-state> to see this workflow's own enterable states",
|
|
208583
|
+
loop: "gtd: `gtd loop` is gone — gtd decides and prints, a driver executes. Copy the driver from the README's \"A complete minimal driver\" section and run that instead"
|
|
207703
208584
|
};
|
|
207704
208585
|
const COLUMN = 19;
|
|
207705
208586
|
const renderBlock = (header, lines) => {
|
|
@@ -207707,14 +208588,7 @@ const renderBlock = (header, lines) => {
|
|
|
207707
208588
|
const headerCell = ` ${header}`;
|
|
207708
208589
|
return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
|
|
207709
208590
|
};
|
|
207710
|
-
const
|
|
207711
|
-
"Launch the loop driver (bin/gtd), which repeatedly drives",
|
|
207712
|
-
"an agent through gtd next/gtd step calls until the",
|
|
207713
|
-
"workflow rests at a human gate (a non-autonomous state)",
|
|
207714
|
-
"or settles. A bare gtd invocation and gtd loop both",
|
|
207715
|
-
"launch it identically"
|
|
207716
|
-
]);
|
|
207717
|
-
const ENTRY_SHORT_FORM = renderBlock("(no command) --entry <state>", ["Short form of 'step human --entry <state>' — starts a new", "process authenticated as human, e.g. 'gtd --entry <state>'"]);
|
|
208591
|
+
const ENTRY_SHORT_FORM = renderBlock("(no command) --entry <state>", ["Starts a new process authenticated as human, e.g.", "'gtd --entry <state>'"]);
|
|
207718
208592
|
const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
|
|
207719
208593
|
const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
|
|
207720
208594
|
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--entry", "--var"]);
|
|
@@ -207727,7 +208601,14 @@ const renderHelp = () => {
|
|
|
207727
208601
|
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : `${row.token} <${row.arity.name}>`;
|
|
207728
208602
|
return renderBlock(header, row.details);
|
|
207729
208603
|
});
|
|
207730
|
-
return `Usage: gtd [command] [options]\n\nCommands:\n${
|
|
208604
|
+
return `Usage: gtd [command] [options]\n\nCommands:\n${[
|
|
208605
|
+
commandBlocks[0],
|
|
208606
|
+
commandBlocks[1],
|
|
208607
|
+
ENTRY_SHORT_FORM,
|
|
208608
|
+
...commandBlocks.slice(2),
|
|
208609
|
+
VERSION_BLOCK,
|
|
208610
|
+
HELP_BLOCK
|
|
208611
|
+
].join("")}\nOptions:\n${FLAGS.map((row) => renderBlock(flagHeader(row), row.help)).join("")}${renderBlock("--version, -v", ["Print version and exit"]) + renderBlock("--help, -h", ["Print this help and exit"])}`;
|
|
207731
208612
|
};
|
|
207732
208613
|
/**
|
|
207733
208614
|
* The stderr line for a CLI error: a `gtd: ` prefix UNLESS the message
|
|
@@ -207834,6 +208715,12 @@ const decodeFlags = (byFlag) => {
|
|
|
207834
208715
|
}
|
|
207835
208716
|
return right(bag);
|
|
207836
208717
|
};
|
|
208718
|
+
/** Assembles a `land` `Command` from its already-scope-checked flag bag — split out of `parseArgv` so its two independent, omit-when-absent optional fields don't inflate that function's own branching. */
|
|
208719
|
+
const buildLandCommand = (bag) => ({
|
|
208720
|
+
kind: "land",
|
|
208721
|
+
...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
|
|
208722
|
+
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
208723
|
+
});
|
|
207837
208724
|
const parseArgv = (argv) => {
|
|
207838
208725
|
if (argv.includes("--help") || argv.includes("-h")) return {
|
|
207839
208726
|
kind: "output",
|
|
@@ -207859,12 +208746,12 @@ const parseArgv = (argv) => {
|
|
|
207859
208746
|
};
|
|
207860
208747
|
const row = first === void 0 ? void 0 : commandByToken(first);
|
|
207861
208748
|
const removedMessage = first === void 0 ? void 0 : REMOVED[first];
|
|
207862
|
-
const selectsEntry = entryPresent &&
|
|
208749
|
+
const selectsEntry = entryPresent && first === void 0;
|
|
207863
208750
|
const kind = selectsEntry ? "entry" : row?.kind;
|
|
207864
208751
|
if (row === void 0 && removedMessage === void 0 && !selectsEntry) {
|
|
207865
208752
|
const violation = scopeViolation(void 0, present);
|
|
207866
208753
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
207867
|
-
if (first === void 0) return usagePlan("gtd: missing command — see usage above (`gtd --help`)", jsonSeen, renderHelp());
|
|
208754
|
+
if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; copy one from the README's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
|
|
207868
208755
|
return usagePlan(`unknown command '${first}'`, jsonSeen);
|
|
207869
208756
|
}
|
|
207870
208757
|
if (removedMessage !== void 0 && !selectsEntry) {
|
|
@@ -207875,8 +208762,7 @@ const parseArgv = (argv) => {
|
|
|
207875
208762
|
if (kind === "entry" && first === void 0) {
|
|
207876
208763
|
if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
|
|
207877
208764
|
} else {
|
|
207878
|
-
const
|
|
207879
|
-
const arityMsg = arityError(first, restPositionals, arityRow.arity);
|
|
208765
|
+
const arityMsg = arityError(first, restPositionals, row.arity);
|
|
207880
208766
|
if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
|
|
207881
208767
|
}
|
|
207882
208768
|
const violation = scopeViolation(kind, present);
|
|
@@ -207885,27 +208771,21 @@ const parseArgv = (argv) => {
|
|
|
207885
208771
|
if (isLeft(decoded)) return usagePlan(decoded.left, jsonSeen);
|
|
207886
208772
|
const bag = decoded.right;
|
|
207887
208773
|
const json = present.has("--json");
|
|
207888
|
-
if (kind === "
|
|
208774
|
+
if (kind === "land") {
|
|
207889
208775
|
if (bag["--model"] !== void 0 && bag["--cost"] === void 0) return usagePlan("gtd: --model requires --cost — it tags the recorded cost with the model that ran", json);
|
|
207890
208776
|
return {
|
|
207891
208777
|
kind: "command",
|
|
207892
|
-
command:
|
|
207893
|
-
kind: "step",
|
|
207894
|
-
actor: restPositionals[0],
|
|
207895
|
-
...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
|
|
207896
|
-
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
207897
|
-
},
|
|
208778
|
+
command: buildLandCommand(bag),
|
|
207898
208779
|
json
|
|
207899
208780
|
};
|
|
207900
208781
|
}
|
|
207901
208782
|
if (kind === "entry") {
|
|
207902
|
-
const
|
|
207903
|
-
const label = first === void 0 ? `gtd --entry ${entryRaw}` : `gtd step ${actor} --entry ${entryRaw}`;
|
|
208783
|
+
const label = `gtd --entry ${entryRaw}`;
|
|
207904
208784
|
return {
|
|
207905
208785
|
kind: "command",
|
|
207906
208786
|
command: {
|
|
207907
208787
|
kind: "entry",
|
|
207908
|
-
actor,
|
|
208788
|
+
actor: "human",
|
|
207909
208789
|
state: entryRaw,
|
|
207910
208790
|
vars: bag["--var"] ?? {},
|
|
207911
208791
|
label
|
|
@@ -207979,7 +208859,9 @@ const runCli = (argv, io) => {
|
|
|
207979
208859
|
io.stderr(`${cliErrorLine(plan.message)}\n`);
|
|
207980
208860
|
io.exit(1);
|
|
207981
208861
|
});
|
|
207982
|
-
return runCommand(plan.command, plan.json, io.stdout).pipe(
|
|
208862
|
+
return runCommand(plan.command, plan.json, io.stdout).pipe(map$4((code) => {
|
|
208863
|
+
if (code !== 0) io.exit(code);
|
|
208864
|
+
}), provide$2(io.layers()), sandbox, catchAll((cause) => isInterruptedOnly(cause) ? failCause$2(cause) : report(io, plan.json)(cause)));
|
|
207983
208865
|
};
|
|
207984
208866
|
//#endregion
|
|
207985
208867
|
//#region src/main.ts
|