@pmelab/gtd 8.5.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 +468 -150
- package/dist/gtd.bundle.mjs +1989 -515
- package/package.json +2 -3
- package/bin/gtd +0 -715
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
|
|
@@ -191387,6 +191420,20 @@ const STEERING_FORMATS = /* @__PURE__ */ new Map([["qa", QA_FORMAT], ["review",
|
|
|
191387
191420
|
const builtInModeNames = () => [...STEERING_FORMATS.keys()];
|
|
191388
191421
|
/** The built-in `SteeringFormat` named `mode`, or `undefined` when `mode` names no built-in. */
|
|
191389
191422
|
const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
|
|
191423
|
+
/**
|
|
191424
|
+
* The literal (unrendered) Eta template a workflow compiler seeds as a
|
|
191425
|
+
* built-in mode's own `validate:` command — quoted so a file path containing
|
|
191426
|
+
* spaces survives `renderModeCommand`'s interpolation.
|
|
191427
|
+
*/
|
|
191428
|
+
const seededValidateCommand = (mode) => `gtd check ${mode} '<%= it.file %>'`;
|
|
191429
|
+
/**
|
|
191430
|
+
* True when `command` is EXACTLY `mode`'s own seeded template string — a
|
|
191431
|
+
* literal comparison against the declared template, never a rendered
|
|
191432
|
+
* command, so recognition doesn't depend on which file the mode is applied
|
|
191433
|
+
* to. Lets a later mode resolver tell gtd's own seeding apart from a user's
|
|
191434
|
+
* hand-written override of the same mode.
|
|
191435
|
+
*/
|
|
191436
|
+
const isSeededValidateCommand = (mode, command) => command === seededValidateCommand(mode);
|
|
191390
191437
|
//#endregion
|
|
191391
191438
|
//#region src/PatternConfig.ts
|
|
191392
191439
|
/**
|
|
@@ -191427,7 +191474,7 @@ const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
|
|
|
191427
191474
|
* label: <string> # optional, opaque display name — never on a commit state
|
|
191428
191475
|
* file: <string> # optional, an Eta template naming the state's steering file — never on a commit state
|
|
191429
191476
|
* mode: <modeName> # optional, requires "file" — a built-in (qa/review) or a `modes:` entry; never on a commit state
|
|
191430
|
-
* 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
|
|
191431
191478
|
* <local>: { machine: <name>, with: { ... } } # a REFERENCE — instantiates <name> as a child, see src/Machines.ts
|
|
191432
191479
|
* ```
|
|
191433
191480
|
*
|
|
@@ -191460,11 +191507,13 @@ const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
|
|
|
191460
191507
|
* `ConfigService` compiles through this module's `compileModesMap` and hands
|
|
191461
191508
|
* back in as `rcModes`, merged per half by `mergeModes`. Underneath BOTH layers
|
|
191462
191509
|
* this compiler seeds `src/SteeringFormats.ts`'s built-in registry names (`qa`/
|
|
191463
|
-
* `review`)
|
|
191464
|
-
*
|
|
191465
|
-
*
|
|
191466
|
-
*
|
|
191467
|
-
*
|
|
191510
|
+
* `review`) with that module's own `seededValidateCommand(name)` template as
|
|
191511
|
+
* their `validate:`, so every compiled definition's `modes` map always carries
|
|
191512
|
+
* a usable `validate:` command for them and a workflow never has to declare
|
|
191513
|
+
* them just to use gtd's own validators — a workflow (or `.gtdrc`) may still
|
|
191514
|
+
* override either half per name, and `src/SteeringFormats.ts`'s
|
|
191515
|
+
* `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
|
|
191516
|
+
* own seeding apart from a genuine user override.
|
|
191468
191517
|
*
|
|
191469
191518
|
* ## File references
|
|
191470
191519
|
*
|
|
@@ -192105,7 +192154,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192105
192154
|
const unknownTopKeys = Object.keys(raw).filter((k) => !KNOWN_TOP_KEYS.has(k));
|
|
192106
192155
|
if (unknownTopKeys.length > 0) errors.push(`unknown top-level key(s) ${unknownTopKeys.join(", ")}`);
|
|
192107
192156
|
const vars = compileVarsMap(raw.vars, errors);
|
|
192108
|
-
const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, {}]));
|
|
192157
|
+
const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
|
|
192109
192158
|
const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
|
|
192110
192159
|
validateMachinesShape(raw.machines, errors);
|
|
192111
192160
|
const flattened = flattenMachines(raw, errors);
|
|
@@ -192138,7 +192187,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192138
192187
|
};
|
|
192139
192188
|
//#endregion
|
|
192140
192189
|
//#region src/workflows/unified.yaml
|
|
192141
|
-
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";
|
|
192142
192191
|
//#endregion
|
|
192143
192192
|
//#region src/workflows/templates.ts
|
|
192144
192193
|
/**
|
|
@@ -192761,13 +192810,15 @@ const makeGitImpl = (executor, root) => {
|
|
|
192761
192810
|
hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
|
|
192762
192811
|
readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
|
|
192763
192812
|
changedPathsSince: (ref) => exec("git", "diff", "--name-status", ref, "HEAD").pipe(map$4(parseNameStatus)),
|
|
192764
|
-
changedPaths: () => gen(function* () {
|
|
192765
|
-
const nameStatusOut = yield* exec("git", "diff", "--name-status", "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
|
|
192813
|
+
changedPaths: (base) => gen(function* () {
|
|
192814
|
+
const nameStatusOut = yield* exec("git", "diff", "--name-status", base ?? "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
|
|
192766
192815
|
const trackedPaths = parseNameStatus(nameStatusOut);
|
|
192767
|
-
const
|
|
192816
|
+
const untrackedAll = (yield* exec("git", "ls-files", "--others", "--exclude-standard", "-z")).split("\0").filter((s) => s.length > 0).map((path) => ({
|
|
192768
192817
|
path,
|
|
192769
192818
|
status: "A"
|
|
192770
192819
|
}));
|
|
192820
|
+
const atBase = base === void 0 ? /* @__PURE__ */ new Set() : new Set((yield* exec("git", "ls-tree", "-r", "--name-only", "-z", base)).split("\0").filter((s) => s.length > 0));
|
|
192821
|
+
const untracked = untrackedAll.filter((entry) => !atBase.has(entry.path));
|
|
192771
192822
|
const seen = /* @__PURE__ */ new Set();
|
|
192772
192823
|
const all = [];
|
|
192773
192824
|
for (const entry of [...trackedPaths, ...untracked]) if (!seen.has(entry.path)) {
|
|
@@ -192780,8 +192831,9 @@ const makeGitImpl = (executor, root) => {
|
|
|
192780
192831
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
192781
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))),
|
|
192782
192833
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
192783
|
-
|
|
192784
|
-
|
|
192834
|
+
gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
|
|
192835
|
+
commitHistory: (base, head = "HEAD") => {
|
|
192836
|
+
const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
|
|
192785
192837
|
const args = [
|
|
192786
192838
|
"git",
|
|
192787
192839
|
"log",
|
|
@@ -192890,6 +192942,79 @@ var CommandRunner = class CommandRunner extends Tag("CommandRunner")() {
|
|
|
192890
192942
|
}));
|
|
192891
192943
|
};
|
|
192892
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
|
|
192893
193018
|
//#region node_modules/eta/dist/index.mjs
|
|
192894
193019
|
var EtaError = class extends Error {
|
|
192895
193020
|
constructor(message) {
|
|
@@ -193455,13 +193580,6 @@ const withHistoryTrailer = (subject, hash) => `${subject}\n\n${HISTORY_TRAILER_P
|
|
|
193455
193580
|
* by a later `gtd restore` command to recover the pre-squash tip.
|
|
193456
193581
|
*/
|
|
193457
193582
|
const parseHistoryTrailer = (message) => HISTORY_TRAILER_RE.exec(message)?.[1];
|
|
193458
|
-
/**
|
|
193459
|
-
* Record the pre-squash tip so a later `gtd restore` can find it. A no-op
|
|
193460
|
-
* (the git call is skipped entirely) when `tipHash === startParentHash`: an
|
|
193461
|
-
* empty process — no turns were ever committed — has no turn chain worth
|
|
193462
|
-
* keeping.
|
|
193463
|
-
*/
|
|
193464
|
-
const retainHistory = (git, tipHash, startParentHash) => tipHash === startParentHash ? _void : git.updateRef(HISTORY_REF, tipHash);
|
|
193465
193583
|
/** A thin wrapper over `git.readRefOption(HISTORY_REF)`. */
|
|
193466
193584
|
const readRetainedHistory = (git) => git.readRefOption(HISTORY_REF);
|
|
193467
193585
|
/**
|
|
@@ -193491,8 +193609,421 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
|
|
|
193491
193609
|
reason: "HEAD has advanced past the squash — restoring would discard commits built on top of it"
|
|
193492
193610
|
};
|
|
193493
193611
|
});
|
|
193494
|
-
|
|
193495
|
-
|
|
193612
|
+
//#endregion
|
|
193613
|
+
//#region src/GitScript.ts
|
|
193614
|
+
/**
|
|
193615
|
+
* Bash-script builders mirroring `GitWriterOperations` (`src/Git.ts`) — one
|
|
193616
|
+
* function per writer method, each returning the equivalent bash as a
|
|
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.
|
|
193621
|
+
*
|
|
193622
|
+
* Pure, like `src/PatternMachine.ts`: no git, no filesystem, no `Effect`, no
|
|
193623
|
+
* IO of any kind. Every export is a plain, total function of its arguments.
|
|
193624
|
+
* `src/Git.ts`'s doc comments on `GitWriterOperations` are the source of
|
|
193625
|
+
* truth for each shape below; read them for WHY, not just WHAT.
|
|
193626
|
+
*/
|
|
193627
|
+
/**
|
|
193628
|
+
* POSIX single-quote escaping: wrap `value` in `'...'`, replacing every
|
|
193629
|
+
* embedded `'` with `'\''` (close quote, escaped literal quote, reopen
|
|
193630
|
+
* quote). The only place a raw string may reach a shell command in this
|
|
193631
|
+
* module — every builder below routes every interpolated value through this.
|
|
193632
|
+
*/
|
|
193633
|
+
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
193634
|
+
/**
|
|
193635
|
+
* `--allow-empty` mirrors `commitAllWithPrefix`/`commitAsIs`'s load-bearing
|
|
193636
|
+
* use in `src/Git.ts`: gtd's workflow commits may be empty on purpose. The
|
|
193637
|
+
* retry-without-hooks behavior there keys off the specific "empty git commit"
|
|
193638
|
+
* hook-rejection message, re-failing on anything else (`src/Git.ts`'s `run`
|
|
193639
|
+
* doc comment records the regression a blind retry-on-any-failure would
|
|
193640
|
+
* reintroduce: reporting success on a genuinely rejected commit — a lint
|
|
193641
|
+
* error, a rejected commit-msg hook). Bash CAN inspect the message: capture
|
|
193642
|
+
* the first attempt's combined output and discriminate on it in a `case`,
|
|
193643
|
+
* exactly like `isIndexLockError`/`error.message.includes(...)` do in
|
|
193644
|
+
* `src/Git.ts`, instead of retrying on the exit code alone.
|
|
193645
|
+
*/
|
|
193646
|
+
const commitAllowEmpty = (message) => {
|
|
193647
|
+
const m = shellQuote(message);
|
|
193648
|
+
return [
|
|
193649
|
+
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
193650
|
+
` case "$out" in`,
|
|
193651
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
|
|
193652
|
+
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
193653
|
+
` esac`,
|
|
193654
|
+
`fi`
|
|
193655
|
+
].join("\n");
|
|
193656
|
+
};
|
|
193657
|
+
/**
|
|
193658
|
+
* `git add -A` then `git commit --allow-empty -m <message>`, retried without
|
|
193659
|
+
* hooks on the same hook rejection `commitAllowEmpty` guards against. Joined
|
|
193660
|
+
* with `&&` so a failing `git add` (e.g. a quoted path breaking it) never
|
|
193661
|
+
* reaches the commit — the same "lost files whose quoted paths broke a
|
|
193662
|
+
* swallowed `git add`" failure mode `src/Git.ts`'s `run` doc comment records.
|
|
193663
|
+
*/
|
|
193664
|
+
const commitAll = (message) => `git add -A &&\n${commitAllowEmpty(message)}`;
|
|
193665
|
+
/** `git commit --allow-empty -m <message>` — commits the index as-is, no implicit `git add`. */
|
|
193666
|
+
const commitAsIs = (message) => commitAllowEmpty(message);
|
|
193667
|
+
/** `git reset --soft <ref>` — HEAD moves; index and working tree untouched. */
|
|
193668
|
+
const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
|
|
193669
|
+
/** `git reset --mixed <ref>` — HEAD and index move; working tree untouched. */
|
|
193670
|
+
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
|
|
193671
|
+
/** `git reset --hard <ref>` — HEAD, index, AND working tree all move. */
|
|
193672
|
+
const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
|
|
193673
|
+
/**
|
|
193674
|
+
* `git add -A` then `git reset --hard HEAD` — discards every pending change,
|
|
193675
|
+
* tracked or untracked. Joined with `&&`: a failed stage must not reach the
|
|
193676
|
+
* hard reset, or untracked survivors remain — the exact outcome this builder
|
|
193677
|
+
* exists to avoid.
|
|
193678
|
+
*/
|
|
193679
|
+
const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
193680
|
+
/** `git update-ref <ref> <hash>` — point a repo-local ref at a commit. */
|
|
193681
|
+
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
193682
|
+
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
193683
|
+
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
193684
|
+
/**
|
|
193685
|
+
* `git restore --staged --source=<source> -- <paths…>` — tolerant of the
|
|
193686
|
+
* SAME two cases `src/Git.ts`'s `Effect.catchIf` tolerates (a missing ref, or
|
|
193687
|
+
* no path matching at `source`), and NOT tolerant of `index.lock` contention,
|
|
193688
|
+
* which must still fail the script rather than being swallowed here (see
|
|
193689
|
+
* `src/Git.ts`'s `restoreStagedFrom` doc comment and `isIndexLockError`) — a
|
|
193690
|
+
* lock failure silently swallowed here would mean the `.gtd/` index pin
|
|
193691
|
+
* silently doesn't happen, leaking `.gtd/` plumbing into the surfaced review
|
|
193692
|
+
* diff, the exact thing the pin exists to prevent. Captures the command's
|
|
193693
|
+
* output and discriminates on it, mirroring `isIndexLockError`'s two
|
|
193694
|
+
* substrings, exactly like `commitAllowEmpty` above. An empty `paths` emits
|
|
193695
|
+
* nothing at all (a harmless, syntactically valid empty script), mirroring
|
|
193696
|
+
* the Effect implementation's own `paths.length === 0` skip.
|
|
193697
|
+
*/
|
|
193698
|
+
const restoreStagedFrom = (source, paths) => {
|
|
193699
|
+
if (paths.length === 0) return "";
|
|
193700
|
+
const pathArgs = paths.map(shellQuote).join(" ");
|
|
193701
|
+
return [
|
|
193702
|
+
`if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
|
|
193703
|
+
` case "$out" in`,
|
|
193704
|
+
` *"index.lock"*|*"Another git process seems to be running"*) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
193705
|
+
` *) : ;;`,
|
|
193706
|
+
` esac`,
|
|
193707
|
+
`fi`
|
|
193708
|
+
].join("\n");
|
|
193709
|
+
};
|
|
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
|
|
193848
|
+
//#region src/Emit.ts
|
|
193849
|
+
/**
|
|
193850
|
+
* Assembles the two scripts a `gtd next`-time decision hands the external
|
|
193851
|
+
* driver: `required` (everything that decides what lands in git) and
|
|
193852
|
+
* `optional` (presentation only — e.g. opening the review checkout window so
|
|
193853
|
+
* an editor's diff view has something to show; a driver that skips it loses
|
|
193854
|
+
* nothing but that view). Pure, like `src/GitScript.ts`: no git, no
|
|
193855
|
+
* filesystem, no `Effect`. Every input string (a `GitScript.ts` builder's
|
|
193856
|
+
* output, a steering mode's already-rendered `format:`/`validate:` command)
|
|
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.
|
|
193862
|
+
*
|
|
193863
|
+
* Both halves are meant to stand alone: a driver may run either, both, or
|
|
193864
|
+
* only `required`, and a person could paste either into a terminal. That's
|
|
193865
|
+
* why each carries its OWN copy of the precondition asserts and the retry
|
|
193866
|
+
* helper, rather than the two sharing one preamble a caller has to splice
|
|
193867
|
+
* together correctly.
|
|
193868
|
+
*/
|
|
193869
|
+
/**
|
|
193870
|
+
* The CLI resolves `expectedHead` (and a review window's `expectedHash`) at
|
|
193871
|
+
* DECIDE time; the script may RUN later, in a process that never shared
|
|
193872
|
+
* memory with the one that decided. This assertion closes that
|
|
193873
|
+
* time-of-check/time-of-use gap. A plain `[ ... ] || { ...; exit 1; }`
|
|
193874
|
+
* statement, not an `if`/`fi` block, so a line-oriented consumer can still
|
|
193875
|
+
* recognize it, and so it stays safe under `set -e`: in an OR list, `-e`
|
|
193876
|
+
* exempts every command except the LAST one, so the left-hand `[ ... ]`
|
|
193877
|
+
* failing here never trips `set -e` on its own — only the explicit `exit 1`
|
|
193878
|
+
* inside the right-hand group does. Exported (alongside `reviewWindowAssertion`)
|
|
193879
|
+
* solely so `src/testing/EmittedScriptRecognizer.ts` can re-derive and
|
|
193880
|
+
* string-compare this exact shape, the same "call the real builder, never
|
|
193881
|
+
* hand-copy its template" discipline it already applies to every
|
|
193882
|
+
* `GitScript.ts` builder.
|
|
193883
|
+
*
|
|
193884
|
+
* The probe is `git rev-parse --verify --quiet HEAD 2>/dev/null`, not the
|
|
193885
|
+
* bare `git rev-parse HEAD` a born repo's assertion could get away with —
|
|
193886
|
+
* load-bearing, not cosmetic: against an UNBORN HEAD (a repository with no
|
|
193887
|
+
* commits), the bare form prints the literal string `HEAD` to stdout (plus a
|
|
193888
|
+
* `fatal:` line to stderr) and exits 128, so `"$(git rev-parse HEAD)"` reads
|
|
193889
|
+
* as `"HEAD"`, never `""`, and an `expectedHead === ""` comparison could
|
|
193890
|
+
* never pass. `--verify --quiet` makes an unborn HEAD read back as an empty
|
|
193891
|
+
* string instead — the same idiom `reviewWindowAssertion` already uses for a
|
|
193892
|
+
* missing ref — so `[ "$(…)" = '' ]` is simply the unborn case of the same
|
|
193893
|
+
* comparison every born repo already runs; only the `printf` wording
|
|
193894
|
+
* branches on it, to avoid naming a hash that doesn't exist.
|
|
193895
|
+
*/
|
|
193896
|
+
const headAssertion = (expectedHead) => {
|
|
193897
|
+
const q = shellQuote(expectedHead);
|
|
193898
|
+
const probe = `[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = ${q} ] || `;
|
|
193899
|
+
return expectedHead === "" ? probe + "{ printf 'gtd: repository changed since this script was generated (expected a repository with no commits yet) — re-run gtd\\n' >&2; exit 1; }" : probe + `{ printf 'gtd: repository changed since this script was generated (expected HEAD %s) — re-run gtd\\n' ${q} >&2; exit 1; }`;
|
|
193900
|
+
};
|
|
193901
|
+
/**
|
|
193902
|
+
* Same shape as `headAssertion`, for the saved-head ref a review window
|
|
193903
|
+
* pins. `--verify --quiet ... 2>/dev/null` makes a MISSING ref read as an
|
|
193904
|
+
* empty string rather than erroring the substitution itself — the assertion
|
|
193905
|
+
* only needs to compare, never to fail outright before the `[ ... ]` runs.
|
|
193906
|
+
*/
|
|
193907
|
+
const reviewWindowAssertion = (ref, expectedHash) => {
|
|
193908
|
+
const refQ = shellQuote(ref);
|
|
193909
|
+
const hashQ = shellQuote(expectedHash);
|
|
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; }`;
|
|
193911
|
+
};
|
|
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
|
+
/**
|
|
193936
|
+
* `gtd_retry` takes ONE already-`shellQuote`d command string and `eval`s it,
|
|
193937
|
+
* mirroring `src/Git.ts`'s `withIndexLockRetry`: retry ONLY on the same two
|
|
193938
|
+
* substrings `isIndexLockError` matches, with jittered exponential backoff
|
|
193939
|
+
* (~10ms doubling, capped at 6 TOTAL attempts — the initial try plus 5
|
|
193940
|
+
* retries), and propagate any other failure on the very first attempt. Output
|
|
193941
|
+
* is captured combined (`2>&1`) so the discrimination can inspect it exactly
|
|
193942
|
+
* like `commitAllowEmpty`/`restoreStagedFrom` in `src/GitScript.ts` do for
|
|
193943
|
+
* their own single-shot retries — this is the same technique, generalized
|
|
193944
|
+
* into a loop. `awk` (not bash, which has no fractional `sleep` builtin)
|
|
193945
|
+
* turns the millisecond backoff into the fractional-second argument `sleep`
|
|
193946
|
+
* needs; `-v` passes the values in rather than interpolating them into the
|
|
193947
|
+
* awk program text, so a variable's value can never be mistaken for awk
|
|
193948
|
+
* syntax.
|
|
193949
|
+
*/
|
|
193950
|
+
const RETRY_HELPER = [
|
|
193951
|
+
`gtd_retry() {`,
|
|
193952
|
+
` local cmd=$1 attempt=1 delay_ms=10 out total_ms jitter_ms`,
|
|
193953
|
+
` while true; do`,
|
|
193954
|
+
` if out=$(eval "$cmd" 2>&1); then`,
|
|
193955
|
+
` [ -n "$out" ] && printf '%s\\n' "$out"`,
|
|
193956
|
+
` return 0`,
|
|
193957
|
+
` fi`,
|
|
193958
|
+
` case "$out" in`,
|
|
193959
|
+
` *"index.lock"*|*"Another git process seems to be running"*) ;;`,
|
|
193960
|
+
` *) printf '%s\\n' "$out" >&2; return 1 ;;`,
|
|
193961
|
+
` esac`,
|
|
193962
|
+
` if [ "$attempt" -ge 6 ]; then`,
|
|
193963
|
+
` printf '%s\\n' "$out" >&2`,
|
|
193964
|
+
` return 1`,
|
|
193965
|
+
` fi`,
|
|
193966
|
+
` jitter_ms=$(( RANDOM % delay_ms + 1 ))`,
|
|
193967
|
+
` total_ms=$(( delay_ms + jitter_ms ))`,
|
|
193968
|
+
` sleep "$(awk -v ms="$total_ms" 'BEGIN { printf "%.3f", ms / 1000 }')"`,
|
|
193969
|
+
` delay_ms=$(( delay_ms * 2 ))`,
|
|
193970
|
+
` attempt=$(( attempt + 1 ))`,
|
|
193971
|
+
` done`,
|
|
193972
|
+
`}`
|
|
193973
|
+
].join("\n");
|
|
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
|
+
};
|
|
193979
|
+
const assembleScript = (preconditions, steps) => {
|
|
193980
|
+
if (steps.length === 0) return "";
|
|
193981
|
+
const sections = ["set -euo pipefail"];
|
|
193982
|
+
if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
|
|
193983
|
+
if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
|
|
193984
|
+
if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
|
|
193985
|
+
if (steps.some((step) => step.kind === "outcome")) sections.push(OUTCOME_PREAMBLE);
|
|
193986
|
+
sections.push(...steps.map(renderStep));
|
|
193987
|
+
return sections.join("\n\n");
|
|
193988
|
+
};
|
|
193989
|
+
/**
|
|
193990
|
+
* Build both halves from already-rendered command strings. `required`
|
|
193991
|
+
* defaults to `[]` (nothing to do), `optional` to `[]` (no review window to
|
|
193992
|
+
* open) — either omitted argument, or an explicit empty array, produces the
|
|
193993
|
+
* fixed empty-string result `EmittedScripts` documents: a driver checking
|
|
193994
|
+
* `if [ -n "$required" ]` (or the JS equivalent) never has to special-case a
|
|
193995
|
+
* bare preamble with no body.
|
|
193996
|
+
*/
|
|
193997
|
+
const emitScripts = (preconditions, required = [], optional = []) => ({
|
|
193998
|
+
required: assembleScript(preconditions, required),
|
|
193999
|
+
optional: assembleScript(preconditions, optional)
|
|
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
|
+
};
|
|
193496
194027
|
//#endregion
|
|
193497
194028
|
//#region src/Edge.ts
|
|
193498
194029
|
/**
|
|
@@ -193537,6 +194068,17 @@ const clearRetainedHistory = (git) => git.deleteRef(HISTORY_REF);
|
|
|
193537
194068
|
* what `PatternMachine.step` consumes, despite having a single reader.
|
|
193538
194069
|
*/
|
|
193539
194070
|
const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
194071
|
+
/**
|
|
194072
|
+
* The review checkout window's saved-head ref — duplicated from
|
|
194073
|
+
* `src/ReviewWindow.ts`'s own `REVIEW_HEAD_REF` rather than imported, because
|
|
194074
|
+
* `ReviewWindow.ts` already imports FROM this module (`currentRun`,
|
|
194075
|
+
* `reviewBaseFor`); importing it back here would be a real circular module
|
|
194076
|
+
* dependency. Mirrors `src/RetainedHistory.ts`'s own `HISTORY_REF` doc
|
|
194077
|
+
* comment on the same tradeoff. Read (never written) by `restAt`'s
|
|
194078
|
+
* window-aware branch and by the script preconditions below — this module
|
|
194079
|
+
* never opens or closes the window itself.
|
|
194080
|
+
*/
|
|
194081
|
+
const REVIEW_HEAD_REF$1 = "refs/worktree/gtd/review-head";
|
|
193540
194082
|
const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
|
|
193541
194083
|
const COST_TRAILER_PREFIX = "Gtd-Cost: ";
|
|
193542
194084
|
const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
|
|
@@ -193564,8 +194106,8 @@ const parseCostTrailers = (messages) => {
|
|
|
193564
194106
|
const REVIEW_BASE_TRAILER_PREFIX = "Gtd-Review-Base: ";
|
|
193565
194107
|
const REVIEW_BASE_TRAILER_RE = /^Gtd-Review-Base:[ \t]*(\S+)[ \t]*$/m;
|
|
193566
194108
|
/**
|
|
193567
|
-
* The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd
|
|
193568
|
-
*
|
|
194109
|
+
* The `Gtd-Review-Base: <hash>` trailer recorded on a `gtd --entry <state>`
|
|
194110
|
+
* entry commit (see `withEntryTrailers`), or `undefined`
|
|
193569
194111
|
* when `message` carries none. Read back by `computeProcessRun` — ONLY off
|
|
193570
194112
|
* the process's first (oldest) commit — to override the run's diff base.
|
|
193571
194113
|
*/
|
|
@@ -193654,12 +194196,33 @@ const resolveRestFrom = (def, headSubject) => {
|
|
|
193654
194196
|
}
|
|
193655
194197
|
};
|
|
193656
194198
|
};
|
|
193657
|
-
/** The working tree's pending changes vs HEAD, as the pattern grammar's `{status, path}[]`. */
|
|
193658
|
-
const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entries.map((e) => ({
|
|
194199
|
+
/** The working tree's pending changes vs `base` (default HEAD), as the pattern grammar's `{status, path}[]`. */
|
|
194200
|
+
const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries) => entries.map((e) => ({
|
|
193659
194201
|
status: normalizeStatus(e.status),
|
|
193660
194202
|
path: e.path
|
|
193661
194203
|
}))));
|
|
193662
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
|
+
/**
|
|
193663
194226
|
* The `Gtd-Review-Base:`/`Gtd-Var:` overrides carried by the process's OLDEST
|
|
193664
194227
|
* commit (its entry commit, when `planEntry` started this process) — a later
|
|
193665
194228
|
* turn's message is never mistaken for it. `{reviewBase: undefined, vars:
|
|
@@ -193696,19 +194259,27 @@ const parseEntryCommitOverrides = (processCommits) => {
|
|
|
193696
194259
|
* same shape a fresh rest at a squashed boundary has always had.
|
|
193697
194260
|
*
|
|
193698
194261
|
* `hasCommits` is supplied by the caller (already read once — see the module
|
|
193699
|
-
* doc comment's stage ordering) rather than re-read here.
|
|
193700
|
-
|
|
193701
|
-
|
|
194262
|
+
* doc comment's stage ordering) rather than re-read here. `head` — a resolved
|
|
194263
|
+
* hash, never a symbolic ref — overrides the literal `HEAD` the walk would
|
|
194264
|
+
* otherwise end at: `restAt`'s window-aware branch passes the review
|
|
194265
|
+
* checkout window's saved-head hash here while the window is open, so the
|
|
194266
|
+
* trace still covers the commits a real `git log` would miss with HEAD
|
|
194267
|
+
* rewound to the review base. `undefined` (every other caller) is today's
|
|
194268
|
+
* exact behavior — an explicit trailing ref is passed to `git.commitHistory`
|
|
194269
|
+
* only when `head` differs from the literal `"HEAD"` it already defaults to.
|
|
194270
|
+
*/
|
|
194271
|
+
const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
|
|
193702
194272
|
if (!hasCommits) return {
|
|
193703
194273
|
startHash: "",
|
|
193704
194274
|
startParentHash: EMPTY_TREE$2,
|
|
193705
194275
|
diffBase: EMPTY_TREE$2,
|
|
193706
194276
|
trace: [],
|
|
193707
194277
|
costEntries: [],
|
|
193708
|
-
entryVars: {}
|
|
194278
|
+
entryVars: {},
|
|
194279
|
+
headTurn: void 0
|
|
193709
194280
|
};
|
|
193710
194281
|
const initialState = initialStateOf(def);
|
|
193711
|
-
const history = yield* git.commitHistory();
|
|
194282
|
+
const history = yield* git.commitHistory(void 0, head);
|
|
193712
194283
|
let i = history.length - 1;
|
|
193713
194284
|
while (i >= 0) {
|
|
193714
194285
|
const parsed = parseStateSubject(subjectOf(history[i].message));
|
|
@@ -193731,7 +194302,8 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
|
|
|
193731
194302
|
diffBase: reviewBaseOverride ?? startParentHash,
|
|
193732
194303
|
trace,
|
|
193733
194304
|
costEntries,
|
|
193734
|
-
entryVars
|
|
194305
|
+
entryVars,
|
|
194306
|
+
headTurn: headTurnFrom(def, history)
|
|
193735
194307
|
};
|
|
193736
194308
|
});
|
|
193737
194309
|
/**
|
|
@@ -193740,12 +194312,19 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
|
|
|
193740
194312
|
* since abandon IS the recovery command for that case) and the review
|
|
193741
194313
|
* window's re-arm (`openReviewWindow` stays graceful there too — see its own
|
|
193742
194314
|
* doc comment).
|
|
194315
|
+
*
|
|
194316
|
+
* Window-aware on exactly the same terms as `restAt`: while a review checkout
|
|
194317
|
+
* window is open, real HEAD is rewound to the review base, so walking the
|
|
194318
|
+
* trace from literal HEAD would see a process that has not started. Both
|
|
194319
|
+
* callers need the REAL head — abandon rewinds the reviewed branch tip, the
|
|
194320
|
+
* re-arm re-derives the review base — so the saved head is the walk's origin.
|
|
193743
194321
|
*/
|
|
193744
194322
|
const currentRun = gen(function* () {
|
|
193745
194323
|
const git = yield* GitService;
|
|
193746
194324
|
const config = yield* (yield* ConfigService).load;
|
|
193747
194325
|
const hasCommits = yield* git.hasCommits();
|
|
193748
|
-
|
|
194326
|
+
const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
|
|
194327
|
+
return yield* computeProcessRun(git, config.workflow, hasCommits, windowHead);
|
|
193749
194328
|
});
|
|
193750
194329
|
/**
|
|
193751
194330
|
* PURE: the most-recent in-process turn commit that entered a `reviewBase`
|
|
@@ -193793,6 +194372,35 @@ const memoryKeyFor = (scopes, rest, run) => {
|
|
|
193793
194372
|
const token = entryIndex <= 0 ? run.startParentHash : run.trace[entryIndex - 1].hash;
|
|
193794
194373
|
return `${scope || ROOT_MEMORY_SCOPE_NAME}#${token.slice(0, 7)}`;
|
|
193795
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
|
+
};
|
|
193796
194404
|
const PREFIX = "GTD_";
|
|
193797
194405
|
/**
|
|
193798
194406
|
* Assemble the merged `it.vars` map every template sees, from four layers
|
|
@@ -193800,7 +194408,7 @@ const PREFIX = "GTD_";
|
|
|
193800
194408
|
* (`ConfigOperations.workflowVars`), the top-level `.gtdrc` `vars:` key
|
|
193801
194409
|
* (`ConfigOperations.rcVars`), the current process's entry commit's
|
|
193802
194410
|
* `Gtd-Var:` trailers (`ProcessRun.entryVars` — fixed overrides recorded at
|
|
193803
|
-
* the moment a process like `gtd
|
|
194411
|
+
* the moment a process like `gtd --entry <state>` started it),
|
|
193804
194412
|
* and — for each name declared by any of those three layers — a
|
|
193805
194413
|
* `GTD_<UPPERCASE-name>` environment variable, if defined. Unlike the first
|
|
193806
194414
|
* three layers, the environment can only OVERRIDE a name some earlier layer
|
|
@@ -193941,6 +194549,20 @@ const omitUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([,
|
|
|
193941
194549
|
* window's saved head via `REVIEW_HEAD_REF`, or `undefined` for HEAD itself —
|
|
193942
194550
|
* see `currentRest`). Assembles every stage documented in the module doc
|
|
193943
194551
|
* comment, threading one `hasCommits()` read through stages 3/5/11.
|
|
194552
|
+
*
|
|
194553
|
+
* Window-aware ONLY at `ref === undefined` (`currentRest`'s own case): when
|
|
194554
|
+
* the review checkout window's saved-head ref (`REVIEW_HEAD_REF`) resolves,
|
|
194555
|
+
* its hash is used as the effective head for `lastCommitSubject` AND
|
|
194556
|
+
* `computeProcessRun`'s trace walk — real git HEAD has been rewound to the
|
|
194557
|
+
* review base while the window is open, so reading literal `HEAD` there
|
|
194558
|
+
* would resolve state/trace/memory against the wrong commit. An explicit
|
|
194559
|
+
* `ref` (`gtd visualize`'s own call pattern) never triggers this branch —
|
|
194560
|
+
* that path resolves exactly as it always has. Safe to land now because
|
|
194561
|
+
* every existing command closes the window (`program.ts`'s
|
|
194562
|
+
* `runInReviewWindowBracket`) before `restAt`/`currentRest` ever runs, so the
|
|
194563
|
+
* saved-head ref is already gone by the time this branch could fire in
|
|
194564
|
+
* production — see the two direct tests in `Edge.test.ts` for the only
|
|
194565
|
+
* coverage today.
|
|
193944
194566
|
*/
|
|
193945
194567
|
const restAt = (ref) => gen(function* () {
|
|
193946
194568
|
const git = yield* GitService;
|
|
@@ -193949,18 +194571,22 @@ const restAt = (ref) => gen(function* () {
|
|
|
193949
194571
|
const envVars = yield* EnvVars;
|
|
193950
194572
|
const def = config.workflow;
|
|
193951
194573
|
const hasCommits = yield* git.hasCommits();
|
|
193952
|
-
|
|
194574
|
+
let windowHead;
|
|
194575
|
+
if (ref === void 0) windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
|
|
194576
|
+
const effectiveRef = windowHead ?? ref;
|
|
194577
|
+
const headSubject = hasCommits ? yield* git.lastCommitSubject(effectiveRef) : "";
|
|
193953
194578
|
const resolution = resolveRestFrom(def, headSubject);
|
|
193954
194579
|
if (!resolution.ok) return yield* fail$2(resolution.error);
|
|
193955
194580
|
const resolved = resolution.rest;
|
|
193956
|
-
const run = yield* computeProcessRun(git, def, hasCommits);
|
|
194581
|
+
const run = yield* computeProcessRun(git, def, hasCommits, windowHead);
|
|
193957
194582
|
const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
|
|
193958
194583
|
const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
|
|
193959
194584
|
const stepDef = withRenderedOn(def, resolved.state, on);
|
|
193960
194585
|
const reviewBase = reviewBaseFor(def, run);
|
|
193961
|
-
const changes = yield* pendingChanges(git);
|
|
194586
|
+
const changes = yield* pendingChanges(git, windowHead);
|
|
193962
194587
|
const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, hasCommits);
|
|
193963
194588
|
const memory = memoryKeyFor(config.stateScopes, resolved, run);
|
|
194589
|
+
const memoryResumed = memoryResumedFor(def, config.stateScopes, resolved, run);
|
|
193964
194590
|
const hints = yield* renderHints(resolved.stateDef, context);
|
|
193965
194591
|
return {
|
|
193966
194592
|
...resolved,
|
|
@@ -193970,6 +194596,7 @@ const restAt = (ref) => gen(function* () {
|
|
|
193970
194596
|
stepDef,
|
|
193971
194597
|
changes,
|
|
193972
194598
|
memory,
|
|
194599
|
+
memoryResumed,
|
|
193973
194600
|
hints,
|
|
193974
194601
|
context
|
|
193975
194602
|
};
|
|
@@ -194028,17 +194655,48 @@ const renderRest = (rest) => gen(function* () {
|
|
|
194028
194655
|
content,
|
|
194029
194656
|
...rest.hints,
|
|
194030
194657
|
...rest.memory !== void 0 ? { memory: rest.memory } : {},
|
|
194658
|
+
memoryResumed: rest.memoryResumed,
|
|
194031
194659
|
edges: rest.context.edges
|
|
194032
194660
|
};
|
|
194033
194661
|
});
|
|
194034
|
-
/**
|
|
194035
|
-
|
|
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)"}`;
|
|
194036
194691
|
/**
|
|
194037
194692
|
* Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
|
|
194038
|
-
* own resting one, with a step's `cost`/`model` folded in
|
|
194039
|
-
*
|
|
194040
|
-
* `TemplateContext`
|
|
194041
|
-
* the
|
|
194693
|
+
* own resting one, with a step's `cost`/`model` folded in. A squash's commit
|
|
194694
|
+
* template is the only thing that needs it (the commit branch never reads a
|
|
194695
|
+
* `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
|
|
194696
|
+
* that one is pinned to the resting state and carries `cost: 0`, so rendering
|
|
194697
|
+
* `it.processCost` against it silently omits the squashing step's own
|
|
194698
|
+
* `--cost`. Exported for `program.ts`'s `planLanding`, which assembles the
|
|
194699
|
+
* same script by hand and must pick the same context.
|
|
194042
194700
|
*/
|
|
194043
194701
|
const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
|
|
194044
194702
|
const git = yield* GitService;
|
|
@@ -194048,98 +194706,220 @@ const contextAt = (rest, targetState, invoker, cost, model) => gen(function* ()
|
|
|
194048
194706
|
return yield* buildTemplateContext(git, templateRead(files), targetState, invoker, rest.run, rest.vars, onEdges, cost ?? 0, model, reviewBaseFor(rest.def, rest.run), hasCommits);
|
|
194049
194707
|
});
|
|
194050
194708
|
/**
|
|
194051
|
-
*
|
|
194052
|
-
*
|
|
194053
|
-
*
|
|
194054
|
-
*
|
|
194055
|
-
*
|
|
194056
|
-
*
|
|
194057
|
-
*
|
|
194058
|
-
*
|
|
194059
|
-
*
|
|
194060
|
-
*
|
|
194061
|
-
* `
|
|
194062
|
-
|
|
194063
|
-
|
|
194709
|
+
* Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
|
|
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.
|
|
194714
|
+
*
|
|
194715
|
+
* The squash branch renders the commit-state template against the PENDING
|
|
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
|
|
194726
|
+
* commit whose target is the workflow's INITIAL state, from a process that
|
|
194727
|
+
* retained nothing, is a no-op probe (`gtd --entry fix-precheck` against a
|
|
194728
|
+
* green suite is the shipped example) and must leave no trace at all. It
|
|
194729
|
+
* emits retain-history + a mixed reset to the process start instead of a
|
|
194730
|
+
* commit, so the entry commit and the probe collapse away together rather
|
|
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.
|
|
194780
|
+
*/
|
|
194781
|
+
const collapsesToInitialState = (rest, decision) => gen(function* () {
|
|
194782
|
+
const git = yield* GitService;
|
|
194783
|
+
return yield* collapsesWith(git, rest, decision);
|
|
194784
|
+
});
|
|
194785
|
+
const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
|
|
194786
|
+
const run = rest.run;
|
|
194064
194787
|
switch (decision.kind) {
|
|
194065
194788
|
case "commit":
|
|
194066
|
-
yield* git
|
|
194067
|
-
|
|
194068
|
-
|
|
194069
|
-
|
|
194070
|
-
|
|
194789
|
+
if (yield* collapsesWith(git, rest, decision)) {
|
|
194790
|
+
const tip = yield* git.resolveRef("HEAD");
|
|
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
|
+
];
|
|
194802
|
+
}
|
|
194803
|
+
return [{
|
|
194804
|
+
kind: "gitWrite",
|
|
194805
|
+
command: commitAll(withCostTrailer(decision.subject, cost, model))
|
|
194806
|
+
}, commitDecisionOutcome(decision)];
|
|
194071
194807
|
case "squash": {
|
|
194072
194808
|
const message = yield* try_({
|
|
194073
194809
|
try: () => renderStateTemplate(decision.template, context),
|
|
194074
194810
|
catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
|
|
194075
194811
|
});
|
|
194076
194812
|
const tip = yield* git.resolveRef("HEAD");
|
|
194077
|
-
|
|
194078
|
-
|
|
194079
|
-
|
|
194080
|
-
|
|
194081
|
-
|
|
194082
|
-
|
|
194083
|
-
|
|
194084
|
-
|
|
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
|
+
];
|
|
194085
194832
|
}
|
|
194086
194833
|
}
|
|
194087
194834
|
});
|
|
194088
194835
|
/**
|
|
194089
|
-
*
|
|
194090
|
-
* `
|
|
194091
|
-
*
|
|
194092
|
-
*
|
|
194093
|
-
*
|
|
194094
|
-
*
|
|
194095
|
-
*
|
|
194096
|
-
*
|
|
194097
|
-
*
|
|
194098
|
-
*
|
|
194099
|
-
|
|
194100
|
-
|
|
194101
|
-
|
|
194102
|
-
|
|
194103
|
-
const
|
|
194836
|
+
* The `EmitPreconditions` a step's/entry's assembled scripts assert against:
|
|
194837
|
+
* `expectedHead` is the resolved HEAD hash the `Rest` snapshot was already
|
|
194838
|
+
* taken at — `rest.context.currentCommit`, passed straight through, `""`
|
|
194839
|
+
* included (its own convention for "no commits yet", which `headAssertion`
|
|
194840
|
+
* now understands natively). When `targetState` declares `reviewWindow: true`
|
|
194841
|
+
* AND a window is currently open (`REVIEW_HEAD_REF` resolves), the script also
|
|
194842
|
+
* pins the window's saved-head hash — a later run whose window has since
|
|
194843
|
+
* closed or moved must re-decide rather than trust a stale script. No window,
|
|
194844
|
+
* or a non-review-window target, carries no `reviewWindow` precondition at
|
|
194845
|
+
* all.
|
|
194846
|
+
*/
|
|
194847
|
+
const buildPreconditions = (git, rest, targetState) => gen(function* () {
|
|
194848
|
+
const expectedHead = rest.context.currentCommit;
|
|
194849
|
+
if (!isReviewWindowState(rest.def, targetState)) return { expectedHead };
|
|
194850
|
+
const windowHead = yield* git.readRefOption(REVIEW_HEAD_REF$1);
|
|
194851
|
+
if (isNone(windowHead)) return { expectedHead };
|
|
194852
|
+
return {
|
|
194853
|
+
expectedHead,
|
|
194854
|
+
reviewWindow: {
|
|
194855
|
+
ref: REVIEW_HEAD_REF$1,
|
|
194856
|
+
expectedHash: windowHead.value
|
|
194857
|
+
}
|
|
194858
|
+
};
|
|
194859
|
+
});
|
|
194860
|
+
/**
|
|
194861
|
+
* The `EmittedScripts` a `"commit"`/`"squash"` `StepPlan` carries alongside
|
|
194862
|
+
* `perform` — built from the SAME `renderDecision` output, but a render
|
|
194863
|
+
* failure collapses to an EMPTY script (`emitScripts` with no steps) instead
|
|
194864
|
+
* of failing this Effect: `perform` stays the one place a render failure is
|
|
194865
|
+
* ever reported to a caller, unchanged by this addition.
|
|
194866
|
+
*/
|
|
194867
|
+
const buildStepScripts = (git, rest, decision, context, cost, model) => gen(function* () {
|
|
194868
|
+
const targetState = decision.kind === "commit" ? decision.to : decision.state;
|
|
194869
|
+
return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
|
|
194870
|
+
});
|
|
194871
|
+
/**
|
|
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, {
|
|
194104
194898
|
changes: rest.changes,
|
|
194105
194899
|
processTrace: rest.run.trace.map((entry) => entry.state)
|
|
194106
194900
|
});
|
|
194107
|
-
if (decision.kind === "refusal") return
|
|
194901
|
+
if (decision.kind === "refusal") return {
|
|
194108
194902
|
kind: "refusal",
|
|
194109
|
-
message: formatStepRefusal(
|
|
194110
|
-
}
|
|
194111
|
-
if (decision.kind === "noop") return
|
|
194903
|
+
message: formatStepRefusal(decision)
|
|
194904
|
+
};
|
|
194905
|
+
if (decision.kind === "noop") return {
|
|
194112
194906
|
kind: "noop",
|
|
194113
|
-
state: decision.state
|
|
194114
|
-
|
|
194907
|
+
state: decision.state,
|
|
194908
|
+
settled: noOpSettles(rest)
|
|
194909
|
+
};
|
|
194115
194910
|
const { cost, model } = opts;
|
|
194116
|
-
const
|
|
194117
|
-
|
|
194118
|
-
|
|
194119
|
-
|
|
194120
|
-
if (target === initialStateOf(rest.def) && rest.run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, rest.run, rest.changes))) {
|
|
194121
|
-
yield* retainHistory(git, yield* git.resolveRef("HEAD"), rest.run.startParentHash);
|
|
194122
|
-
yield* git.mixedResetTo(rest.run.startParentHash);
|
|
194123
|
-
return {
|
|
194124
|
-
kind: "reset",
|
|
194125
|
-
state: target
|
|
194126
|
-
};
|
|
194127
|
-
}
|
|
194128
|
-
return yield* executeDecision(git, rest.run, decision, rest.context, cost, model);
|
|
194129
|
-
}
|
|
194130
|
-
const context = yield* contextAt(rest, decision.state, invoker, cost, model);
|
|
194131
|
-
return yield* executeDecision(git, rest.run, decision, context, cost, model);
|
|
194132
|
-
});
|
|
194133
|
-
return succeed$3({
|
|
194911
|
+
const git = yield* GitService;
|
|
194912
|
+
const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
|
|
194913
|
+
const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
|
|
194914
|
+
return {
|
|
194134
194915
|
kind: decision.kind,
|
|
194135
194916
|
state: rest.state,
|
|
194136
194917
|
decision,
|
|
194137
|
-
|
|
194138
|
-
}
|
|
194139
|
-
};
|
|
194918
|
+
scripts
|
|
194919
|
+
};
|
|
194920
|
+
});
|
|
194140
194921
|
/**
|
|
194141
|
-
* `gtd
|
|
194142
|
-
* --entry <state>`): start a brand NEW process at `entry.state` — any
|
|
194922
|
+
* `gtd --entry <state>`: start a brand NEW process at `entry.state` — any
|
|
194143
194923
|
* declared, non-commit state (see `enterableStates`) — writing an ordinary
|
|
194144
194924
|
* turn commit (`gtd(<actor>): <state>`) carrying zero or more `Gtd-Var:
|
|
194145
194925
|
* <name>=<value>` trailers for each `entry.vars` override, plus — when
|
|
@@ -194147,7 +194927,7 @@ const planStep = (rest, invoker, opts = {}) => {
|
|
|
194147
194927
|
* trailer pinning the new process's diff base (rendered from that template
|
|
194148
194928
|
* against the merged `it.vars`, resolved to a commit, and checked sane).
|
|
194149
194929
|
* Commits via `commitAllWithPrefix` — capturing whatever the working tree
|
|
194150
|
-
* carries at the moment of entry, exactly like an ordinary
|
|
194930
|
+
* carries at the moment of entry, exactly like an ordinary land capture,
|
|
194151
194931
|
* rather than demanding a clean tree first.
|
|
194152
194932
|
*
|
|
194153
194933
|
* All validation is a REFUSAL, not an Effect failure — checked in order:
|
|
@@ -194220,18 +195000,18 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194220
195000
|
...base !== void 0 ? { base } : {},
|
|
194221
195001
|
vars: varOverrides
|
|
194222
195002
|
});
|
|
195003
|
+
const scriptsGit = yield* GitService;
|
|
194223
195004
|
return {
|
|
194224
195005
|
kind: "entry",
|
|
194225
195006
|
state: entryState,
|
|
194226
195007
|
subject,
|
|
194227
|
-
|
|
194228
|
-
|
|
194229
|
-
|
|
194230
|
-
|
|
194231
|
-
|
|
194232
|
-
|
|
194233
|
-
|
|
194234
|
-
})
|
|
195008
|
+
scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
|
|
195009
|
+
kind: "gitWrite",
|
|
195010
|
+
command: commitAll(message)
|
|
195011
|
+
}, {
|
|
195012
|
+
kind: "outcome",
|
|
195013
|
+
command: commitOutcome(subject)
|
|
195014
|
+
}])
|
|
194235
195015
|
};
|
|
194236
195016
|
});
|
|
194237
195017
|
//#endregion
|
|
@@ -194249,24 +195029,37 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194249
195029
|
* `PatternMachine.StateDef`) — the pure engine never observes an open window,
|
|
194250
195030
|
* exactly as before.
|
|
194251
195031
|
*
|
|
194252
|
-
* Lifecycle — driven from the program edge (`src/program.ts`),
|
|
194253
|
-
*
|
|
194254
|
-
*
|
|
194255
|
-
*
|
|
194256
|
-
*
|
|
194257
|
-
*
|
|
194258
|
-
*
|
|
194259
|
-
*
|
|
194260
|
-
*
|
|
194261
|
-
*
|
|
194262
|
-
* `
|
|
194263
|
-
*
|
|
194264
|
-
*
|
|
194265
|
-
*
|
|
194266
|
-
*
|
|
194267
|
-
*
|
|
194268
|
-
*
|
|
194269
|
-
*
|
|
195032
|
+
* Lifecycle — driven from the program edge (`src/program.ts`), which DECIDES
|
|
195033
|
+
* with this module's pure/read-only halves and assembles the actual writes
|
|
195034
|
+
* into the two scripts a `gtd` command hands the external driver:
|
|
195035
|
+
*
|
|
195036
|
+
* - The CLOSE sequence (`decideCloseWindow` + `buildCloseWindowScript`) is
|
|
195037
|
+
* read-only in this module — it decides whether a window is open and safe
|
|
195038
|
+
* to close, keyed solely on `REVIEW_HEAD_REF` existing (the same
|
|
195039
|
+
* reviewed-branch/legacy-containment guards described below) — and
|
|
195040
|
+
* `program.ts`'s `buildRequiredScript` splices the resulting
|
|
195041
|
+
* `mixedResetTo`/`deleteRef`/`deleteRef` bash in as the LEAD steps of the
|
|
195042
|
+
* `required` script, ahead of the commit/squash steps themselves. Nothing
|
|
195043
|
+
* reads or mutates state until the driver actually runs that script; once
|
|
195044
|
+
* it does, HEAD/index are restored exactly, leaving only the reviewer's own
|
|
195045
|
+
* edits dirty (captured by the resting state's own `on` patterns like any
|
|
195046
|
+
* other pending change) — the pure machine never sees the window.
|
|
195047
|
+
* - The OPEN sequence (`decideOpenWindow` + `buildOpenWindowScript`) is
|
|
195048
|
+
* likewise read-only — it self-guards on the STEP'S OWN target state
|
|
195049
|
+
* declaring `reviewWindow: true` — and `program.ts`'s `openWindowScript`
|
|
195050
|
+
* turns that into the `optional` script's `updateRef`/`updateRef`/
|
|
195051
|
+
* `mixedResetTo`/`restoreStagedFrom` bash: saving HEAD to `REVIEW_HEAD_REF`
|
|
195052
|
+
* (the base to `REVIEW_BASE_REF`), then rewinding to the base. The driver
|
|
195053
|
+
* runs it AFTER the required script has already landed the new commit
|
|
195054
|
+
* (`buildOpenWindowScript`'s own doc comment explains why it pins the
|
|
195055
|
+
* literal string `"HEAD"`, never a resolved hash). It re-arms after
|
|
195056
|
+
* read-only commands (`gtd next` / `gtd status`) and refused invocations
|
|
195057
|
+
* too, so the editor's diff view stays consistent no matter which command
|
|
195058
|
+
* the loop last ran.
|
|
195059
|
+
*
|
|
195060
|
+
* Every close/open sequence is idempotent under re-entry, so a crash at any
|
|
195061
|
+
* point is recovered by the next invocation's close (the saved ref also keeps
|
|
195062
|
+
* the real head GC-reachable for the window's whole lifetime).
|
|
194270
195063
|
*/
|
|
194271
195064
|
/**
|
|
194272
195065
|
* The window's two state refs live in git's PER-WORKTREE `refs/worktree/*`
|
|
@@ -194284,7 +195077,7 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194284
195077
|
const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
|
|
194285
195078
|
const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
|
|
194286
195079
|
/**
|
|
194287
|
-
* The pre-7.2 SHARED refs. gtd never writes them any more; `
|
|
195080
|
+
* The pre-7.2 SHARED refs. gtd never writes them any more; `decideCloseWindow`
|
|
194288
195081
|
* only reads them to finish a window an older gtd left open across the upgrade
|
|
194289
195082
|
* — and only when HEAD is still contained in the saved head, since a shared ref
|
|
194290
195083
|
* may belong to a sibling worktree (see `openWindowRefs`).
|
|
@@ -194316,28 +195109,10 @@ const openWindowRefs = (git) => gen(function* () {
|
|
|
194316
195109
|
legacy: true
|
|
194317
195110
|
});
|
|
194318
195111
|
});
|
|
194319
|
-
|
|
194320
|
-
* Restore the real head if a review checkout window is open; no-op otherwise.
|
|
194321
|
-
* Keyed on the saved-head ref's existence (see `openWindowRefs`), NOT on any
|
|
194322
|
-
* config or machine state, so it always runs first and the machine never sees
|
|
194323
|
-
* the window.
|
|
194324
|
-
*
|
|
194325
|
-
* Fails loudly — refs left in place — when HEAD is no longer on the reviewed
|
|
194326
|
-
* branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
|
|
194327
|
-
* mixed reset there would rewrite the wrong branch's tip. Manual commits on
|
|
194328
|
-
* top of the base pass the guard: the reset keeps their content in the working
|
|
194329
|
-
* tree, where the next turn's capture picks it up as review feedback.
|
|
194330
|
-
*
|
|
194331
|
-
* A window found under the LEGACY shared refs gets one extra guard: a shared
|
|
194332
|
-
* ref may have been written by a sibling worktree, so the close only proceeds
|
|
194333
|
-
* when HEAD is contained in the saved head — the shape a window this worktree
|
|
194334
|
-
* opened itself has. Anything else refuses with the manual recovery rather
|
|
194335
|
-
* than resetting this branch onto another worktree's work (issue #118).
|
|
194336
|
-
*/
|
|
194337
|
-
const closeReviewWindow = gen(function* () {
|
|
195112
|
+
const decideCloseWindow = gen(function* () {
|
|
194338
195113
|
const git = yield* GitService;
|
|
194339
195114
|
const open = yield* openWindowRefs(git);
|
|
194340
|
-
if (isNone(open)) return {
|
|
195115
|
+
if (isNone(open)) return { shouldClose: false };
|
|
194341
195116
|
const refs = open.value;
|
|
194342
195117
|
const base = yield* git.readRefOption(refs.baseRef);
|
|
194343
195118
|
if (isSome(base) && base.value !== EMPTY_TREE$1) {
|
|
@@ -194346,44 +195121,65 @@ const closeReviewWindow = gen(function* () {
|
|
|
194346
195121
|
if (refs.legacy) {
|
|
194347
195122
|
if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
|
|
194348
195123
|
}
|
|
194349
|
-
|
|
194350
|
-
|
|
194351
|
-
|
|
194352
|
-
|
|
195124
|
+
return {
|
|
195125
|
+
shouldClose: true,
|
|
195126
|
+
refs
|
|
195127
|
+
};
|
|
194353
195128
|
});
|
|
194354
195129
|
/**
|
|
194355
|
-
*
|
|
194356
|
-
*
|
|
194357
|
-
*
|
|
194358
|
-
*
|
|
194359
|
-
*
|
|
194360
|
-
*
|
|
194361
|
-
*
|
|
194362
|
-
*
|
|
194363
|
-
*
|
|
194364
|
-
*
|
|
194365
|
-
*
|
|
194366
|
-
* is
|
|
194367
|
-
*
|
|
194368
|
-
*
|
|
194369
|
-
*
|
|
194370
|
-
*
|
|
194371
|
-
*
|
|
194372
|
-
*/
|
|
194373
|
-
const
|
|
194374
|
-
|
|
194375
|
-
|
|
194376
|
-
|
|
194377
|
-
|
|
194378
|
-
|
|
194379
|
-
|
|
194380
|
-
|
|
194381
|
-
|
|
194382
|
-
|
|
194383
|
-
|
|
194384
|
-
|
|
194385
|
-
|
|
194386
|
-
}
|
|
195130
|
+
* PURE: the close sequence's bash text — reset to the saved head, then delete
|
|
195131
|
+
* both refs — built from `src/GitScript.ts`'s `mixedResetTo`/`deleteRef`.
|
|
195132
|
+
* Assumes its caller's guards (the reviewed-branch check, the legacy
|
|
195133
|
+
* containment check — see `decideCloseWindow`) have already passed; this
|
|
195134
|
+
* builder performs none of them, only the write sequence for the case where
|
|
195135
|
+
* they did.
|
|
195136
|
+
*
|
|
195137
|
+
* The reset target is `refs.headHash` — the RESOLVED hash, not `refs.headRef`
|
|
195138
|
+
* by name — precisely so the sequence stays idempotent once the ref itself is
|
|
195139
|
+
* gone: the very next statement deletes `headRef`, so a script naming the ref
|
|
195140
|
+
* for the reset would fail on re-entry (the ref `git reset --mixed` would
|
|
195141
|
+
* need is the one the script just deleted). A hash stays resolvable
|
|
195142
|
+
* regardless, so a second run's reset is a true no-op (HEAD is already
|
|
195143
|
+
* there) and both deletes tolerate an already-missing ref. `legacy` on `refs`
|
|
195144
|
+
* goes unused here — the script only needs `headRef`/`baseRef`/`headHash` —
|
|
195145
|
+
* but the parameter carries the whole `WindowRefs` record so a caller can
|
|
195146
|
+
* pass `decideCloseWindow`'s resolved `refs` straight through.
|
|
195147
|
+
*/
|
|
195148
|
+
const buildCloseWindowScript = (refs) => [
|
|
195149
|
+
mixedResetTo(refs.headHash),
|
|
195150
|
+
deleteRef(refs.headRef),
|
|
195151
|
+
deleteRef(refs.baseRef)
|
|
195152
|
+
].join(" &&\n");
|
|
195153
|
+
const decideOpenWindow = (def, target, run, head) => {
|
|
195154
|
+
if (!isReviewWindowState(def, target)) return { shouldOpen: false };
|
|
195155
|
+
const base = reviewBaseFor(def, run);
|
|
195156
|
+
if (base === EMPTY_TREE$1 || base === head) return { shouldOpen: false };
|
|
195157
|
+
return {
|
|
195158
|
+
shouldOpen: true,
|
|
195159
|
+
base,
|
|
195160
|
+
head
|
|
195161
|
+
};
|
|
195162
|
+
};
|
|
195163
|
+
/**
|
|
195164
|
+
* PURE: the open sequence's bash text — base-ref write, head-ref write,
|
|
195165
|
+
* mixed-reset-to-base, then the `.gtd/` restore-staged-from pin, built from
|
|
195166
|
+
* `src/GitScript.ts`'s `updateRef`/`mixedResetTo`/`restoreStagedFrom`, in the
|
|
195167
|
+
* same crash-safe order `openReviewWindow` uses (base ref → head ref → mixed
|
|
195168
|
+
* reset → `.gtd/` pin). Deliberately no `git add --intent-to-add` anywhere —
|
|
195169
|
+
* new files stay untracked, for the exact two reasons `openReviewWindow`'s
|
|
195170
|
+
* own doc comment records — and no whole-tree index write: `restoreStagedFrom`
|
|
195171
|
+
* is scoped to `.gtd/` alone.
|
|
195172
|
+
*
|
|
195173
|
+
* Idempotent under re-entry: `updateRef`/`mixedResetTo` re-pointing at the
|
|
195174
|
+
* same target is a no-op, and `restoreStagedFrom` re-pinning an already-pinned
|
|
195175
|
+
* path is a no-op.
|
|
195176
|
+
*/
|
|
195177
|
+
const buildOpenWindowScript = (decision) => [
|
|
195178
|
+
updateRef(REVIEW_BASE_REF, decision.base),
|
|
195179
|
+
updateRef(REVIEW_HEAD_REF, decision.head),
|
|
195180
|
+
mixedResetTo(decision.base),
|
|
195181
|
+
restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"])
|
|
195182
|
+
].join(" &&\n");
|
|
194387
195183
|
//#endregion
|
|
194388
195184
|
//#region node_modules/vscode-languageserver/lib/common/utils/is.js
|
|
194389
195185
|
var require_is$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -205438,17 +206234,17 @@ const resolveBuiltInMode = (mode) => {
|
|
|
205438
206234
|
};
|
|
205439
206235
|
const steeringCapabilities = (resolved) => {
|
|
205440
206236
|
if (resolved === void 0) return {};
|
|
206237
|
+
const seeded = resolved.builtIn !== void 0 && resolved.validate?.kind === "command" && isSeededValidateCommand(resolved.mode, resolved.validate.command);
|
|
206238
|
+
const liveValidate = resolved.builtIn !== void 0 && (resolved.validate?.kind === "builtin" || seeded) ? resolved.builtIn.validate : void 0;
|
|
205441
206239
|
return {
|
|
205442
206240
|
...resolved.builtIn !== void 0 ? { format: resolved.builtIn } : {},
|
|
205443
|
-
...
|
|
205444
|
-
...resolved.validate?.kind === "command" ? { externalValidate: true } : {}
|
|
206241
|
+
...liveValidate !== void 0 ? { liveValidate } : {},
|
|
206242
|
+
...resolved.validate?.kind === "command" && !seeded ? { externalValidate: true } : {}
|
|
205445
206243
|
};
|
|
205446
206244
|
};
|
|
205447
206245
|
/** The error message for a `mode:` that resolves to nothing — lists what the active workflow does know. */
|
|
205448
206246
|
const unknownModeMessage = (def, state, mode) => `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})`;
|
|
205449
206247
|
const errorText$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
205450
|
-
/** How a non-zero exit reads in a message: a status number, or a signal death. */
|
|
205451
|
-
const exitText = (status) => status === null ? "a signal" : `status ${status}`;
|
|
205452
206248
|
/** Render one command template, turning an Eta failure into a named error rather than executing anything. */
|
|
205453
206249
|
const renderCommand = (mode, key, command, file, context) => try_({
|
|
205454
206250
|
try: () => renderModeCommand(command, {
|
|
@@ -205457,41 +206253,22 @@ const renderCommand = (mode, key, command, file, context) => try_({
|
|
|
205457
206253
|
}),
|
|
205458
206254
|
catch: (e) => /* @__PURE__ */ new Error(`mode "${mode}": "${key}" command failed to render — ${errorText$1(e)}`)
|
|
205459
206255
|
});
|
|
205460
|
-
/** The findings a non-zero `validate` exit reports: its output lines, or a synthesized line when it said nothing. */
|
|
205461
|
-
const findingsFrom = (mode, outcome) => {
|
|
205462
|
-
const lines = outcome.output.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
|
|
205463
|
-
if (lines.length > 0) return lines;
|
|
205464
|
-
return [`mode "${mode}": validate command exited with ${exitText(outcome.status)} and no output`];
|
|
205465
|
-
};
|
|
205466
|
-
/**
|
|
205467
|
-
* Format `file` in place by running the mode's `format:` command. A mode with
|
|
205468
|
-
* no formatter (every mode, until a `modes:` layer declares one — gtd ships
|
|
205469
|
-
* none) formats nothing. A failing command is a hard error: the file is left
|
|
205470
|
-
* exactly as it was and the caller refuses, rather than validating a
|
|
205471
|
-
* half-formatted file.
|
|
205472
|
-
*/
|
|
205473
|
-
const formatSteeringFile = (resolved, file, context) => gen(function* () {
|
|
205474
|
-
const command = resolved.formatCommand;
|
|
205475
|
-
if (command === void 0) return;
|
|
205476
|
-
const rendered = yield* renderCommand(resolved.mode, "format", command, file, context);
|
|
205477
|
-
const outcome = yield* (yield* CommandRunner).bash(rendered);
|
|
205478
|
-
if (outcome.status !== 0) return yield* fail$2(/* @__PURE__ */ new Error(`mode "${resolved.mode}": format command exited with ${exitText(outcome.status)}${outcome.output.trim().length > 0 ? `:\n${outcome.output.trimEnd()}` : ""}`));
|
|
205479
|
-
});
|
|
205480
206256
|
/**
|
|
205481
|
-
*
|
|
205482
|
-
*
|
|
205483
|
-
*
|
|
205484
|
-
*
|
|
205485
|
-
*
|
|
205486
|
-
*
|
|
205487
|
-
|
|
205488
|
-
|
|
205489
|
-
|
|
205490
|
-
|
|
205491
|
-
|
|
205492
|
-
const
|
|
205493
|
-
|
|
205494
|
-
|
|
206257
|
+
* Render a mode's `format:`/`validate:` commands as plain strings, WITHOUT
|
|
206258
|
+
* running either — for a caller (an emitter that prints scripts rather than
|
|
206259
|
+
* executing them) that wants the commands themselves, not their outcome.
|
|
206260
|
+
* Unlike its siblings above, this needs no `CommandRunner`: it shares their
|
|
206261
|
+
* `renderCommand` helper for the rendering (and its errors) but never reaches
|
|
206262
|
+
* for a runner to execute the result. Order matches `formatAndValidateSteeringFile`
|
|
206263
|
+
* (format then validate); a half the mode doesn't declare — no `formatCommand`,
|
|
206264
|
+
* or a `validate` that is absent or `"builtin"` (an in-process parser has no
|
|
206265
|
+
* shell command to render) — is OMITTED from the result, not rendered as `""`.
|
|
206266
|
+
*/
|
|
206267
|
+
const renderSteeringCommands = (resolved, file, context) => gen(function* () {
|
|
206268
|
+
const commands = [];
|
|
206269
|
+
if (resolved.formatCommand !== void 0) commands.push(yield* renderCommand(resolved.mode, "format", resolved.formatCommand, file, context));
|
|
206270
|
+
if (resolved.validate?.kind === "command") commands.push(yield* renderCommand(resolved.mode, "validate", resolved.validate.command, file, context));
|
|
206271
|
+
return commands;
|
|
205495
206272
|
});
|
|
205496
206273
|
//#endregion
|
|
205497
206274
|
//#region src/Lsp.ts
|
|
@@ -205542,7 +206319,7 @@ const validateSteeringFile = (resolved, file, content, context) => gen(function*
|
|
|
205542
206319
|
* mode with no built-in format at all (a workflow-only name, or a declared
|
|
205543
206320
|
* `{}` entry) dispatches to no symbols, no actions, and no diagnostics. Either
|
|
205544
206321
|
* way the file is still formatted and validated by `gtd validate` and the
|
|
205545
|
-
* `gtd
|
|
206322
|
+
* `gtd land` capture gate — just not live in the editor.
|
|
205546
206323
|
*
|
|
205547
206324
|
* `gtd.openSteeringFile` (an `executeCommand`) resolves the CURRENT state
|
|
205548
206325
|
* exactly like the CLI (`resolveRest`/`computeProcessRun`/
|
|
@@ -206174,27 +206951,6 @@ const openInBrowser = (url) => {
|
|
|
206174
206951
|
//#region src/StepGuards.ts
|
|
206175
206952
|
/** Normalize every markdown checkbox to a single placeholder so a pure `[ ]`→`[x]` tick is invisible to a text comparison; any surviving difference is a human note. */
|
|
206176
206953
|
const normalizeCheckboxes = (content) => content.replace(/\[[ xX]\]/g, "[_]");
|
|
206177
|
-
const steeringFileGuard = {
|
|
206178
|
-
name: "steering-file",
|
|
206179
|
-
appliesTo: (rest) => rest.stateDef.mode !== void 0,
|
|
206180
|
-
prepare: (ctx) => gen(function* () {
|
|
206181
|
-
const mode = ctx.rest.stateDef.mode;
|
|
206182
|
-
const resolved = resolveSteeringMode(ctx.rest.def, mode);
|
|
206183
|
-
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(ctx.rest.def, ctx.rest.state, mode)));
|
|
206184
|
-
if ((yield* RepoFiles).working(ctx.file) === void 0) return;
|
|
206185
|
-
yield* formatSteeringFile(resolved, ctx.file, ctx.template);
|
|
206186
|
-
}),
|
|
206187
|
-
check: (ctx) => gen(function* () {
|
|
206188
|
-
const mode = ctx.rest.stateDef.mode;
|
|
206189
|
-
const resolved = resolveSteeringMode(ctx.rest.def, mode);
|
|
206190
|
-
if (resolved === void 0) return void 0;
|
|
206191
|
-
const current = yield* ctx.worktree;
|
|
206192
|
-
if (current === void 0) return void 0;
|
|
206193
|
-
const errors = yield* validateSteeringFile(resolved, ctx.file, current, ctx.template);
|
|
206194
|
-
if (errors.length === 0) return void 0;
|
|
206195
|
-
return `${ctx.file} is not valid at "${ctx.rest.state}" — fix these before stepping:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
206196
|
-
})
|
|
206197
|
-
};
|
|
206198
206954
|
/** The `mode:` name of gtd's built-in REVIEW.md checkbox validator — the only mode the sign-off guard understands. */
|
|
206199
206955
|
const REVIEW_MODE = "review";
|
|
206200
206956
|
const reviewSignoffGuard = {
|
|
@@ -206224,9 +206980,8 @@ const feedbackProgressGuard = {
|
|
|
206224
206980
|
};
|
|
206225
206981
|
/** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
|
|
206226
206982
|
const QA_MODE = "qa";
|
|
206227
|
-
/** The
|
|
206983
|
+
/** The three step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
|
|
206228
206984
|
const stepGuards = [
|
|
206229
|
-
steeringFileGuard,
|
|
206230
206985
|
reviewSignoffGuard,
|
|
206231
206986
|
feedbackProgressGuard,
|
|
206232
206987
|
{
|
|
@@ -206241,15 +206996,16 @@ const stepGuards = [
|
|
|
206241
206996
|
}
|
|
206242
206997
|
];
|
|
206243
206998
|
/**
|
|
206244
|
-
* The `gtd
|
|
206245
|
-
* applies to
|
|
206999
|
+
* The `gtd land` capture gate: run every guard the resolved rest's state
|
|
207000
|
+
* applies to against ONE sample of the current committed/working bytes — see
|
|
206246
207001
|
* the module docstring — and fail with the first refusal's reason, prefixed
|
|
206247
|
-
* `gtd
|
|
206248
|
-
* `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision,
|
|
206249
|
-
*
|
|
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.
|
|
206250
207006
|
*/
|
|
206251
207007
|
const enforceStepGuards = (input) => gen(function* () {
|
|
206252
|
-
if (input.kind !== "commit") return;
|
|
207008
|
+
if (input.kind !== "commit" || input.attempt) return;
|
|
206253
207009
|
const file = input.file;
|
|
206254
207010
|
if (file === void 0) return;
|
|
206255
207011
|
const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
|
|
@@ -206264,7 +207020,6 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206264
207020
|
hasCodeChange,
|
|
206265
207021
|
template: input.context
|
|
206266
207022
|
};
|
|
206267
|
-
for (const g of applicable) if (g.prepare !== void 0) yield* g.prepare(base);
|
|
206268
207023
|
const files = yield* RepoFiles;
|
|
206269
207024
|
const head = yield* cached(files.committed(file));
|
|
206270
207025
|
const worktree = yield* cached(try_(() => files.working(file)));
|
|
@@ -206275,41 +207030,349 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206275
207030
|
};
|
|
206276
207031
|
for (const g of applicable) {
|
|
206277
207032
|
const refusal = yield* g.check(ctx);
|
|
206278
|
-
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}`));
|
|
206279
207034
|
}
|
|
206280
207035
|
});
|
|
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
|
+
});
|
|
206281
207135
|
/**
|
|
206282
|
-
*
|
|
206283
|
-
*
|
|
206284
|
-
*
|
|
206285
|
-
*
|
|
206286
|
-
*
|
|
206287
|
-
*
|
|
206288
|
-
*
|
|
206289
|
-
*/
|
|
206290
|
-
const
|
|
206291
|
-
const
|
|
206292
|
-
|
|
206293
|
-
|
|
206294
|
-
|
|
206295
|
-
errors: []
|
|
206296
|
-
};
|
|
206297
|
-
const files = yield* RepoFiles;
|
|
206298
|
-
if (files.working(file) === void 0) return {
|
|
206299
|
-
file,
|
|
206300
|
-
present: false,
|
|
206301
|
-
errors: []
|
|
206302
|
-
};
|
|
206303
|
-
const resolved = resolveSteeringMode(rest.def, mode);
|
|
206304
|
-
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
206305
|
-
yield* formatSteeringFile(resolved, file, context);
|
|
206306
|
-
return {
|
|
206307
|
-
file,
|
|
206308
|
-
present: true,
|
|
206309
|
-
errors: yield* validateSteeringFile(resolved, file, files.working(file) ?? "", context)
|
|
206310
|
-
};
|
|
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");
|
|
206311
207149
|
});
|
|
206312
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
|
|
206313
207376
|
//#region src/program.ts
|
|
206314
207377
|
/**
|
|
206315
207378
|
* `gtd lsp`: start the LSP server for `.gtd/` steering files over stdio. Its
|
|
@@ -206321,6 +207384,23 @@ const checkSteeringFile = (rest, context, file) => gen(function* () {
|
|
|
206321
207384
|
*/
|
|
206322
207385
|
const runLspCommand = () => startLspServer();
|
|
206323
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
|
+
/**
|
|
206324
207404
|
* `gtd init`: scaffold a MINIMAL `.gtdrc.json` seeding the default variables a
|
|
206325
207405
|
* fresh project is most likely to change — the test command (`vars.testCommand`)
|
|
206326
207406
|
* and a ready-to-edit Prettier formatting suggestion (`modes:`). It writes NO
|
|
@@ -206334,7 +207414,7 @@ const runLspCommand = () => startLspServer();
|
|
|
206334
207414
|
* instead (`assertInitLocation`, which also allows a directory outside any
|
|
206335
207415
|
* repository) since it writes `.gtdrc.json` at the root and refuses to
|
|
206336
207416
|
* clobber an existing config. The file is left UNCOMMITTED, so the message
|
|
206337
|
-
* warns to commit it before the first `gtd
|
|
207417
|
+
* warns to commit it before the first `gtd land` (an uncommitted config
|
|
206338
207418
|
* counts as a pending change the initial state's `* **` edge would otherwise
|
|
206339
207419
|
* capture).
|
|
206340
207420
|
*/
|
|
@@ -206351,79 +207431,185 @@ const runInitCommand = (json, write) => gen(function* () {
|
|
|
206351
207431
|
written: ".gtdrc.json",
|
|
206352
207432
|
inRepo
|
|
206353
207433
|
}) + "\n");
|
|
206354
|
-
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"));
|
|
206355
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;
|
|
207438
|
+
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
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. */
|
|
207440
|
+
const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
|
|
206356
207441
|
/**
|
|
206357
|
-
*
|
|
206358
|
-
*
|
|
206359
|
-
*
|
|
206360
|
-
*
|
|
206361
|
-
*
|
|
206362
|
-
* null` rather than failing (exit zero, per the plan's "clean no-op exits
|
|
206363
|
-
* zero").
|
|
207442
|
+
* The resting state's own steering-mode commands, embedded ahead of the
|
|
207443
|
+
* commit — the step script now carries its own format/validate pair instead
|
|
207444
|
+
* of gtd running it in process (see `src/StepGuards.ts`'s shrunk registry).
|
|
207445
|
+
* Empty for a state declaring no `file:`+`mode:` pair; an unknown mode name
|
|
207446
|
+
* is a refusal, exactly as it was when gtd ran the commands itself.
|
|
206364
207447
|
*/
|
|
206365
|
-
const
|
|
207448
|
+
const steeringModeSteps = (rest) => gen(function* () {
|
|
207449
|
+
const file = rest.hints.file;
|
|
207450
|
+
const mode = rest.stateDef.mode;
|
|
207451
|
+
if (file === void 0 || mode === void 0) return [];
|
|
207452
|
+
const resolved = resolveSteeringMode(rest.def, mode);
|
|
207453
|
+
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
207454
|
+
return (yield* renderSteeringCommands(resolved, file, rest.context)).map((command) => ({
|
|
207455
|
+
kind: "command",
|
|
207456
|
+
command
|
|
207457
|
+
}));
|
|
207458
|
+
});
|
|
207459
|
+
/**
|
|
207460
|
+
* The `optional` half: re-open the review checkout window when the step lands
|
|
207461
|
+
* at a `reviewWindow: true` state, `""` otherwise.
|
|
207462
|
+
*
|
|
207463
|
+
* `"HEAD"` is deliberately the literal string, not a resolved hash — git
|
|
207464
|
+
* resolves it at the moment this script runs, which is after the required
|
|
207465
|
+
* script has already landed the new commit (see `ReviewWindow.ts`'s
|
|
207466
|
+
* `decideOpenWindow` doc comment). For the same reason it carries no
|
|
207467
|
+
* `expectedHead` precondition: the HEAD it will see is a hash that does not
|
|
207468
|
+
* exist yet at decide time (see `EmitPreconditions`).
|
|
207469
|
+
*/
|
|
207470
|
+
const openWindowScript = (rest, targetState) => {
|
|
207471
|
+
const decision = decideOpenWindow(rest.def, targetState, rest.run, "HEAD");
|
|
207472
|
+
if (!decision.shouldOpen) return "";
|
|
207473
|
+
return emitScripts({}, [], [{
|
|
207474
|
+
kind: "gitWrite",
|
|
207475
|
+
command: buildOpenWindowScript({
|
|
207476
|
+
base: decision.base,
|
|
207477
|
+
head: "HEAD"
|
|
207478
|
+
})
|
|
207479
|
+
}]).optional;
|
|
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;
|
|
207483
|
+
/**
|
|
207484
|
+
* The subject the required script WILL produce. A `"commit"` decision's is
|
|
207485
|
+
* fully deterministic already; a `"squash"` decision's is only known once its
|
|
207486
|
+
* template is rendered. Unreachable-on-failure: `renderDecision` already
|
|
207487
|
+
* rendered the same template and refused the command if it threw, so this
|
|
207488
|
+
* second render can never be the first to fail.
|
|
207489
|
+
*/
|
|
207490
|
+
const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$3(decision.subject) : try_(() => renderStateTemplate(decision.template, rest.context).split("\n")[0].trim()).pipe(catchAll(() => succeed$3(null)));
|
|
207491
|
+
/**
|
|
207492
|
+
* The `required` half: everything that decides what lands in git, in order —
|
|
207493
|
+
* the review-window CLOSE (when one is open), the resting state's own
|
|
207494
|
+
* steering-mode format/validate commands, then the commit/squash steps
|
|
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* () {
|
|
207503
|
+
const git = yield* GitService;
|
|
207504
|
+
const closeDecision = yield* decideCloseWindow;
|
|
207505
|
+
const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, rest.actor, cost, model);
|
|
207506
|
+
const isAttempt = isAttemptDecision(decision);
|
|
207507
|
+
const steps = [
|
|
207508
|
+
...closeDecision.shouldClose ? [{
|
|
207509
|
+
kind: "gitWrite",
|
|
207510
|
+
command: buildCloseWindowScript(closeDecision.refs)
|
|
207511
|
+
}] : [],
|
|
207512
|
+
...isAttempt ? [] : yield* steeringModeSteps(rest),
|
|
207513
|
+
...yield* renderDecision(git, rest, decision, commitContext, cost, model)
|
|
207514
|
+
];
|
|
207515
|
+
return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
|
|
207516
|
+
});
|
|
207517
|
+
/**
|
|
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
|
|
207524
|
+
* `required` script (the review-window CLOSE, when one is open; the resting
|
|
207525
|
+
* state's steering-mode format/validate commands, when it declares
|
|
207526
|
+
* `file:`+`mode:`; then the commit/squash steps themselves, via
|
|
207527
|
+
* `renderDecision`) and an `optional` one (the review-window OPEN, when the
|
|
207528
|
+
* target declares `reviewWindow: true`) — see the "Review-window management"
|
|
207529
|
+
* recipe this mirrors. Refusals fail the Effect with a formatted message; a
|
|
207530
|
+
* no-op returns `subject: null` and empty scripts (exit zero, nothing to
|
|
207531
|
+
* run).
|
|
207532
|
+
*
|
|
207533
|
+
* `plan.scripts` (built by `Edge.ts` itself) is NOT reused here: its baked-in
|
|
207534
|
+
* precondition uses `rest.context.currentCommit`, the LITERAL git HEAD at
|
|
207535
|
+
* decide time — which, while a review window is open, is the window's BASE,
|
|
207536
|
+
* not the real head — and it contains no window close/open steps at all. This
|
|
207537
|
+
* function builds its own required/optional pair to sidestep that trap.
|
|
207538
|
+
*/
|
|
207539
|
+
const planLanding = (opts = {}) => gen(function* () {
|
|
206366
207540
|
const rest = yield* currentRest;
|
|
206367
|
-
const plan = yield* planStep(rest,
|
|
206368
|
-
...cost !== void 0 ? { cost } : {},
|
|
206369
|
-
...model !== void 0 ? { model } : {}
|
|
206370
|
-
});
|
|
207541
|
+
const plan = yield* planStep(rest, opts);
|
|
206371
207542
|
if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
|
|
206372
207543
|
if (plan.kind === "noop") return {
|
|
206373
207544
|
state: plan.state,
|
|
206374
207545
|
subject: null,
|
|
206375
207546
|
cost: null,
|
|
206376
|
-
model: null
|
|
207547
|
+
model: null,
|
|
207548
|
+
required: emitScripts({}, [{
|
|
207549
|
+
kind: "outcome",
|
|
207550
|
+
command: noteOutcome(noopText(plan.state))
|
|
207551
|
+
}]).required,
|
|
207552
|
+
optional: "",
|
|
207553
|
+
settled: plan.settled
|
|
206377
207554
|
};
|
|
207555
|
+
const decision = plan.decision;
|
|
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}"`));
|
|
206378
207557
|
yield* enforceStepGuards({
|
|
206379
207558
|
rest,
|
|
206380
207559
|
context: rest.context,
|
|
206381
207560
|
file: rest.hints.file,
|
|
206382
207561
|
changes: rest.changes,
|
|
206383
|
-
|
|
206384
|
-
|
|
207562
|
+
kind: decision.kind,
|
|
207563
|
+
attempt: isAttemptDecision(decision)
|
|
206385
207564
|
});
|
|
206386
|
-
const
|
|
206387
|
-
if (outcome.kind === "reset" || outcome.kind === "noop") return {
|
|
206388
|
-
state: outcome.state,
|
|
206389
|
-
subject: null,
|
|
206390
|
-
cost: null,
|
|
206391
|
-
model: null
|
|
206392
|
-
};
|
|
207565
|
+
const targetState = decision.kind === "commit" ? decision.to : decision.state;
|
|
206393
207566
|
return {
|
|
206394
207567
|
state: rest.state,
|
|
206395
|
-
subject:
|
|
206396
|
-
cost: cost ?? null,
|
|
206397
|
-
model: model ?? null
|
|
207568
|
+
subject: yield* previewSubject(decision, rest),
|
|
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)
|
|
206398
207574
|
};
|
|
206399
207575
|
});
|
|
206400
|
-
/** Renders `
|
|
206401
|
-
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);
|
|
206402
207579
|
if (json) write(JSON.stringify({
|
|
206403
207580
|
state: result.state,
|
|
206404
207581
|
subject: result.subject,
|
|
206405
207582
|
...result.cost !== null ? { cost: result.cost } : {},
|
|
206406
|
-
...result.model !== null ? { model: result.model } : {}
|
|
207583
|
+
...result.model !== null ? { model: result.model } : {},
|
|
207584
|
+
script,
|
|
207585
|
+
required: result.required,
|
|
207586
|
+
optional: result.optional,
|
|
207587
|
+
settled: result.settled
|
|
206407
207588
|
}) + "\n");
|
|
206408
|
-
else write(
|
|
206409
|
-
};
|
|
206410
|
-
/**
|
|
206411
|
-
* `gtd
|
|
206412
|
-
*
|
|
206413
|
-
*
|
|
206414
|
-
*
|
|
206415
|
-
*
|
|
206416
|
-
*
|
|
206417
|
-
* matched
|
|
206418
|
-
|
|
206419
|
-
|
|
206420
|
-
|
|
206421
|
-
|
|
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;
|
|
206422
207609
|
});
|
|
206423
207610
|
/**
|
|
206424
|
-
* `gtd
|
|
206425
|
-
*
|
|
206426
|
-
* `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>`
|
|
206427
207613
|
* — any declared, non-commit state (see `PatternMachine.enterableStates`) —
|
|
206428
207614
|
* replacing the two former named commands `gtd review <commitish>`/`gtd fix`
|
|
206429
207615
|
* with one generic mechanism. Writes an ordinary turn commit
|
|
@@ -206434,7 +207620,7 @@ const runStepCommand = (actor, cost, model, json, write) => gen(function* () {
|
|
|
206434
207620
|
* the merged `it.vars`, resolved to a commit, and checked sane). Unlike the
|
|
206435
207621
|
* old commands (which required a clean tree and used `commitAsIs`), this
|
|
206436
207622
|
* commits via `commitAllWithPrefix` — capturing whatever the working tree
|
|
206437
|
-
* carries at the moment of entry, exactly like an ordinary `gtd
|
|
207623
|
+
* carries at the moment of entry, exactly like an ordinary `gtd land`
|
|
206438
207624
|
* capture, rather than demanding a clean tree first.
|
|
206439
207625
|
*
|
|
206440
207626
|
* Any failure below is a plain refusal: nothing is written. Checked in order:
|
|
@@ -206461,14 +207647,14 @@ const runEntryCommand = (actor, entryState, varOverrides, json, write, commandLa
|
|
|
206461
207647
|
vars: varOverrides
|
|
206462
207648
|
});
|
|
206463
207649
|
if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
|
|
206464
|
-
yield* plan.perform;
|
|
206465
207650
|
if (json) write(JSON.stringify({
|
|
206466
207651
|
state: plan.state,
|
|
206467
|
-
subject: plan.subject
|
|
207652
|
+
subject: plan.subject,
|
|
207653
|
+
required: plan.scripts.required,
|
|
207654
|
+
optional: plan.scripts.optional
|
|
206468
207655
|
}) + "\n");
|
|
206469
|
-
else write(
|
|
207656
|
+
else write(combinedScript(plan.scripts.required, plan.scripts.optional));
|
|
206470
207657
|
});
|
|
206471
|
-
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
206472
207658
|
/**
|
|
206473
207659
|
* `gtd abandon`: end the process currently underway WITHOUT completing it,
|
|
206474
207660
|
* returning the machine to the workflow's initial state — the recovery path out
|
|
@@ -206500,6 +207686,13 @@ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
|
206500
207686
|
* that fails when there is nothing to recover is a worse tool. The one refusal
|
|
206501
207687
|
* is a process whose first commit is the repository's own root commit: there is
|
|
206502
207688
|
* no earlier commit to rewind to.
|
|
207689
|
+
*
|
|
207690
|
+
* The read-side refusal/no-op checks above stay direct `GitService` reads (the
|
|
207691
|
+
* documented exception, `AGENTS.md`) — abandon must still work when a `Rest`
|
|
207692
|
+
* would refuse. The actual mutation (retaining history, then the mixed reset)
|
|
207693
|
+
* is no longer PERFORMED here: it's emitted as a `required` bash script for
|
|
207694
|
+
* the external driver to run, built from `src/GitScript.ts`'s pure
|
|
207695
|
+
* `updateRef`/`mixedResetTo` — narrowing the exception further still.
|
|
206503
207696
|
*/
|
|
206504
207697
|
const runAbandonCommand = (json, write) => gen(function* () {
|
|
206505
207698
|
const git = yield* GitService;
|
|
@@ -206507,25 +207700,51 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206507
207700
|
const initial = initialStateOf(def);
|
|
206508
207701
|
const run = yield* currentRun;
|
|
206509
207702
|
if (run.trace.length === 0) {
|
|
207703
|
+
const required = emitScripts({}, [{
|
|
207704
|
+
kind: "outcome",
|
|
207705
|
+
command: abandonNoopOutcome(initial)
|
|
207706
|
+
}]).required;
|
|
206510
207707
|
if (json) write(JSON.stringify({
|
|
206511
207708
|
state: initial,
|
|
206512
|
-
abandoned: false
|
|
207709
|
+
abandoned: false,
|
|
207710
|
+
required,
|
|
207711
|
+
optional: ""
|
|
206513
207712
|
}) + "\n");
|
|
206514
|
-
else write(
|
|
207713
|
+
else write(combinedScript(required, ""));
|
|
206515
207714
|
return;
|
|
206516
207715
|
}
|
|
206517
207716
|
const restState = run.trace[run.trace.length - 1].state;
|
|
206518
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`));
|
|
206519
|
-
|
|
206520
|
-
yield* git.
|
|
206521
|
-
const
|
|
207718
|
+
const closeDecision = yield* decideCloseWindow;
|
|
207719
|
+
const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
|
|
207720
|
+
const steps = [
|
|
207721
|
+
...closeDecision.shouldClose ? [{
|
|
207722
|
+
kind: "gitWrite",
|
|
207723
|
+
command: buildCloseWindowScript(closeDecision.refs)
|
|
207724
|
+
}] : [],
|
|
207725
|
+
{
|
|
207726
|
+
kind: "gitWrite",
|
|
207727
|
+
command: updateRef(HISTORY_REF, tip)
|
|
207728
|
+
},
|
|
207729
|
+
{
|
|
207730
|
+
kind: "gitWrite",
|
|
207731
|
+
command: mixedResetTo(run.startParentHash)
|
|
207732
|
+
},
|
|
207733
|
+
{
|
|
207734
|
+
kind: "outcome",
|
|
207735
|
+
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
207736
|
+
}
|
|
207737
|
+
];
|
|
207738
|
+
const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
|
|
206522
207739
|
if (json) write(JSON.stringify({
|
|
206523
207740
|
state: initial,
|
|
206524
207741
|
abandoned: true,
|
|
206525
207742
|
from: restState,
|
|
206526
|
-
head: run.startParentHash
|
|
207743
|
+
head: run.startParentHash,
|
|
207744
|
+
required,
|
|
207745
|
+
optional: ""
|
|
206527
207746
|
}) + "\n");
|
|
206528
|
-
else write(
|
|
207747
|
+
else write(combinedScript(required, ""));
|
|
206529
207748
|
});
|
|
206530
207749
|
/**
|
|
206531
207750
|
* `gtd restore`: hard-reset HEAD back to the pre-squash tip a squash or
|
|
@@ -206537,7 +207756,10 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206537
207756
|
* Guarded by `restorability` so it never discards work it didn't create:
|
|
206538
207757
|
* refuses on a dirty working tree, when there is no retained history, and
|
|
206539
207758
|
* when HEAD has advanced past the retained tip with commits that would be
|
|
206540
|
-
* lost by resetting.
|
|
207759
|
+
* lost by resetting. Those checks stay direct `GitService` reads (same
|
|
207760
|
+
* documented exception as `gtd abandon`); the mutation itself (the hard
|
|
207761
|
+
* reset, then clearing the retained-history ref) is emitted as a `required`
|
|
207762
|
+
* script instead of performed here.
|
|
206541
207763
|
*/
|
|
206542
207764
|
const runRestoreCommand = (json, write) => gen(function* () {
|
|
206543
207765
|
const git = yield* GitService;
|
|
@@ -206549,94 +207771,274 @@ const runRestoreCommand = (json, write) => gen(function* () {
|
|
|
206549
207771
|
const headHash = yield* git.resolveRef("HEAD");
|
|
206550
207772
|
const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
|
|
206551
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)}.`));
|
|
206552
|
-
yield*
|
|
206553
|
-
|
|
206554
|
-
|
|
206555
|
-
|
|
207774
|
+
const after = yield* restAt(tip);
|
|
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
|
+
];
|
|
207789
|
+
const required = emitScripts(headPreconditions(headHash), steps).required;
|
|
206556
207790
|
if (json) write(JSON.stringify({
|
|
206557
207791
|
state: after.state,
|
|
206558
207792
|
restored: true,
|
|
206559
207793
|
to: tip,
|
|
206560
|
-
from: before.state
|
|
207794
|
+
from: before.state,
|
|
207795
|
+
required,
|
|
207796
|
+
optional: ""
|
|
206561
207797
|
}) + "\n");
|
|
206562
|
-
else write(
|
|
207798
|
+
else write(combinedScript(required, ""));
|
|
207799
|
+
});
|
|
207800
|
+
/**
|
|
207801
|
+
* The mode's own resolved VALIDATE command, rendered against `file` — the
|
|
207802
|
+
* command `selfValidateInstruction` names. Resolution mirrors
|
|
207803
|
+
* `renderSteeringCommands`'s own layering (`src/SteeringMode.ts`) but picks
|
|
207804
|
+
* out the validate half alone: a declared `validate:` command (or, for
|
|
207805
|
+
* `qa`/`review`, `PatternConfig.ts`'s own seeded `gtd check <mode> '<file>'`
|
|
207806
|
+
* default — every compiled definition carries ONE of the two) renders as the
|
|
207807
|
+
* LAST command `renderSteeringCommands` would emit (format, if any, always
|
|
207808
|
+
* comes first); a mode resolving to a `"builtin"` validator (no shell command
|
|
207809
|
+
* at all — only reachable when `resolveSteeringMode` is asked about a mode
|
|
207810
|
+
* outside the active definition's own compiled `modes:`, since a real
|
|
207811
|
+
* definition always seeds one) or to no mode at all names the leaf
|
|
207812
|
+
* `gtd check <mode> '<file>'` invocation directly instead
|
|
207813
|
+
* (`seededValidateCommand`), so there is always something concrete to name.
|
|
207814
|
+
*/
|
|
207815
|
+
const resolveSelfValidateCommand = (def, mode, file, context) => gen(function* () {
|
|
207816
|
+
const resolved = resolveSteeringMode(def, mode);
|
|
207817
|
+
if (resolved?.validate?.kind === "command") {
|
|
207818
|
+
const rendered = yield* renderSteeringCommands(resolved, file, context);
|
|
207819
|
+
const validateCommand = rendered[rendered.length - 1];
|
|
207820
|
+
if (validateCommand !== void 0) return validateCommand;
|
|
207821
|
+
}
|
|
207822
|
+
return yield* try_({
|
|
207823
|
+
try: () => renderModeCommand(seededValidateCommand(mode), {
|
|
207824
|
+
...context,
|
|
207825
|
+
file
|
|
207826
|
+
}),
|
|
207827
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
207828
|
+
});
|
|
206563
207829
|
});
|
|
206564
207830
|
/**
|
|
206565
207831
|
* The self-validation instruction gtd APPENDS to a `prompt` rest that declares
|
|
206566
207832
|
* both `file:` and `mode:` — i.e. a state whose actor hands over a steering
|
|
206567
|
-
* file
|
|
206568
|
-
*
|
|
206569
|
-
*
|
|
206570
|
-
*
|
|
206571
|
-
*
|
|
206572
|
-
* advisory: `gtd
|
|
206573
|
-
*
|
|
206574
|
-
*
|
|
206575
|
-
|
|
206576
|
-
|
|
207833
|
+
* file some command validates. Appended ONLY to plain `gtd next` output (for a
|
|
207834
|
+
* human or a simple driver who reads the prompt and hands it to an agent, so
|
|
207835
|
+
* the agent self-validates); withheld from `gtd next --json`, where the
|
|
207836
|
+
* driving loop instead runs `gtd validate` after the turn and re-prompts on
|
|
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.
|
|
207842
|
+
*/
|
|
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:`;
|
|
206577
207854
|
/** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
|
|
206578
207855
|
const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
|
|
206579
|
-
/**
|
|
206580
|
-
|
|
206581
|
-
|
|
206582
|
-
|
|
206583
|
-
|
|
206584
|
-
|
|
206585
|
-
|
|
206586
|
-
|
|
206587
|
-
|
|
206588
|
-
|
|
206589
|
-
|
|
206590
|
-
|
|
206591
|
-
|
|
206592
|
-
|
|
206593
|
-
|
|
206594
|
-
const
|
|
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
|
+
});
|
|
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. */
|
|
207890
|
+
const nextPlainOutput = (rendered, selfValidateCommand) => {
|
|
206595
207891
|
const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
|
|
206596
|
-
return emitsValidatablePrompt(rendered) ? base + selfValidateInstruction(rendered.file) : base;
|
|
206597
|
-
};
|
|
207892
|
+
return emitsValidatablePrompt(rendered) && selfValidateCommand !== void 0 ? base + selfValidateInstruction(selfValidateCommand, rendered.file) : base;
|
|
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
|
+
*/
|
|
206598
207934
|
const runNextCommand = (json, write) => gen(function* () {
|
|
206599
|
-
const
|
|
206600
|
-
|
|
207935
|
+
const rest = yield* currentRest;
|
|
207936
|
+
const rendered = yield* renderRest(rest);
|
|
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));
|
|
206601
207947
|
});
|
|
206602
207948
|
/**
|
|
206603
|
-
* `gtd validate [--json]`: format (in place) then
|
|
206604
|
-
* the resolved rest declares (`file:` rendered,
|
|
206605
|
-
*
|
|
206606
|
-
*
|
|
206607
|
-
*
|
|
206608
|
-
*
|
|
206609
|
-
*
|
|
206610
|
-
*
|
|
206611
|
-
*
|
|
206612
|
-
*
|
|
207949
|
+
* `gtd validate [--json]`: emit the script that would format (in place) then
|
|
207950
|
+
* validate the steering file the resolved rest declares (`file:` rendered,
|
|
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
|
|
207953
|
+
* human or agent to run directly. gtd itself reads no file and executes
|
|
207954
|
+
* nothing: a state with no `file:`/`mode:`, or a file absent from the working
|
|
207955
|
+
* tree, has nothing to validate (`script: ""`, exit 0 either way) — the
|
|
207956
|
+
* verdict now lives in the emitted script's own future exit code, not this
|
|
207957
|
+
* command's, so this never fails on a bad file. `RepoFiles`/`FileSystem` is
|
|
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).
|
|
206613
207966
|
*/
|
|
206614
207967
|
const runValidateCommand = (json, write) => gen(function* () {
|
|
206615
207968
|
const rest = yield* currentRest;
|
|
206616
|
-
const
|
|
206617
|
-
|
|
207969
|
+
const file = rest.hints.file;
|
|
207970
|
+
const mode = rest.stateDef.mode;
|
|
207971
|
+
const emitNothingToValidate = () => {
|
|
206618
207972
|
if (json) write(JSON.stringify({
|
|
206619
207973
|
state: rest.state,
|
|
206620
|
-
|
|
206621
|
-
|
|
207974
|
+
...file !== void 0 ? { file } : {},
|
|
207975
|
+
...mode !== void 0 ? { mode } : {},
|
|
207976
|
+
script: ""
|
|
206622
207977
|
}) + "\n");
|
|
206623
207978
|
else write(`nothing to validate at "${rest.state}"\n`);
|
|
207979
|
+
};
|
|
207980
|
+
const resolved = yield* resolveValidateScript(rest);
|
|
207981
|
+
if (resolved === void 0) {
|
|
207982
|
+
emitNothingToValidate();
|
|
206624
207983
|
return;
|
|
206625
207984
|
}
|
|
207985
|
+
if (json) write(JSON.stringify({
|
|
207986
|
+
state: rest.state,
|
|
207987
|
+
...resolved
|
|
207988
|
+
}) + "\n");
|
|
207989
|
+
else write(resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
207990
|
+
});
|
|
207991
|
+
/**
|
|
207992
|
+
* `gtd check <mode> <file>`: read `<file>` and run the BUILT-IN steering
|
|
207993
|
+
* format named `<mode>`'s pure parser over its contents, printing each
|
|
207994
|
+
* finding one per line and exiting non-zero when there are any. Unlike `gtd
|
|
207995
|
+
* validate` (which resolves the currently resting state's own `file:`/`mode:`
|
|
207996
|
+
* and formats before validating), this resolves NO workflow state and reads
|
|
207997
|
+
* NO config — both `mode` and `file` are given explicitly, so it needs only
|
|
207998
|
+
* `FileSystem.FileSystem` and runs from any directory. This is what a
|
|
207999
|
+
* workflow's seeded `validate:` command (`SteeringFormats.ts`'s
|
|
208000
|
+
* `seededValidateCommand`) invokes as a leaf step; it does no formatting
|
|
208001
|
+
* in-place — an emitted script has no template context to format against.
|
|
208002
|
+
*
|
|
208003
|
+
* An absent file mirrors `gtd validate`'s own absent-file behavior: exit 0,
|
|
208004
|
+
* no output in plain mode, `{valid: true, errors: []}` under `--json`. A
|
|
208005
|
+
* clean parse is the same shape. A non-clean parse writes `--json`'s
|
|
208006
|
+
* `{valid: false, errors}` body (or each finding, one per line, in plain
|
|
208007
|
+
* mode) BEFORE failing the Effect. In PLAIN mode the failure then carries only
|
|
208008
|
+
* a summary message, landing on stderr alone (`cliErrorLine`'s line) — the
|
|
208009
|
+
* findings are already on stdout. Under `--json`, `Cli.ts`'s single envelope
|
|
208010
|
+
* writer (`report`) ALSO puts its own `{state:"error",prompt}` object on
|
|
208011
|
+
* stdout for any failing Effect, so a `--json` invocation with findings emits
|
|
208012
|
+
* stdout as two newline-delimited JSON documents — this handler's own
|
|
208013
|
+
* `{valid,errors}` body first, then the generic error envelope — rather than
|
|
208014
|
+
* one; a consumer must read line-delimited JSON, not `JSON.parse(stdout)`
|
|
208015
|
+
* whole. Kept as the pragmatic resolution of a real tension in this command's
|
|
208016
|
+
* two governing requirements ("exits non-zero" and "`--json` reports
|
|
208017
|
+
* `{valid,errors}`"): the alternative was piping a `--json` failure through a
|
|
208018
|
+
* NEW exit-code channel bypassing `Cli.ts`'s shared envelope entirely, which
|
|
208019
|
+
* every other command's `--json` failure goes through today.
|
|
208020
|
+
*/
|
|
208021
|
+
const runCheckCommand = (mode, file, json, write) => gen(function* () {
|
|
208022
|
+
const format = steeringFormatFor(mode);
|
|
208023
|
+
if (format === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: unknown mode "${mode}" — known modes: ${builtInModeNames().join(", ")}`));
|
|
208024
|
+
const fs = yield* FileSystem;
|
|
208025
|
+
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
208026
|
+
const errors = (yield* fs.exists(file).pipe(mapError$1(toError))) ? format.validate(yield* fs.readFileString(file).pipe(mapError$1(toError))) : [];
|
|
206626
208027
|
if (errors.length === 0) {
|
|
206627
208028
|
if (json) write(JSON.stringify({
|
|
206628
|
-
state: rest.state,
|
|
206629
|
-
file,
|
|
206630
|
-
mode: rest.stateDef.mode,
|
|
206631
208029
|
valid: true,
|
|
206632
208030
|
errors: []
|
|
206633
208031
|
}) + "\n");
|
|
206634
|
-
else write(`${file}: valid\n`);
|
|
206635
208032
|
return;
|
|
206636
208033
|
}
|
|
206637
|
-
|
|
208034
|
+
if (json) write(JSON.stringify({
|
|
208035
|
+
valid: false,
|
|
208036
|
+
errors
|
|
208037
|
+
}) + "\n");
|
|
208038
|
+
else write(errors.join("\n") + "\n");
|
|
208039
|
+
return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s))`));
|
|
206638
208040
|
});
|
|
206639
|
-
/** 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. */
|
|
206640
208042
|
const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
|
|
206641
208043
|
const matchedRow = onEdges.find(([patternStr]) => {
|
|
206642
208044
|
const parsed = parsePattern(patternStr);
|
|
@@ -206840,48 +208242,59 @@ const assertInitLocation = (git, fs) => gen(function* () {
|
|
|
206840
208242
|
*/
|
|
206841
208243
|
const needsOf = (kind) => {
|
|
206842
208244
|
switch (kind) {
|
|
206843
|
-
case "lsp":
|
|
206844
|
-
case "
|
|
208245
|
+
case "lsp":
|
|
208246
|
+
case "install": return "none";
|
|
208247
|
+
case "init":
|
|
208248
|
+
case "check": return "fs";
|
|
206845
208249
|
case "visualize": return "config";
|
|
206846
208250
|
default: return "state";
|
|
206847
208251
|
}
|
|
206848
208252
|
};
|
|
206849
|
-
/** 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. */
|
|
206850
|
-
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) => {
|
|
206851
208255
|
switch (command.kind) {
|
|
206852
208256
|
case "lsp": return runLspCommand();
|
|
206853
208257
|
case "init": return runInitCommand(json, write);
|
|
206854
208258
|
case "visualize": return runVisualizeCommand(command.port, command.open, json, write);
|
|
206855
|
-
case "step": return runStepCommand(command.actor, command.cost, command.model, json, write);
|
|
206856
208259
|
case "entry": return runEntryCommand(command.actor, command.state, command.vars, json, write, command.label);
|
|
206857
208260
|
case "abandon": return runAbandonCommand(json, write);
|
|
206858
208261
|
case "restore": return runRestoreCommand(json, write);
|
|
206859
208262
|
case "next": return runNextCommand(json, write);
|
|
206860
208263
|
case "status": return runStatusCommand(json, write);
|
|
206861
208264
|
case "validate": return runValidateCommand(json, write);
|
|
208265
|
+
case "check": return runCheckCommand(command.mode, command.file, json, write);
|
|
208266
|
+
case "install": return runInstallCommand(json, write);
|
|
206862
208267
|
}
|
|
206863
208268
|
};
|
|
206864
208269
|
/**
|
|
206865
|
-
*
|
|
206866
|
-
*
|
|
206867
|
-
*
|
|
206868
|
-
* `
|
|
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.
|
|
206869
208275
|
*/
|
|
206870
|
-
const
|
|
206871
|
-
|
|
206872
|
-
|
|
206873
|
-
|
|
206874
|
-
|
|
206875
|
-
|
|
206876
|
-
|
|
206877
|
-
});
|
|
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
|
+
};
|
|
206878
208283
|
/**
|
|
206879
208284
|
* The one entry point `Cli.ts`'s `runCli` calls for a resolved `Command`:
|
|
206880
208285
|
* dispatches to the matching `run*Command` handler (see `dispatchCommand`),
|
|
206881
|
-
* wrapped in the repo-root guard
|
|
206882
|
-
*
|
|
206883
|
-
*
|
|
206884
|
-
*
|
|
208286
|
+
* wrapped in the repo-root guard exactly when `needsOf(command.kind) ===
|
|
208287
|
+
* "state"` — `lsp`/`init`/`visualize`/`check` (the `standaloneKinds`) run
|
|
208288
|
+
* bare. `Cli.ts` has already validated every field on `command` (arity, flag
|
|
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`).
|
|
208292
|
+
*
|
|
208293
|
+
* No review-window close/open bracket runs here any more: every state-command
|
|
208294
|
+
* handler (see `planLanding`) now decides for itself, off `ReviewWindow.ts`'s
|
|
208295
|
+
* pure `decideCloseWindow`/`decideOpenWindow`, whether a close/open belongs in
|
|
208296
|
+
* the script it emits — there is no in-process HEAD to rewind ahead of time
|
|
208297
|
+
* (gtd itself no longer writes git for a state command at all).
|
|
206885
208298
|
*/
|
|
206886
208299
|
const runCommand = (command, json, write) => {
|
|
206887
208300
|
const dispatch = dispatchCommand(command, json, write);
|
|
@@ -206889,7 +208302,8 @@ const runCommand = (command, json, write) => {
|
|
|
206889
208302
|
return gen(function* () {
|
|
206890
208303
|
const git = yield* GitService;
|
|
206891
208304
|
const fs = yield* FileSystem;
|
|
206892
|
-
yield*
|
|
208305
|
+
yield* assertRunningFromRepoRoot(git, fs);
|
|
208306
|
+
return yield* dispatch;
|
|
206893
208307
|
});
|
|
206894
208308
|
};
|
|
206895
208309
|
//#endregion
|
|
@@ -206948,21 +208362,21 @@ const FLAGS = [
|
|
|
206948
208362
|
name: "--cost",
|
|
206949
208363
|
arity: 1,
|
|
206950
208364
|
repeatable: false,
|
|
206951
|
-
scope: (kind) => kind === "
|
|
208365
|
+
scope: (kind) => kind === "land",
|
|
206952
208366
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
206953
|
-
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",
|
|
206954
208368
|
valueHint: "<n>",
|
|
206955
|
-
help: ["(gtd
|
|
208369
|
+
help: ["(gtd land only) record the invocation's token cost"]
|
|
206956
208370
|
},
|
|
206957
208371
|
{
|
|
206958
208372
|
name: "--model",
|
|
206959
208373
|
arity: 1,
|
|
206960
208374
|
repeatable: false,
|
|
206961
|
-
scope: (kind) => kind === "
|
|
208375
|
+
scope: (kind) => kind === "land",
|
|
206962
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),
|
|
206963
|
-
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",
|
|
206964
208378
|
valueHint: "<name>",
|
|
206965
|
-
help: ["(gtd
|
|
208379
|
+
help: ["(gtd land only, with --cost) tag that cost's model"]
|
|
206966
208380
|
},
|
|
206967
208381
|
{
|
|
206968
208382
|
name: "--entry",
|
|
@@ -206970,14 +208384,12 @@ const FLAGS = [
|
|
|
206970
208384
|
repeatable: false,
|
|
206971
208385
|
scope: (kind) => kind === "entry",
|
|
206972
208386
|
decode: ([raw]) => right(raw ?? ""),
|
|
206973
|
-
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",
|
|
206974
208388
|
valueHint: "<state>",
|
|
206975
208389
|
help: [
|
|
206976
|
-
"(
|
|
206977
|
-
"
|
|
206978
|
-
"
|
|
206979
|
-
"combinable with --cost/--model (an entry is not a metered",
|
|
206980
|
-
"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"
|
|
206981
208393
|
]
|
|
206982
208394
|
},
|
|
206983
208395
|
{
|
|
@@ -207031,20 +208443,22 @@ const COMMAND_ROWS = [
|
|
|
207031
208443
|
]
|
|
207032
208444
|
},
|
|
207033
208445
|
{
|
|
207034
|
-
token: "
|
|
207035
|
-
kind: "
|
|
207036
|
-
arity:
|
|
208446
|
+
token: "land",
|
|
208447
|
+
kind: "land",
|
|
208448
|
+
arity: "none",
|
|
207037
208449
|
details: [
|
|
207038
|
-
"
|
|
207039
|
-
"
|
|
207040
|
-
"(
|
|
207041
|
-
"
|
|
207042
|
-
"
|
|
207043
|
-
"
|
|
207044
|
-
"
|
|
207045
|
-
"
|
|
207046
|
-
"
|
|
207047
|
-
"
|
|
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"
|
|
207048
208462
|
]
|
|
207049
208463
|
},
|
|
207050
208464
|
{
|
|
@@ -207076,7 +208490,14 @@ const COMMAND_ROWS = [
|
|
|
207076
208490
|
token: "next",
|
|
207077
208491
|
kind: "next",
|
|
207078
208492
|
arity: "none",
|
|
207079
|
-
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
|
+
]
|
|
207080
208501
|
},
|
|
207081
208502
|
{
|
|
207082
208503
|
token: "status",
|
|
@@ -207089,9 +208510,17 @@ const COMMAND_ROWS = [
|
|
|
207089
208510
|
kind: "validate",
|
|
207090
208511
|
arity: "none",
|
|
207091
208512
|
details: [
|
|
207092
|
-
"
|
|
207093
|
-
"
|
|
207094
|
-
"
|
|
208513
|
+
"Print the script that formats (when declared) then",
|
|
208514
|
+
"validates the resolved rest's steering file, using its",
|
|
208515
|
+
"mode's commands (its file:/mode:), instead of running it —",
|
|
208516
|
+
"a driver runs the script and reads the findings from its",
|
|
208517
|
+
"own exit code/output. Always exits 0; --json emits",
|
|
208518
|
+
"{state, file?, mode?, script} (script is \"\" when there is",
|
|
208519
|
+
"nothing to validate; plain text prints \"nothing to",
|
|
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"
|
|
207095
208524
|
]
|
|
207096
208525
|
},
|
|
207097
208526
|
{
|
|
@@ -207109,6 +208538,33 @@ const COMMAND_ROWS = [
|
|
|
207109
208538
|
"local web server (--port <n>, --no-open; --json prints the",
|
|
207110
208539
|
"model and exits)"
|
|
207111
208540
|
]
|
|
208541
|
+
},
|
|
208542
|
+
{
|
|
208543
|
+
token: "check",
|
|
208544
|
+
kind: "check",
|
|
208545
|
+
arity: { names: ["mode", "file"] },
|
|
208546
|
+
details: [
|
|
208547
|
+
"Read <file> and run the built-in steering format named",
|
|
208548
|
+
"<mode> (see `gtd validate`'s modes: qa, review) over its",
|
|
208549
|
+
"contents, printing each finding one per line and exiting",
|
|
208550
|
+
"non-zero when there are any. Resolves no workflow state and",
|
|
208551
|
+
"reads no config — standalone, runnable from any directory",
|
|
208552
|
+
"with <mode>/<file> given explicitly. This is what a",
|
|
208553
|
+
"workflow's emitted validation script invokes as a leaf step"
|
|
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
|
+
]
|
|
207112
208568
|
}
|
|
207113
208569
|
];
|
|
207114
208570
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
@@ -207121,8 +208577,10 @@ const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
|
207121
208577
|
* enterable states.
|
|
207122
208578
|
*/
|
|
207123
208579
|
const REMOVED = {
|
|
207124
|
-
|
|
207125
|
-
|
|
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"
|
|
207126
208584
|
};
|
|
207127
208585
|
const COLUMN = 19;
|
|
207128
208586
|
const renderBlock = (header, lines) => {
|
|
@@ -207130,14 +208588,7 @@ const renderBlock = (header, lines) => {
|
|
|
207130
208588
|
const headerCell = ` ${header}`;
|
|
207131
208589
|
return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
|
|
207132
208590
|
};
|
|
207133
|
-
const
|
|
207134
|
-
"Launch the loop driver (bin/gtd), which repeatedly drives",
|
|
207135
|
-
"an agent through gtd next/gtd step calls until the",
|
|
207136
|
-
"workflow rests at a human gate (a non-autonomous state)",
|
|
207137
|
-
"or settles. A bare gtd invocation and gtd loop both",
|
|
207138
|
-
"launch it identically"
|
|
207139
|
-
]);
|
|
207140
|
-
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>'"]);
|
|
207141
208592
|
const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
|
|
207142
208593
|
const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
|
|
207143
208594
|
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--entry", "--var"]);
|
|
@@ -207147,10 +208598,17 @@ const flagHeader = (row) => {
|
|
|
207147
208598
|
};
|
|
207148
208599
|
const renderHelp = () => {
|
|
207149
208600
|
const commandBlocks = COMMAND_ROWS.map((row) => {
|
|
207150
|
-
const header = row.arity === "none" ? row.token : `${row.token} <${row.arity.name}>`;
|
|
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}>`;
|
|
207151
208602
|
return renderBlock(header, row.details);
|
|
207152
208603
|
});
|
|
207153
|
-
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"])}`;
|
|
207154
208612
|
};
|
|
207155
208613
|
/**
|
|
207156
208614
|
* The stderr line for a CLI error: a `gtd: ` prefix UNLESS the message
|
|
@@ -207233,6 +208691,12 @@ const scopeViolation = (kind, present) => {
|
|
|
207233
208691
|
};
|
|
207234
208692
|
const arityError = (cmd, rest, arity) => {
|
|
207235
208693
|
if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
|
|
208694
|
+
if ("names" in arity) {
|
|
208695
|
+
const [first, second] = arity.names;
|
|
208696
|
+
if (rest.length < 2) return rest.length === 0 ? `gtd ${cmd}: missing ${first} and ${second} arguments` : `gtd ${cmd}: missing ${second} argument`;
|
|
208697
|
+
if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
|
|
208698
|
+
return;
|
|
208699
|
+
}
|
|
207236
208700
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
207237
208701
|
if (rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
|
|
207238
208702
|
};
|
|
@@ -207251,6 +208715,12 @@ const decodeFlags = (byFlag) => {
|
|
|
207251
208715
|
}
|
|
207252
208716
|
return right(bag);
|
|
207253
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
|
+
});
|
|
207254
208724
|
const parseArgv = (argv) => {
|
|
207255
208725
|
if (argv.includes("--help") || argv.includes("-h")) return {
|
|
207256
208726
|
kind: "output",
|
|
@@ -207276,12 +208746,12 @@ const parseArgv = (argv) => {
|
|
|
207276
208746
|
};
|
|
207277
208747
|
const row = first === void 0 ? void 0 : commandByToken(first);
|
|
207278
208748
|
const removedMessage = first === void 0 ? void 0 : REMOVED[first];
|
|
207279
|
-
const selectsEntry = entryPresent &&
|
|
208749
|
+
const selectsEntry = entryPresent && first === void 0;
|
|
207280
208750
|
const kind = selectsEntry ? "entry" : row?.kind;
|
|
207281
208751
|
if (row === void 0 && removedMessage === void 0 && !selectsEntry) {
|
|
207282
208752
|
const violation = scopeViolation(void 0, present);
|
|
207283
208753
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
207284
|
-
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());
|
|
207285
208755
|
return usagePlan(`unknown command '${first}'`, jsonSeen);
|
|
207286
208756
|
}
|
|
207287
208757
|
if (removedMessage !== void 0 && !selectsEntry) {
|
|
@@ -207292,8 +208762,7 @@ const parseArgv = (argv) => {
|
|
|
207292
208762
|
if (kind === "entry" && first === void 0) {
|
|
207293
208763
|
if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
|
|
207294
208764
|
} else {
|
|
207295
|
-
const
|
|
207296
|
-
const arityMsg = arityError(first, restPositionals, arityRow.arity);
|
|
208765
|
+
const arityMsg = arityError(first, restPositionals, row.arity);
|
|
207297
208766
|
if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
|
|
207298
208767
|
}
|
|
207299
208768
|
const violation = scopeViolation(kind, present);
|
|
@@ -207302,27 +208771,21 @@ const parseArgv = (argv) => {
|
|
|
207302
208771
|
if (isLeft(decoded)) return usagePlan(decoded.left, jsonSeen);
|
|
207303
208772
|
const bag = decoded.right;
|
|
207304
208773
|
const json = present.has("--json");
|
|
207305
|
-
if (kind === "
|
|
208774
|
+
if (kind === "land") {
|
|
207306
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);
|
|
207307
208776
|
return {
|
|
207308
208777
|
kind: "command",
|
|
207309
|
-
command:
|
|
207310
|
-
kind: "step",
|
|
207311
|
-
actor: restPositionals[0],
|
|
207312
|
-
...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
|
|
207313
|
-
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
207314
|
-
},
|
|
208778
|
+
command: buildLandCommand(bag),
|
|
207315
208779
|
json
|
|
207316
208780
|
};
|
|
207317
208781
|
}
|
|
207318
208782
|
if (kind === "entry") {
|
|
207319
|
-
const
|
|
207320
|
-
const label = first === void 0 ? `gtd --entry ${entryRaw}` : `gtd step ${actor} --entry ${entryRaw}`;
|
|
208783
|
+
const label = `gtd --entry ${entryRaw}`;
|
|
207321
208784
|
return {
|
|
207322
208785
|
kind: "command",
|
|
207323
208786
|
command: {
|
|
207324
208787
|
kind: "entry",
|
|
207325
|
-
actor,
|
|
208788
|
+
actor: "human",
|
|
207326
208789
|
state: entryRaw,
|
|
207327
208790
|
vars: bag["--var"] ?? {},
|
|
207328
208791
|
label
|
|
@@ -207339,6 +208802,15 @@ const parseArgv = (argv) => {
|
|
|
207339
208802
|
},
|
|
207340
208803
|
json
|
|
207341
208804
|
};
|
|
208805
|
+
if (kind === "check") return {
|
|
208806
|
+
kind: "command",
|
|
208807
|
+
command: {
|
|
208808
|
+
kind: "check",
|
|
208809
|
+
mode: restPositionals[0],
|
|
208810
|
+
file: restPositionals[1]
|
|
208811
|
+
},
|
|
208812
|
+
json
|
|
208813
|
+
};
|
|
207342
208814
|
return {
|
|
207343
208815
|
kind: "command",
|
|
207344
208816
|
command: { kind },
|
|
@@ -207387,7 +208859,9 @@ const runCli = (argv, io) => {
|
|
|
207387
208859
|
io.stderr(`${cliErrorLine(plan.message)}\n`);
|
|
207388
208860
|
io.exit(1);
|
|
207389
208861
|
});
|
|
207390
|
-
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)));
|
|
207391
208865
|
};
|
|
207392
208866
|
//#endregion
|
|
207393
208867
|
//#region src/main.ts
|