@pmelab/gtd 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gtd.bundle.mjs +275 -43
- package/docs/cli.md +16 -5
- package/docs/configuration.md +90 -1
- package/docs/upgrading.md +8 -2
- package/package.json +1 -1
- package/schema.json +30 -2
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -189117,6 +189117,10 @@ const contentKindOf = (state) => {
|
|
|
189117
189117
|
};
|
|
189118
189118
|
/** True when a state is a commit (final, squash) state. */
|
|
189119
189119
|
const isCommitState = (state) => state.commit !== void 0;
|
|
189120
|
+
/** True when a rest at `state` should open the review checkout window (see `StateDef.reviewWindow`). Safe for an unknown state name (returns `false`). */
|
|
189121
|
+
const isReviewWindowState = (def, state) => def.states[state]?.reviewWindow === true;
|
|
189122
|
+
/** True when `state` anchors the review window's diff base (see `StateDef.reviewBase`). Safe for an unknown state name (returns `false`). */
|
|
189123
|
+
const isReviewBaseState = (def, state) => def.states[state]?.reviewBase === true;
|
|
189120
189124
|
/**
|
|
189121
189125
|
* `gtd(<actor>): <state>` — the subject a step commit carries. Per decision
|
|
189122
189126
|
* 2, `<actor>` is WHO AUTHORED THE STEP (the invoker), and `<state>` is the
|
|
@@ -189405,6 +189409,19 @@ const validateMode = (name, state) => {
|
|
|
189405
189409
|
if (isCommitState(state)) errors.push(`state "${name}": a commit state cannot declare "mode"`);
|
|
189406
189410
|
return errors;
|
|
189407
189411
|
};
|
|
189412
|
+
/**
|
|
189413
|
+
* `reviewWindow`/`reviewBase`, when present, are booleans (the compiler
|
|
189414
|
+
* enforces the type) — forbidden on a commit state, same rule family as
|
|
189415
|
+
* `model`/`file`/`mode`: a commit state is never at rest, so no window ever
|
|
189416
|
+
* opens or anchors there.
|
|
189417
|
+
*/
|
|
189418
|
+
const validateReviewWindow = (name, state) => {
|
|
189419
|
+
if (!isCommitState(state)) return [];
|
|
189420
|
+
const errors = [];
|
|
189421
|
+
if (state.reviewWindow !== void 0) errors.push(`state "${name}": a commit state cannot declare "reviewWindow"`);
|
|
189422
|
+
if (state.reviewBase !== void 0) errors.push(`state "${name}": a commit state cannot declare "reviewBase"`);
|
|
189423
|
+
return errors;
|
|
189424
|
+
};
|
|
189408
189425
|
/** Every `on` row parses, and its target names a defined state. */
|
|
189409
189426
|
const validateOnEdges = (name, state, names) => {
|
|
189410
189427
|
const errors = [];
|
|
@@ -189463,7 +189480,8 @@ const validateState = (def, name, names) => {
|
|
|
189463
189480
|
...validateRetry(name, state, names),
|
|
189464
189481
|
...validateModel(name, state),
|
|
189465
189482
|
...validateFile(name, state),
|
|
189466
|
-
...validateMode(name, state)
|
|
189483
|
+
...validateMode(name, state),
|
|
189484
|
+
...validateReviewWindow(name, state)
|
|
189467
189485
|
];
|
|
189468
189486
|
};
|
|
189469
189487
|
/**
|
|
@@ -189480,8 +189498,9 @@ const validateState = (def, name, names) => {
|
|
|
189480
189498
|
* never declared on a commit state; `file`, when present, is a non-empty
|
|
189481
189499
|
* string and is never declared on a commit state; `mode`, when present, is
|
|
189482
189500
|
* one of the closed vocabulary (`qa`/`review`), requires a sibling `file`,
|
|
189483
|
-
* and is never declared on a commit state;
|
|
189484
|
-
*
|
|
189501
|
+
* and is never declared on a commit state; `reviewWindow`/`reviewBase`, when
|
|
189502
|
+
* present, are never declared on a commit state; every state is reachable from
|
|
189503
|
+
* the initial state by walking `on` targets and `retry.otherwise` redirects
|
|
189485
189504
|
* (checked only when the initial-state rule itself passed — see
|
|
189486
189505
|
* `validateReachability`).
|
|
189487
189506
|
*/
|
|
@@ -189517,7 +189536,8 @@ const validateDefinition = (def) => {
|
|
|
189517
189536
|
* actor: <string> # forbidden on a commit state, required otherwise
|
|
189518
189537
|
* script: <string> # exactly one of script/prompt/message/commit
|
|
189519
189538
|
* on: # a mapping, DECLARATION ORDER PRESERVED
|
|
189520
|
-
* "<pattern>": <targetState>
|
|
189539
|
+
* "<pattern>": <targetState> # short form
|
|
189540
|
+
* "<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
|
|
189521
189541
|
* initial: true # exactly one state across the whole workflow
|
|
189522
189542
|
* retry:
|
|
189523
189543
|
* max: <number>
|
|
@@ -189617,7 +189637,9 @@ const KNOWN_STATE_KEYS = /* @__PURE__ */ new Set([
|
|
|
189617
189637
|
"retry",
|
|
189618
189638
|
"model",
|
|
189619
189639
|
"file",
|
|
189620
|
-
"mode"
|
|
189640
|
+
"mode",
|
|
189641
|
+
"reviewWindow",
|
|
189642
|
+
"reviewBase"
|
|
189621
189643
|
]);
|
|
189622
189644
|
const KNOWN_TOP_KEYS = /* @__PURE__ */ new Set(["vars", "states"]);
|
|
189623
189645
|
const formatErrors = (errors) => `workflow config:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
@@ -189642,7 +189664,42 @@ const resolveContent = (value, configDir, where, errors) => {
|
|
|
189642
189664
|
return;
|
|
189643
189665
|
}
|
|
189644
189666
|
};
|
|
189645
|
-
|
|
189667
|
+
const KNOWN_EDGE_KEYS = /* @__PURE__ */ new Set(["to", "describe"]);
|
|
189668
|
+
/**
|
|
189669
|
+
* Compile one `on` row's value into an `OnEdge` (or `undefined`, pushing a
|
|
189670
|
+
* finding, when the value is malformed). The value is EITHER a target-state
|
|
189671
|
+
* name (a string) OR a `{ to: <target>, describe: <sentence> }` object — the
|
|
189672
|
+
* object form attaches an optional human-readable `describe` a `message:`
|
|
189673
|
+
* template can surface at a rest (see `PatternMachine.OnEdge`).
|
|
189674
|
+
*/
|
|
189675
|
+
const compileOnEdge = (pattern, value, name, errors) => {
|
|
189676
|
+
if (typeof value === "string") return [pattern, value];
|
|
189677
|
+
if (!isPlainObject$1(value)) {
|
|
189678
|
+
errors.push(`state "${name}": "on" entry for pattern "${pattern}" must be a target state name (string) or a { to, describe } object`);
|
|
189679
|
+
return;
|
|
189680
|
+
}
|
|
189681
|
+
const unknownKeys = Object.keys(value).filter((k) => !KNOWN_EDGE_KEYS.has(k));
|
|
189682
|
+
if (unknownKeys.length > 0) errors.push(`state "${name}": "on" entry for pattern "${pattern}" has unknown key(s) ${unknownKeys.join(", ")}`);
|
|
189683
|
+
const { to, describe } = value;
|
|
189684
|
+
if (typeof to !== "string") {
|
|
189685
|
+
errors.push(`state "${name}": "on.${pattern}.to" must be a target state name (string)`);
|
|
189686
|
+
return;
|
|
189687
|
+
}
|
|
189688
|
+
if (describe !== void 0 && typeof describe !== "string") {
|
|
189689
|
+
errors.push(`state "${name}": "on.${pattern}.describe" must be a string`);
|
|
189690
|
+
return;
|
|
189691
|
+
}
|
|
189692
|
+
return describe !== void 0 ? [
|
|
189693
|
+
pattern,
|
|
189694
|
+
to,
|
|
189695
|
+
describe
|
|
189696
|
+
] : [pattern, to];
|
|
189697
|
+
};
|
|
189698
|
+
/**
|
|
189699
|
+
* The `on` mapping: pattern -> edge, preserving declaration order as `OnEdge`
|
|
189700
|
+
* tuples. Each row's value is compiled by `compileOnEdge` (a target string or
|
|
189701
|
+
* a `{ to, describe }` object).
|
|
189702
|
+
*/
|
|
189646
189703
|
const compileOn = (raw, name, errors) => {
|
|
189647
189704
|
if (raw === void 0) return void 0;
|
|
189648
189705
|
if (!isPlainObject$1(raw)) {
|
|
@@ -189650,12 +189707,9 @@ const compileOn = (raw, name, errors) => {
|
|
|
189650
189707
|
return;
|
|
189651
189708
|
}
|
|
189652
189709
|
const edges = [];
|
|
189653
|
-
for (const [pattern,
|
|
189654
|
-
|
|
189655
|
-
|
|
189656
|
-
continue;
|
|
189657
|
-
}
|
|
189658
|
-
edges.push([pattern, target]);
|
|
189710
|
+
for (const [pattern, value] of Object.entries(raw)) {
|
|
189711
|
+
const edge = compileOnEdge(pattern, value, name, errors);
|
|
189712
|
+
if (edge !== void 0) edges.push(edge);
|
|
189659
189713
|
}
|
|
189660
189714
|
return edges;
|
|
189661
189715
|
};
|
|
@@ -189730,13 +189784,21 @@ const compileMode = (raw, name, errors) => {
|
|
|
189730
189784
|
return raw.mode;
|
|
189731
189785
|
};
|
|
189732
189786
|
/** The `initial` field: `true` only when the raw value is the literal boolean `true`. */
|
|
189733
|
-
const compileInitial = (raw, name, errors) =>
|
|
189734
|
-
|
|
189735
|
-
|
|
189736
|
-
|
|
189787
|
+
const compileInitial = (raw, name, errors) => compileBooleanFlag(raw, "initial", name, errors);
|
|
189788
|
+
/**
|
|
189789
|
+
* A boolean state flag (`initial`/`reviewWindow`/`reviewBase`): `true` only
|
|
189790
|
+
* when the raw value is the literal `true`; a non-boolean is a config error;
|
|
189791
|
+
* `false` (or absent) compiles away to `undefined` so it never lands in the
|
|
189792
|
+
* `StateDef` — `false` and "unset" mean the same thing for every such flag.
|
|
189793
|
+
*/
|
|
189794
|
+
const compileBooleanFlag = (raw, key, name, errors) => {
|
|
189795
|
+
const value = raw[key];
|
|
189796
|
+
if (value === void 0) return void 0;
|
|
189797
|
+
if (value !== true && value !== false) {
|
|
189798
|
+
errors.push(`state "${name}": "${key}" must be a boolean`);
|
|
189737
189799
|
return;
|
|
189738
189800
|
}
|
|
189739
|
-
return
|
|
189801
|
+
return value === true ? true : void 0;
|
|
189740
189802
|
};
|
|
189741
189803
|
const assembleContentFields = (content) => ({
|
|
189742
189804
|
...content.script !== void 0 ? { script: content.script } : {},
|
|
@@ -189760,7 +189822,9 @@ const assembleStateDef = (parts) => ({
|
|
|
189760
189822
|
retry: parts.retry,
|
|
189761
189823
|
model: parts.model,
|
|
189762
189824
|
file: parts.file,
|
|
189763
|
-
mode: parts.mode
|
|
189825
|
+
mode: parts.mode,
|
|
189826
|
+
reviewWindow: parts.reviewWindow,
|
|
189827
|
+
reviewBase: parts.reviewBase
|
|
189764
189828
|
}),
|
|
189765
189829
|
...assembleContentFields(parts.content)
|
|
189766
189830
|
});
|
|
@@ -189780,7 +189844,9 @@ const compileState = (name, raw, configDir, errors) => {
|
|
|
189780
189844
|
retry: compileRetry(raw.retry, name, errors),
|
|
189781
189845
|
model: compileModel(raw, name, errors),
|
|
189782
189846
|
file: compileFile(raw, name, errors),
|
|
189783
|
-
mode: compileMode(raw, name, errors)
|
|
189847
|
+
mode: compileMode(raw, name, errors),
|
|
189848
|
+
reviewWindow: compileBooleanFlag(raw, "reviewWindow", name, errors),
|
|
189849
|
+
reviewBase: compileBooleanFlag(raw, "reviewBase", name, errors)
|
|
189784
189850
|
});
|
|
189785
189851
|
};
|
|
189786
189852
|
/**
|
|
@@ -189830,7 +189896,7 @@ const compileWorkflowConfig = (raw, configDir) => {
|
|
|
189830
189896
|
* default.yaml's content values start with `./`/`../`, so
|
|
189831
189897
|
* `compileWorkflowConfig` never resolves a file reference against it.
|
|
189832
189898
|
*/
|
|
189833
|
-
const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n message: |\n No active gtd cycle.\n\n To start one: write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough — then run `gtd step human`.\n on:\n \"* **\": grilling\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\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 \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default.\n\n To answer a question, edit its entry in place (replace\n `Suggested default: ...` with `Answer: ...`, or add further detail),\n then run `gtd step human`.\n\n To accept all suggested defaults as-is and move on to implementation,\n run `gtd step human` with no edits.\n on:\n \"C\": building\n \"* **\": grilling\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\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\n all 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 \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\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 Investigate and fix it yourself (editing code and/or\n `<%= it.vars.feedbackFile %>`), then run `gtd step human` to try the\n check again.\n on:\n \"* **\": checking\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — tick a pointer's `- [ ]` box to `- [x]` to approve that item;\n add notes, untick, or edit code for feedback.\n\n To approve: tick every box, then run `gtd step human`.\n\n To approve the whole cycle without ticking every box individually:\n delete `<%= it.vars.reviewFile %>` and run `gtd step human`.\n\n To request changes: edit `<%= it.vars.reviewFile %>`\n (ticking/unticking boxes, adding notes) and/or the code, then run\n `gtd step human` — any edit to `<%= it.vars.reviewFile %>` sends the\n cycle to a deterministic check that either closes the cycle out (every\n box ticked) or extracts the still-unticked items into a fresh\n `<%= it.vars.todoFile %>` and routes back through grilling with that\n feedback. Code-only edits (leaving `<%= it.vars.reviewFile %>`\n untouched) go straight back to grilling.\n on:\n \"D .gtd/REVIEW.md\": idle\n \"M .gtd/REVIEW.md\": review-deciding\n \"* **\": grilling\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
|
|
189899
|
+
const compiled = compileWorkflowConfig((0, import_dist$1.parse)("# The bundled default gtd workflow (v3 \"pattern machine\" — see\n# docs/design/pattern-machine-plan.md, and\n# docs/design/steering-file-loops.md for the two validation loops below).\n# Compiled through the exact same `compileWorkflowConfig` a user's `.gtdrc`\n# `workflow:` key goes through — no privileged code path\n# (src/workflows/default.ts does the compiling).\n#\n# Every content string below is INLINE (no `./`-relative file references):\n# the bundled default ships inside a single-file build\n# (`dist/gtd.bundle.mjs`), so its content must already be resolved at BUILD\n# time rather than read from disk at RUNTIME — the same reason\n# `src/prompts/*.md` are imported through tsdown's `.md`-as-text loader\n# instead of read via `fs` at runtime. `default.yaml` itself is imported the\n# same way (tsdown/vitest both treat `.yaml` as text — see\n# tsdown.config.ts / tests/vitest.rawMd.ts), so this file never needs a\n# sibling prompts/ directory to exist on disk once installed.\n#\n# States, in cycle order — 12 states, no squash:\n# idle (initial, human, message)\n# -> grilling (agent, prompt): reads .gtd/TODO.md's sketch (or a prior\n# lap's feedback), explores the codebase, and develops it into a concrete\n# implementation plan, stating any open question it can't settle itself\n# under a `## Open Questions` heading (see \"Loop 1\" below) rather than\n# guessing silently.\n# grilling -> todo-validating (check, script): mechanics-only parser port\n# of the open-questions format (see src/OpenQuestions.ts's module doc for\n# the executable spec this is a bash rendition of). Malformed ->\n# .gtd/FORMAT.md -> back to grilling; valid -> grilling-answer.\n# todo-validating -> grilling-answer (human, message): tick off suggested\n# defaults by replacing them with `Answer:` lines; a clean step accepts\n# every remaining default -> building; any edit loops back to grilling.\n# grilling-answer -> building (agent, prompt): implements the plan in\n# .gtd/TODO.md directly (no task decomposition, no per-task queue) and\n# deletes .gtd/TODO.md once the work is complete and verified.\n# building -> checking (check, script): runs the test command inline;\n# A/M .gtd/FEEDBACK.md -> fixing (capped, redirects to escalate); D\n# .gtd/FEEDBACK.md (a green run that just cleaned up prior feedback) or C\n# (green, nothing to clean) -> reviewing.\n# fixing (agent, prompt) <-> checking\n# escalate (human, message): manual intervention, then back to checking\n# checking (green) -> reviewing (agent, prompt): writes .gtd/REVIEW.md in\n# the checkbox review format (see \"Loop 2\" below), grouping the cycle's\n# full diff into reviewable chunks.\n# reviewing -> review-validating (check, script): mechanics-only parser\n# port of the review format (see src/ReviewDoc.ts's module doc for the\n# executable spec). Malformed -> .gtd/FORMAT.md -> back to reviewing;\n# valid -> await-review.\n# review-validating -> await-review (human, message): tick a pointer's `- [\n# ]` to `- [x]` to approve that item; deleting .gtd/REVIEW.md outright is\n# the power-user approve-everything shortcut; anything else pending\n# (code edits with REVIEW.md untouched) is feedback straight to grilling.\n# await-review -> review-deciding (check, script), entered by any `M\n# .gtd/REVIEW.md` step: if no unticked `- [ ]` pointer remains, the cycle\n# is approved (rm .gtd/REVIEW.md -> idle); otherwise the still-unticked\n# pointers (with their notes) are extracted into a fresh .gtd/TODO.md and\n# .gtd/REVIEW.md is removed (-> grilling, feedback lap).\n#\n# The cycle ends at human approval, resting back at idle — there is no\n# squash state. The cycle's commits stay in history; whether/how to squash\n# them (amend, interactive rebase, a PR's squash-merge) is entirely the\n# human's business once they're done reviewing — gtd makes no assumption.\n# That squash-flavored finale (a `squashing` prompt state + a `done` commit\n# state) still exists as an ENGINE capability (see STATES.md §8) and is kept\n# as part of the fuller machine at docs/examples/advanced-workflow.md.\n#\n# A `gtd(<actor>): idle` commit — entering the workflow's own initial state —\n# is therefore a PROCESS BOUNDARY (see `computeProcessRun` in src/Edge.ts):\n# the next cycle's `retry` counts, `startCommit`, and diffs never reach back\n# across an approved cycle's own idle-entering commit.\n#\n# HYGIENE INVARIANT (asserted in e2e — see default-workflow.feature): an\n# approved cycle leaves `.gtd/` empty. FEEDBACK.md is cleaned up by a green\n# `checking` run; FORMAT.md by a valid validator run (either loop); REVIEW.md\n# by the review-deciding approval branch; TODO.md by `building`.\n#\n# Loop 1 — TODO.md open questions (src/OpenQuestions.ts is this format's\n# EXECUTABLE SPEC — its unit tests are the format's spec tests; the\n# `todo-validating`/`review-validating` bash scripts below are a pragmatic\n# grep/awk PORT of the exact same rules, not a full markdown parser. Keep\n# both in sync by hand — there is no shared code path between the two\n# implementations, by design: the engine stays git/filesystem/Effect-free,\n# and the LSP's parsers stay dependency-free).\n#\n# Loop 2 — REVIEW.md checkboxes: same dual-implementation contract, this time\n# against src/ReviewDoc.ts.\n#\n# `grilling` and `reviewing` both declare `model: smart` — an opaque harness\n# hint (see the comment at `grilling` below) for their heavier one-shot\n# planning/reviewing turns. Every other state is unset (harness default).\n#\n# `vars.testCommand` is this workflow's own declared default for `checking`'s\n# script (see it.vars in PatternTemplates.ts) — override it per-repo with a\n# top-level `.gtdrc` `vars: { testCommand: ... }` key, or per-invocation with\n# a `GTD_VAR_testCommand` environment variable (highest precedence).\n#\n# `vars.todoFile`/`reviewFile`/`feedbackFile` are this workflow's ONE source\n# of truth for the three steering-file paths every prompt/script below reads\n# or writes — each appears in templates as `<%~ it.vars.todoFile %>` etc\n# (see docs/design/state-file-association.md §2). Every state that rests on\n# one of these files also declares a `file:`/`mode:` pair (see STATES.md's\n# property table) so `gtd status`/`gtd next --json` and the LSP (`gtd lsp`)\n# know which file a human/editor should look at, and in what format\n# (`qa` for TODO.md's open-questions format, `review` for REVIEW.md's\n# checkbox format) — `fixing`/`escalate` declare `file:` alone (FEEDBACK.md\n# is plain text, no LSP format). KNOWN LIMITATION: `on` pattern keys below are\n# NOT Eta templates — they keep the LITERAL `.gtd/...` paths matching these\n# vars' DEFAULT values. Repointing `todoFile`/`reviewFile`/`feedbackFile` via\n# `.gtdrc`'s top-level `vars:` or a `GTD_VAR_` override changes what `file:`\n# renders to (and what a template reads/writes) WITHOUT changing what the\n# `on` patterns match against — desyncing the machine. See\n# docs/configuration.md's `file:`/`mode:` section for the full caveat.\n#\n# A fuller machine — two-phase Q&A planning, an architecture phase, task\n# decomposition, a deterministic per-task builder loop, agent-prepared\n# review, and the squash finale (`squashing` + `done`) — is preserved as a\n# copy-paste `.gtdrc` example at docs/examples/advanced-workflow.md.\n\nvars:\n testCommand: npm test\n todoFile: .gtd/TODO.md\n reviewFile: .gtd/REVIEW.md\n feedbackFile: .gtd/FEEDBACK.md\n\nstates:\n idle:\n actor: human\n initial: true\n # Every human-gate `message:` below closes with the SAME rendered block —\n # a \"What each change does next\" list built from `it.edges`, the resting\n # state's own `on` edges (see PatternTemplates.TemplateContext.edges). Each\n # edge's human-readable route lives in its `describe:` (the single source\n # of truth), so the list can never drift from the routing it documents.\n # The block skips edges without a `describe`, and each `<%~ ... \"\\n\" %>`\n # keeps the bullet's own newline (Eta's autoTrim strips the template's own\n # line breaks around `<% %>` tags, not text inside an interpolation).\n message: |\n No active gtd cycle.\n\n To start one, write `.gtd/TODO.md` with a short sketch of what you want\n built — a few sentences is enough.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: grilling\n describe: >-\n Save your sketch to `.gtd/TODO.md` (any working-tree change) to start\n the cycle — the agent reads it, explores the codebase, and develops it\n into a concrete implementation plan (**grilling**).\n\n grilling:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n # `model` is an opaque harness hint — gtd never interprets this string, it\n # only passes it through to `gtd next --json`/`gtd status --json` so the\n # driving loop can map it onto whatever models its agent harness\n # provides. \"smart\" here just names the harness-chosen tier for the\n # heavier planning turn; states without a `model` use the harness's\n # default.\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.todoFile %>`, which this prompt tells you to develop. Never\n touch `.gtd/FORMAT.md` yourself if it exists — a validator writes it,\n and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.todoFile %>` — fix those findings first, then\n continue below.\n\n `<%= it.vars.todoFile %>` holds a short sketch of what to build (or\n feedback from a prior review lap). Read it, explore the codebase, and\n develop it into a concrete implementation plan.\n\n For every question you cannot settle yourself, add it under a\n `## Open Questions` heading, one `### <question>` sub-heading each,\n whose body's FIRST non-blank line is exactly\n `Suggested default: <your answer>` — the human can accept it as-is or\n override it. Omit the section entirely once there are no open\n questions. If `<%= it.vars.todoFile %>` already carries\n `## Open Questions` entries from a previous lap, fold any\n `Answer: <text>` line into the plan; every remaining entry's body must\n still start with either `Suggested default: ...` or `Answer: ...` —\n never anything else.\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 \"* **\": todo-validating\n\n todo-validating:\n actor: check\n file: <%= it.vars.todoFile %>\n mode: qa\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/OpenQuestions.ts's parser rules (the format's executable spec) —\n # free-form prose, plus an OPTIONAL \"## Open Questions\" section; every\n # \"### \" sub-heading directly under it is one question whose body's\n # first non-blank line must start with \"Suggested default: \" or\n # \"Answer: \". A violation is one finding line (file + line + rule) in\n # .gtd/FORMAT.md; a clean parse removes any stale .gtd/FORMAT.md\n # instead. What FORMAT.md's presence/absence MEANS (another drafting\n # round vs. proceeding to the human) is decided by this state's own\n # `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN { in_section = 0; have_question = 0; question = \"\"; qline = 0; body_seen = 0 }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n level = 0\n if (match(trimmed, /^#+/)) level = RLENGTH\n\n if (in_section && level >= 1 && level <= 2) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n in_section = 0\n have_question = 0\n }\n\n if (trimmed == \"## Open Questions\") { in_section = 1; next }\n\n if (in_section && level == 3) {\n if (have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n question = trimmed\n sub(/^### */, \"\", question)\n qline = NR\n have_question = 1\n body_seen = 0\n next\n }\n\n if (in_section && have_question && !body_seen && trimmed != \"\") {\n if (trimmed ~ /^Suggested default: *.+/ || trimmed ~ /^Answer: *.+/) {\n body_seen = 1\n } else {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n body_seen = 1\n }\n }\n }\n END {\n if (in_section && have_question && !body_seen) {\n printf \"<%~ it.vars.todoFile %>:%d: open question \\\"%s\\\" is missing a \\\"Suggested default: ...\\\" or \\\"Answer: ...\\\" line\\n\", qline, question\n }\n }\n ' <%~ it.vars.todoFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": grilling\n \"M .gtd/FORMAT.md\": grilling\n \"D .gtd/FORMAT.md\": grilling-answer\n \"C\": grilling-answer\n\n grilling-answer:\n actor: human\n file: <%= it.vars.todoFile %>\n mode: qa\n message: |\n `<%= it.vars.todoFile %>` holds the plan under development, with any\n open questions under `## Open Questions` — each carrying a suggested\n default. Answer a question by editing its entry in place: replace its\n `Suggested default: ...` line with `Answer: ...`, or add further detail.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"C\":\n to: building\n describe: >-\n Change nothing to accept every suggested default as-is and move on to\n implementation (**building**).\n \"* **\":\n to: grilling\n describe: >-\n Edit the plan — answer a question, add detail, or raise a new one — to\n send it back for another round of grilling (**grilling**).\n\n building:\n actor: agent\n file: <%= it.vars.todoFile %>\n mode: qa\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create or edit anything under `.gtd/` except deleting\n `<%= it.vars.todoFile %>`, which this prompt tells you to delete once\n its work is complete.\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\n all 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 \"* **\": checking\n\n checking:\n actor: check\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: run the test suite and record a\n # red run's output as .gtd/FEEDBACK.md. What that output MEANS (another\n # fix round vs escalation) is decided by the workflow's own `on`/`retry`\n # rules at capture time — never here.\n #\n # `it.vars.testCommand` defaults to \"npm test\" (this workflow's own\n # `vars:` above) — override it with a top-level `.gtdrc`\n # `vars: { testCommand: ... }` key, or a `GTD_VAR_testCommand`\n # environment variable (highest precedence).\n #\n # A green run also removes any `<%= it.vars.feedbackFile %>` left over\n # from an earlier red run — there is no squash to discard it for us\n # anymore, so the script cleans it up itself before moving on to\n # review.\n set +e\n mkdir -p .gtd\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output <%~ it.vars.feedbackFile %>\n else\n rm -f .gtd/.check-output\n printf 'npm test failed with exit code %s and produced no output.' \"$code\" > <%~ it.vars.feedbackFile %>\n fi\n else\n rm -f .gtd/.check-output\n rm -f <%~ it.vars.feedbackFile %>\n fi\n on:\n \"A .gtd/FEEDBACK.md\": fixing\n \"M .gtd/FEEDBACK.md\": fixing\n \"D .gtd/FEEDBACK.md\": reviewing\n \"C\": reviewing\n\n fixing:\n actor: agent\n file: <%= it.vars.feedbackFile %>\n retry:\n max: 3\n otherwise: escalate\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.feedbackFile %>`, which this prompt tells you to address.\n\n Read `<%= it.vars.feedbackFile %>` (the failing test output) and fix\n the code so the suite passes. Keep the change focused — do not\n refactor unrelated code. If the feedback is wrong, empty or delete\n `<%= it.vars.feedbackFile %>` instead of \"fixing\" a non-issue; the\n machine picks the dispute up either way.\n\n Leave everything uncommitted and finish your turn — do not commit.\n on:\n \"* **\": checking\n\n escalate:\n actor: human\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.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: checking\n describe: >-\n Fix the failure yourself — edit the code and/or `.gtd/FEEDBACK.md` —\n to re-run the check (**checking**).\n\n reviewing:\n actor: agent\n file: <%= it.vars.reviewFile %>\n mode: review\n model: smart\n prompt: |\n You are an autonomous coding agent. `.gtd/` holds this workflow's own\n state — never create, edit, or delete anything under `.gtd/` except\n `<%= it.vars.reviewFile %>`, which this prompt tells you to write.\n Never touch `.gtd/FORMAT.md` yourself if it exists — a validator writes\n it, and only the validator removes it once you've fixed the underlying\n problem.\n\n If `.gtd/FORMAT.md` exists, it holds format findings from your last\n draft of `<%= it.vars.reviewFile %>` — fix those findings first, then\n continue below.\n\n Write `<%= it.vars.reviewFile %>` to help a human review the cycle's\n full diff (inlined below) in this EXACT format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.startCommit %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation\n of what changed and why, then one\n `- [ ] ./path/to/file.ts#42 — note` file pointer per hunk (checkboxes\n are for the human to tick, not you; every path is `./`-relative, with\n an optional `#line` and an optional ` — note`).\n\n Leave `<%= it.vars.reviewFile %>` uncommitted and finish your turn.\n <% if (it.processDiff.trim()) { %>\n\n ## Full cycle diff\n\n ```diff\n <%~ it.processDiff %>\n ```\n <% } %>\n on:\n \"* **\": review-validating\n\n review-validating:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: a pragmatic grep/awk port of\n # src/ReviewDoc.ts's parser rules (the format's executable spec) — a\n # `# Review: <hash>` header as the first non-blank line, a\n # `<!-- base: <hash> -->` comment, and at least one `##` chunk with at\n # least one `- [ ]`/`- [x]` file pointer. A violation is one finding\n # line (file + line + rule) in .gtd/FORMAT.md; a clean parse removes any\n # stale .gtd/FORMAT.md instead. What FORMAT.md's presence/absence MEANS\n # (another drafting round vs. handing off to the human) is decided by\n # this state's own `on` rules at capture time — never here.\n set +e\n mkdir -p .gtd\n awk '\n BEGIN {\n first_nonblank_seen = 0\n header_ok = 0\n base_ok = 0\n chunk_open = 0\n chunk_title = \"\"\n chunk_line = 0\n chunk_files = 0\n chunk_count = 0\n }\n {\n trimmed = $0\n gsub(/^[ \\t]+|[ \\t]+$/, \"\", trimmed)\n\n if (!first_nonblank_seen && trimmed != \"\") {\n first_nonblank_seen = 1\n if (trimmed ~ /^# Review: [^ ]+$/) {\n header_ok = 1\n } else {\n printf \"<%~ it.vars.reviewFile %>:%d: first non-blank line must be \\\"# Review: <hash>\\\"\\n\", NR\n }\n }\n\n if (trimmed ~ /^<!-- *base: *[^ ]+ *-->$/) base_ok = 1\n\n if (trimmed ~ /^## [^ ]/) {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n chunk_open = 1\n chunk_title = trimmed\n sub(/^## /, \"\", chunk_title)\n chunk_line = NR\n chunk_files = 0\n chunk_count++\n next\n }\n\n if (chunk_open) {\n if (trimmed ~ /^-[ \\t]*\\[[ xX]\\][ \\t]*\\.\\/[^ \\t]+/) {\n chunk_files++\n } else if (trimmed ~ /^-[ \\t]*\\[/) {\n printf \"<%~ it.vars.reviewFile %>:%d: file pointer does not match \\\"- [ ] ./path#line — note\\\"\\n\", NR\n }\n }\n }\n END {\n if (chunk_open && chunk_files == 0) {\n printf \"<%~ it.vars.reviewFile %>:%d: chunk \\\"%s\\\" has no file pointers\\n\", chunk_line, chunk_title\n }\n if (!header_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing or malformed \\\"# Review: <hash>\\\" header as first line\"\n }\n if (!base_ok) {\n print \"<%~ it.vars.reviewFile %>:1: missing \\\"<!-- base: <hash> -->\\\" comment\"\n }\n if (chunk_count == 0) {\n print \"<%~ it.vars.reviewFile %>:1: no \\\"##\\\" chunks found\"\n }\n }\n ' <%~ it.vars.reviewFile %> > .gtd/.format-findings\n\n if [ -s .gtd/.format-findings ]; then\n mv .gtd/.format-findings .gtd/FORMAT.md\n else\n rm -f .gtd/.format-findings\n rm -f .gtd/FORMAT.md\n fi\n on:\n \"A .gtd/FORMAT.md\": reviewing\n \"M .gtd/FORMAT.md\": reviewing\n \"D .gtd/FORMAT.md\": await-review\n \"C\": await-review\n\n await-review:\n actor: human\n file: <%= it.vars.reviewFile %>\n mode: review\n # While the cycle rests here for human review, gtd opens a \"review checkout\n # window\": HEAD and the index are temporarily rewound to this cycle's start\n # (the process boundary — no `reviewBase` state is declared, so the base\n # defaults to it) with the working tree untouched, so the whole cycle diff\n # surfaces as ordinary uncommitted changes in the editor's git integration.\n # The window closes automatically on the next invocation, once the cycle\n # rests anywhere else (see src/ReviewWindow.ts, STATES.md §11).\n reviewWindow: true\n message: |\n `<%= it.vars.reviewFile %>` holds the review record for the completed\n cycle — one `- [ ]` checkbox per reviewable item, grouped into chunks.\n\n What each change does next (then run `gtd step human`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"D .gtd/REVIEW.md\":\n to: idle\n describe: >-\n Delete `.gtd/REVIEW.md` outright to approve the whole cycle at once\n and rest back at the start (**idle**).\n \"M .gtd/REVIEW.md\":\n to: review-deciding\n describe: >-\n Edit `.gtd/REVIEW.md` — tick a `- [ ]` box to `- [x]` to approve that\n item, or add notes — to hand it to a deterministic check\n (**review-deciding**) that closes the cycle out when every box is\n ticked, or extracts the still-unticked items into a fresh\n `.gtd/TODO.md` and loops back through **grilling** with that feedback.\n \"* **\":\n to: grilling\n describe: >-\n Change only code, leaving `.gtd/REVIEW.md` untouched, to send that as\n feedback straight back to **grilling**.\n\n review-deciding:\n actor: check\n file: <%= it.vars.reviewFile %>\n mode: review\n script: |\n #!/usr/bin/env bash\n # gtd check turn — the driver (`gtd run`) executes this verbatim, then\n # steps the check actor. Mechanics only: if .gtd/REVIEW.md still has any\n # unticked \"- [ ]\" pointer, extract those pointers (with their notes)\n # into a fresh .gtd/TODO.md; either way, remove .gtd/REVIEW.md. What\n # the resulting diff MEANS (feedback lap vs. full approval) is decided\n # by this state's own `on` rules at capture time — never here. The\n # A/M .gtd/TODO.md row is declared BEFORE the D .gtd/REVIEW.md row (see\n # this workflow's `on` map below) so a feedback lap — which always also\n # deletes .gtd/REVIEW.md — is captured as feedback, not mistaken for an\n # approval.\n set +e\n mkdir -p .gtd\n if grep -qE '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %> 2>/dev/null; then\n {\n echo \"Feedback from review — address these before continuing:\"\n echo\n grep -E '^[[:space:]]*-[[:space:]]*\\[[[:space:]]*\\]' <%~ it.vars.reviewFile %>\n } > <%~ it.vars.todoFile %>\n fi\n rm -f <%~ it.vars.reviewFile %>\n on:\n \"A .gtd/TODO.md\": grilling\n \"M .gtd/TODO.md\": grilling\n \"D .gtd/REVIEW.md\": idle\n \"C\": await-review\n"), ".");
|
|
189834
189900
|
const defaultWorkflowDefinition = compiled.definition;
|
|
189835
189901
|
/** The default workflow's own declared `vars:` defaults — `{ testCommand: "npm test" }`, overridable via a top-level `.gtdrc` `vars:` key or a `GTD_VAR_testCommand` environment variable (see `src/Edge.ts`'s `resolveVars`). */
|
|
189836
189902
|
const defaultWorkflowVars = compiled.vars;
|
|
@@ -189923,8 +189989,25 @@ const workflowJsonSchema = {
|
|
|
189923
189989
|
},
|
|
189924
189990
|
on: {
|
|
189925
189991
|
type: "object",
|
|
189926
|
-
description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise).",
|
|
189927
|
-
additionalProperties: {
|
|
189992
|
+
description: "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
|
|
189993
|
+
additionalProperties: { oneOf: [{
|
|
189994
|
+
type: "string",
|
|
189995
|
+
description: "The target state name."
|
|
189996
|
+
}, {
|
|
189997
|
+
type: "object",
|
|
189998
|
+
additionalProperties: false,
|
|
189999
|
+
required: ["to"],
|
|
190000
|
+
properties: {
|
|
190001
|
+
to: {
|
|
190002
|
+
type: "string",
|
|
190003
|
+
description: "The target state name."
|
|
190004
|
+
},
|
|
190005
|
+
describe: {
|
|
190006
|
+
type: "string",
|
|
190007
|
+
description: "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
|
|
190008
|
+
}
|
|
190009
|
+
}
|
|
190010
|
+
}] }
|
|
189928
190011
|
},
|
|
189929
190012
|
initial: {
|
|
189930
190013
|
type: "boolean",
|
|
@@ -189959,6 +190042,14 @@ const workflowJsonSchema = {
|
|
|
189959
190042
|
type: "string",
|
|
189960
190043
|
enum: ["qa", "review"],
|
|
189961
190044
|
description: "The steering file's format, dispatched on by the LSP. Requires a sibling `file:`. Forbidden on a commit state."
|
|
190045
|
+
},
|
|
190046
|
+
reviewWindow: {
|
|
190047
|
+
type: "boolean",
|
|
190048
|
+
description: "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
|
|
190049
|
+
},
|
|
190050
|
+
reviewBase: {
|
|
190051
|
+
type: "boolean",
|
|
190052
|
+
description: "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
|
|
189962
190053
|
}
|
|
189963
190054
|
}
|
|
189964
190055
|
}
|
|
@@ -190881,7 +190972,7 @@ const renderDiff = (files) => {
|
|
|
190881
190972
|
};
|
|
190882
190973
|
//#endregion
|
|
190883
190974
|
//#region src/Git.ts
|
|
190884
|
-
const EMPTY_TREE$
|
|
190975
|
+
const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
190885
190976
|
/**
|
|
190886
190977
|
* Parse `git diff --name-status` output into `{ path, status }` pairs.
|
|
190887
190978
|
* Status codes: A (added), D (deleted), M (modified), R (renamed), C (copied), etc.
|
|
@@ -191011,7 +191102,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
191011
191102
|
}),
|
|
191012
191103
|
commitDiff: (hash, exclude = []) => gen(function* () {
|
|
191013
191104
|
yield* exec("git", "rev-parse", "--verify", hash);
|
|
191014
|
-
const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$
|
|
191105
|
+
const parent = yield* exec("git", "rev-parse", "--verify", `${hash}~1`).pipe(map$4((s) => s.trim()), catchAll(() => succeed$3(EMPTY_TREE$2)));
|
|
191015
191106
|
const nameStatusOut = yield* exec("git", "diff", "--name-status", parent, hash).pipe(catchAll(() => succeed$3("")));
|
|
191016
191107
|
const allPaths = parseNameStatus(nameStatusOut);
|
|
191017
191108
|
const filtered = applyExcludes(allPaths, exclude);
|
|
@@ -191055,6 +191146,8 @@ const makeGitImpl = (executor, root) => {
|
|
|
191055
191146
|
return all;
|
|
191056
191147
|
}),
|
|
191057
191148
|
resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => /^[0-9a-f]{40}$/.test(hash) ? succeed$3(hash) : fail$2(/* @__PURE__ */ new Error(`Invalid ref: ${ref}`)))),
|
|
191149
|
+
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
191150
|
+
isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$1(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
|
|
191058
191151
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
191059
191152
|
commitHistory: (base) => {
|
|
191060
191153
|
const range = base !== void 0 ? `${base}..HEAD` : void 0;
|
|
@@ -191089,7 +191182,12 @@ const makeGitImpl = (executor, root) => {
|
|
|
191089
191182
|
discardPending: () => gen(function* () {
|
|
191090
191183
|
yield* exec("git", "add", "-A");
|
|
191091
191184
|
yield* exec("git", "reset", "--hard", "HEAD");
|
|
191092
|
-
}).pipe(asVoid)
|
|
191185
|
+
}).pipe(asVoid),
|
|
191186
|
+
updateRef: (ref, hash) => exec("git", "update-ref", ref, hash).pipe(asVoid),
|
|
191187
|
+
deleteRef: (ref) => exec("git", "update-ref", "-d", ref).pipe(asVoid),
|
|
191188
|
+
mixedResetTo: (ref) => exec("git", "reset", "--mixed", ref).pipe(asVoid),
|
|
191189
|
+
restoreStagedFrom: (source, paths) => paths.length === 0 ? _void : exec("git", "restore", "--staged", `--source=${source}`, "--", ...paths).pipe(catchAll(() => _void)),
|
|
191190
|
+
addIntentToAdd: () => exec("git", "add", "--intent-to-add", ".").pipe(asVoid)
|
|
191093
191191
|
};
|
|
191094
191192
|
};
|
|
191095
191193
|
const makeLiveEffect = gen(function* () {
|
|
@@ -191639,8 +191737,8 @@ const renderStateTemplate = (template, context) => eta.renderString(template, co
|
|
|
191639
191737
|
* one commit (or one squash). A caller that wants several transitions issues
|
|
191640
191738
|
* several invocations.
|
|
191641
191739
|
*/
|
|
191642
|
-
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
191643
|
-
const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
|
|
191740
|
+
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
191741
|
+
const subjectOf$1 = (message) => (message.split("\n")[0] ?? "").trim();
|
|
191644
191742
|
/** Collapse an arbitrary git status letter to the pattern grammar's closed `A|M|D` set (mirrors the plan's decision 5 — only those three are meaningful statuses). */
|
|
191645
191743
|
const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
|
|
191646
191744
|
/** Resolve HEAD's subject against the active workflow definition (the bundled default, or a compiled `.gtdrc` `workflow:` key). */
|
|
@@ -191684,20 +191782,20 @@ const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entri
|
|
|
191684
191782
|
const computeProcessRun = (git, def) => gen(function* () {
|
|
191685
191783
|
if (!(yield* git.hasCommits())) return {
|
|
191686
191784
|
startHash: "",
|
|
191687
|
-
startParentHash: EMPTY_TREE,
|
|
191785
|
+
startParentHash: EMPTY_TREE$1,
|
|
191688
191786
|
trace: []
|
|
191689
191787
|
};
|
|
191690
191788
|
const initialState = initialStateOf(def);
|
|
191691
191789
|
const history = yield* git.commitHistory();
|
|
191692
191790
|
let i = history.length - 1;
|
|
191693
191791
|
while (i >= 0) {
|
|
191694
|
-
const parsed = parseStateSubject(subjectOf(history[i].message));
|
|
191792
|
+
const parsed = parseStateSubject(subjectOf$1(history[i].message));
|
|
191695
191793
|
if (parsed === void 0 || parsed.state === initialState) break;
|
|
191696
191794
|
i--;
|
|
191697
191795
|
}
|
|
191698
191796
|
const startIdx = i + 1;
|
|
191699
|
-
const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf(h.message)).state);
|
|
191700
|
-
const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE;
|
|
191797
|
+
const trace = history.slice(startIdx).map((h) => parseStateSubject(subjectOf$1(h.message)).state);
|
|
191798
|
+
const startParentHash = i >= 0 ? history[i].hash : EMPTY_TREE$1;
|
|
191701
191799
|
return {
|
|
191702
191800
|
startHash: startIdx < history.length ? history[startIdx].hash : history[history.length - 1].hash,
|
|
191703
191801
|
startParentHash,
|
|
@@ -191729,8 +191827,17 @@ const resolveVars = (workflowVars, rcVars, env) => ({
|
|
|
191729
191827
|
...rcVars,
|
|
191730
191828
|
...envVarsFrom(env)
|
|
191731
191829
|
});
|
|
191732
|
-
/**
|
|
191733
|
-
const
|
|
191830
|
+
/** Map a state's raw `on` edges to the `{ pattern, target, describe? }` shape templates see as `it.edges`. `undefined` (a commit state, no `on`) yields an empty list. */
|
|
191831
|
+
const toTemplateEdges = (edges) => (edges ?? []).map(([pattern, target, describe]) => describe !== void 0 ? {
|
|
191832
|
+
pattern,
|
|
191833
|
+
target,
|
|
191834
|
+
describe
|
|
191835
|
+
} : {
|
|
191836
|
+
pattern,
|
|
191837
|
+
target
|
|
191838
|
+
});
|
|
191839
|
+
/** Build the `PatternTemplates.TemplateContext` for rendering `state`'s content at the resolved rest. `vars` is the already-merged three-layer map (see `resolveVars`); `edges` is the resting state's own `on` edges (see `toTemplateEdges`). */
|
|
191840
|
+
const buildTemplateContext = (git, read, state, actor, run, vars, edges) => gen(function* () {
|
|
191734
191841
|
const hasCommits = yield* git.hasCommits();
|
|
191735
191842
|
const currentCommit = hasCommits ? yield* git.resolveRef("HEAD") : "";
|
|
191736
191843
|
const previousCommit = hasCommits ? yield* git.resolveRef("HEAD~1").pipe(catchAll(() => succeed$3(run.startParentHash))) : "";
|
|
@@ -191745,7 +191852,8 @@ const buildTemplateContext = (git, read, state, actor, run, vars) => gen(functio
|
|
|
191745
191852
|
processDiff,
|
|
191746
191853
|
lastDiff,
|
|
191747
191854
|
read,
|
|
191748
|
-
vars
|
|
191855
|
+
vars,
|
|
191856
|
+
edges: toTemplateEdges(edges)
|
|
191749
191857
|
};
|
|
191750
191858
|
});
|
|
191751
191859
|
/**
|
|
@@ -191790,7 +191898,8 @@ const renderRest = (rest, context) => gen(function* () {
|
|
|
191790
191898
|
content,
|
|
191791
191899
|
...model !== void 0 ? { model } : {},
|
|
191792
191900
|
...file !== void 0 ? { file } : {},
|
|
191793
|
-
...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
|
|
191901
|
+
...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
|
|
191902
|
+
edges: toTemplateEdges(rest.stateDef.on)
|
|
191794
191903
|
};
|
|
191795
191904
|
});
|
|
191796
191905
|
/**
|
|
@@ -191821,7 +191930,7 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
|
|
|
191821
191930
|
yield* git.discardPending();
|
|
191822
191931
|
return {
|
|
191823
191932
|
kind: "squash",
|
|
191824
|
-
subject: subjectOf(message)
|
|
191933
|
+
subject: subjectOf$1(message)
|
|
191825
191934
|
};
|
|
191826
191935
|
}
|
|
191827
191936
|
case "noop": return {
|
|
@@ -191831,6 +191940,120 @@ const executeDecision = (git, run, decision, context) => gen(function* () {
|
|
|
191831
191940
|
}
|
|
191832
191941
|
});
|
|
191833
191942
|
//#endregion
|
|
191943
|
+
//#region src/ReviewWindow.ts
|
|
191944
|
+
/**
|
|
191945
|
+
* The review checkout window (v3 re-introduction — see
|
|
191946
|
+
* `docs/design/pattern-machine-plan.md`'s follow-up note and STATES.md §11).
|
|
191947
|
+
* While a process RESTS at a state declaring `reviewWindow: true` (the
|
|
191948
|
+
* bundled default's `await-review`), HEAD and the index are temporarily
|
|
191949
|
+
* rewound to the review base with the working tree untouched, so the entire
|
|
191950
|
+
* `base..HEAD` diff surfaces as ordinary uncommitted changes in any editor's
|
|
191951
|
+
* standard git integration (SCM panel, gutters, per-file diffs, discard-hunk).
|
|
191952
|
+
*
|
|
191953
|
+
* v2 hard-wired this to a single `awaiting-review` gate; v3 drives it purely
|
|
191954
|
+
* from the DECLARATIVE `reviewWindow`/`reviewBase` state properties (see
|
|
191955
|
+
* `PatternMachine.StateDef`) — the pure engine never observes an open window,
|
|
191956
|
+
* exactly as before.
|
|
191957
|
+
*
|
|
191958
|
+
* Lifecycle — driven from the program edge (`src/program.ts`), bracketing
|
|
191959
|
+
* every state subcommand:
|
|
191960
|
+
*
|
|
191961
|
+
* - `closeReviewWindow` runs BEFORE anything reads or mutates state, keyed
|
|
191962
|
+
* solely on `refs/gtd/review-head` existing: `git reset --mixed
|
|
191963
|
+
* refs/gtd/review-head` restores HEAD/index exactly, leaving only the
|
|
191964
|
+
* reviewer's own edits dirty (captured by the resting state's own `on`
|
|
191965
|
+
* patterns like any other pending change). The pure machine therefore never
|
|
191966
|
+
* sees the window.
|
|
191967
|
+
* - `openReviewWindow` runs AFTER the subcommand finishes and self-guards on
|
|
191968
|
+
* the resolved rest declaring `reviewWindow: true`: it saves HEAD to
|
|
191969
|
+
* `refs/gtd/review-head` (the base to `refs/gtd/review-base`), then `git
|
|
191970
|
+
* reset --mixed <base>`. It re-arms after read-only commands (`gtd next` /
|
|
191971
|
+
* `gtd status`) and refused invocations too, so the editor's diff view stays
|
|
191972
|
+
* consistent no matter which command the loop last ran.
|
|
191973
|
+
*
|
|
191974
|
+
* Every open/close step is idempotent under re-entry, so a crash at any point
|
|
191975
|
+
* is recovered by the next invocation's close (the saved ref also keeps the
|
|
191976
|
+
* real head GC-reachable for the window's whole lifetime).
|
|
191977
|
+
*/
|
|
191978
|
+
const REVIEW_HEAD_REF = "refs/gtd/review-head";
|
|
191979
|
+
const REVIEW_BASE_REF = "refs/gtd/review-base";
|
|
191980
|
+
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
191981
|
+
const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
|
|
191982
|
+
/**
|
|
191983
|
+
* The diff base while the window is open: the hash of the most-recent
|
|
191984
|
+
* in-process turn commit that ENTERED a `reviewBase` state, or `undefined`
|
|
191985
|
+
* when the workflow declares no such state (the caller then falls back to the
|
|
191986
|
+
* process start). Walks the current process run's commits (oldest→newest, via
|
|
191987
|
+
* `commitHistory(startParentHash)`), so it never reaches across the process
|
|
191988
|
+
* boundary into a previous cycle.
|
|
191989
|
+
*/
|
|
191990
|
+
const reviewBaseHash = (git, def, run) => gen(function* () {
|
|
191991
|
+
const history = yield* git.commitHistory(run.startParentHash);
|
|
191992
|
+
let base;
|
|
191993
|
+
for (const commit of history) {
|
|
191994
|
+
const parsed = parseStateSubject(subjectOf(commit.message));
|
|
191995
|
+
if (parsed !== void 0 && isReviewBaseState(def, parsed.state)) base = commit.hash;
|
|
191996
|
+
}
|
|
191997
|
+
return base;
|
|
191998
|
+
});
|
|
191999
|
+
/**
|
|
192000
|
+
* Restore the real head if a review checkout window is open; no-op otherwise.
|
|
192001
|
+
* Keyed on `refs/gtd/review-head` existence, NOT on any config or machine
|
|
192002
|
+
* state, so it always runs first and the machine never sees the window.
|
|
192003
|
+
*
|
|
192004
|
+
* Fails loudly — refs left in place — when HEAD is no longer on the reviewed
|
|
192005
|
+
* branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
|
|
192006
|
+
* mixed reset there would rewrite the wrong branch's tip. Manual commits on
|
|
192007
|
+
* top of the base pass the guard: the reset keeps their content in the working
|
|
192008
|
+
* tree, where the next turn's capture picks it up as review feedback.
|
|
192009
|
+
*/
|
|
192010
|
+
const closeReviewWindow = gen(function* () {
|
|
192011
|
+
const git = yield* GitService;
|
|
192012
|
+
const savedHead = yield* git.readRefOption(REVIEW_HEAD_REF);
|
|
192013
|
+
if (isNone(savedHead)) return { closed: false };
|
|
192014
|
+
const base = yield* git.readRefOption(REVIEW_BASE_REF);
|
|
192015
|
+
if (isSome(base) && base.value !== EMPTY_TREE) {
|
|
192016
|
+
if (!(yield* git.isAncestor(base.value, "HEAD"))) return yield* fail$2(/* @__PURE__ */ new Error("a gtd review checkout window is open but HEAD has moved off the reviewed branch — return to it, or restore manually with `git reset --mixed refs/gtd/review-head && git update-ref -d refs/gtd/review-head && git update-ref -d refs/gtd/review-base`"));
|
|
192017
|
+
}
|
|
192018
|
+
yield* git.mixedResetTo(REVIEW_HEAD_REF);
|
|
192019
|
+
yield* git.deleteRef(REVIEW_HEAD_REF);
|
|
192020
|
+
yield* git.deleteRef(REVIEW_BASE_REF);
|
|
192021
|
+
return { closed: true };
|
|
192022
|
+
});
|
|
192023
|
+
/**
|
|
192024
|
+
* Open (or re-arm) the review checkout window. Self-guarded: a no-op unless
|
|
192025
|
+
* the resolved rest's state declares `reviewWindow: true` and a distinct base
|
|
192026
|
+
* commit exists, so the caller invokes it unconditionally after every state
|
|
192027
|
+
* subcommand.
|
|
192028
|
+
*
|
|
192029
|
+
* The base is the most-recent in-process `reviewBase` commit
|
|
192030
|
+
* (`reviewBaseHash`) or, absent any such state, the process start
|
|
192031
|
+
* (`startParentHash`). When that resolves to the empty tree (a process with no
|
|
192032
|
+
* prior commit) or to HEAD itself (an empty process), there is nothing to
|
|
192033
|
+
* surface and the window stays closed.
|
|
192034
|
+
*
|
|
192035
|
+
* Ordering is crash-safe: base ref → head ref → mixed reset → `.gtd/` index
|
|
192036
|
+
* pin → intent-to-add. A crash before the head-ref write leaves only a stale
|
|
192037
|
+
* base ref (overwritten on the next open); a crash after it leaves HEAD ==
|
|
192038
|
+
* review-head, which the next invocation's close restores as a no-op.
|
|
192039
|
+
*/
|
|
192040
|
+
const openReviewWindow = gen(function* () {
|
|
192041
|
+
const git = yield* GitService;
|
|
192042
|
+
const def = (yield* ConfigService).workflow;
|
|
192043
|
+
if (!(yield* git.hasCommits())) return { opened: false };
|
|
192044
|
+
if (!isReviewWindowState(def, resolveState(def, yield* git.lastCommitSubject()))) return { opened: false };
|
|
192045
|
+
const run = yield* computeProcessRun(git, def);
|
|
192046
|
+
const base = (yield* reviewBaseHash(git, def, run)) ?? run.startParentHash;
|
|
192047
|
+
const headHash = yield* git.resolveRef("HEAD");
|
|
192048
|
+
if (base === EMPTY_TREE || base === headHash) return { opened: false };
|
|
192049
|
+
yield* git.updateRef(REVIEW_BASE_REF, base);
|
|
192050
|
+
yield* git.updateRef(REVIEW_HEAD_REF, headHash);
|
|
192051
|
+
yield* git.mixedResetTo(base);
|
|
192052
|
+
yield* git.restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"]);
|
|
192053
|
+
yield* git.addIntentToAdd();
|
|
192054
|
+
return { opened: true };
|
|
192055
|
+
});
|
|
192056
|
+
//#endregion
|
|
191834
192057
|
//#region node_modules/prettier/doc.mjs
|
|
191835
192058
|
var doc_exports = /* @__PURE__ */ __exportAll({
|
|
191836
192059
|
builders: () => builders,
|
|
@@ -364466,7 +364689,8 @@ const buildFileModeMap = (def, vars, root) => {
|
|
|
364466
364689
|
read: (path) => {
|
|
364467
364690
|
throw new Error(`"file:" is not readable while building the path→mode map (path: ${path})`);
|
|
364468
364691
|
},
|
|
364469
|
-
vars
|
|
364692
|
+
vars,
|
|
364693
|
+
edges: []
|
|
364470
364694
|
});
|
|
364471
364695
|
} catch (e) {
|
|
364472
364696
|
warnings.push(`state "${name}": "file:" failed to render, skipped — ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -364555,7 +364779,7 @@ const resolveSteeringFile = (root) => gen(function* () {
|
|
|
364555
364779
|
const rest = yield* resolveRest();
|
|
364556
364780
|
const run = yield* computeProcessRun(git, rest.def);
|
|
364557
364781
|
const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
|
|
364558
|
-
const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars);
|
|
364782
|
+
const context = yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on);
|
|
364559
364783
|
const file = yield* renderFile(rest.stateDef, context);
|
|
364560
364784
|
return {
|
|
364561
364785
|
state: rest.state,
|
|
@@ -364787,7 +365011,7 @@ const resolveRestContext = (git) => gen(function* () {
|
|
|
364787
365011
|
return {
|
|
364788
365012
|
rest,
|
|
364789
365013
|
run,
|
|
364790
|
-
context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars)
|
|
365014
|
+
context: yield* buildTemplateContext(git, worktree.read, rest.state, rest.actor, run, vars, rest.stateDef.on)
|
|
364791
365015
|
};
|
|
364792
365016
|
});
|
|
364793
365017
|
/**
|
|
@@ -364820,7 +365044,8 @@ const stepAsActor = (invoker) => gen(function* () {
|
|
|
364820
365044
|
};
|
|
364821
365045
|
const executable = decision;
|
|
364822
365046
|
const vars = resolveVars(config.workflowVars, config.rcVars, envVars.all);
|
|
364823
|
-
const
|
|
365047
|
+
const renderedState = decision.kind === "squash" ? decision.state : rest.state;
|
|
365048
|
+
const outcome = yield* executeDecision(git, run, executable, yield* buildTemplateContext(git, worktree.read, renderedState, invoker, run, vars, rest.def.states[renderedState]?.on));
|
|
364824
365049
|
return {
|
|
364825
365050
|
state: rest.state,
|
|
364826
365051
|
subject: outcome.kind === "noop" ? null : outcome.subject
|
|
@@ -364854,7 +365079,8 @@ const runNextCommand = (json, write) => gen(function* () {
|
|
|
364854
365079
|
content: rendered.content,
|
|
364855
365080
|
...rendered.model !== void 0 ? { model: rendered.model } : {},
|
|
364856
365081
|
...rendered.file !== void 0 ? { file: rendered.file } : {},
|
|
364857
|
-
...rendered.mode !== void 0 ? { mode: rendered.mode } : {}
|
|
365082
|
+
...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
|
|
365083
|
+
...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
|
|
364858
365084
|
}) + "\n");
|
|
364859
365085
|
else write(rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n");
|
|
364860
365086
|
});
|
|
@@ -364898,13 +365124,15 @@ const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
|
|
|
364898
365124
|
});
|
|
364899
365125
|
/** `gtd status --json`'s emission — `{state, actor, changes, model?, file?, mode?}`. */
|
|
364900
365126
|
const writeStatusJson = (write, rest, statusChanges, model, file) => {
|
|
365127
|
+
const edges = toTemplateEdges(rest.stateDef.on);
|
|
364901
365128
|
write(JSON.stringify({
|
|
364902
365129
|
state: rest.state,
|
|
364903
365130
|
actor: rest.actor,
|
|
364904
365131
|
changes: statusChanges,
|
|
364905
365132
|
...model !== void 0 ? { model } : {},
|
|
364906
365133
|
...file !== void 0 ? { file } : {},
|
|
364907
|
-
...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {}
|
|
365134
|
+
...rest.stateDef.mode !== void 0 ? { mode: rest.stateDef.mode } : {},
|
|
365135
|
+
...edges.length > 0 ? { edges } : {}
|
|
364908
365136
|
}) + "\n");
|
|
364909
365137
|
};
|
|
364910
365138
|
/** `gtd status`'s plain-text emission — `State:`/`Awaits:`/`Model:`/`File:`/`Mode:`/`Pending:` lines. */
|
|
@@ -365034,8 +365262,12 @@ function makeProgram(opts = {}) {
|
|
|
365034
365262
|
const git = yield* GitService;
|
|
365035
365263
|
const fs = yield* FileSystem;
|
|
365036
365264
|
yield* assertRunningFromRepoRoot(git, fs);
|
|
365265
|
+
yield* closeReviewWindow;
|
|
365037
365266
|
yield* (yield* ConfigInit).ensure;
|
|
365038
|
-
yield* dispatchKnownSubcommand(sub, argv, json, write);
|
|
365267
|
+
const outcome = yield* either(dispatchKnownSubcommand(sub, argv, json, write));
|
|
365268
|
+
const rearm = yield* either(openReviewWindow);
|
|
365269
|
+
if (isLeft(outcome)) return yield* fail$2(outcome.left);
|
|
365270
|
+
if (isLeft(rearm)) return yield* fail$2(rearm.left);
|
|
365039
365271
|
}).pipe(json ? catchAll((error) => sync$1(() => write(JSON.stringify({
|
|
365040
365272
|
state: "error",
|
|
365041
365273
|
prompt: error.message
|
package/docs/cli.md
CHANGED
|
@@ -86,8 +86,9 @@ process in the same step that entered it — see
|
|
|
86
86
|
[STATES.md §5](../STATES.md#5-resolution)).
|
|
87
87
|
|
|
88
88
|
Plain mode prints the rendered content verbatim (with exactly one trailing
|
|
89
|
-
newline) — never the `model`/`file`/`mode`
|
|
90
|
-
|
|
89
|
+
newline) — never the `model`/`file`/`mode`/`edges` structured keys, which are
|
|
90
|
+
JSON-only. `--json` emits
|
|
91
|
+
`{state, actor, kind, content, model?, file?, mode?, edges?}`:
|
|
91
92
|
|
|
92
93
|
```json
|
|
93
94
|
{
|
|
@@ -113,6 +114,14 @@ emits `{state, actor, kind, content, model?, file?, mode?}`:
|
|
|
113
114
|
[Configuration](configuration.md#filemode--the-steering-file-association)).
|
|
114
115
|
Both present only when the state declares them; **omitted entirely** (never
|
|
115
116
|
`null`) otherwise.
|
|
117
|
+
- `edges` — the resting state's `on` edges as `[{ pattern, target, describe? }]`
|
|
118
|
+
(declaration order) — the same list the content template sees as `it.edges`
|
|
119
|
+
(see
|
|
120
|
+
[Configuration](configuration.md#on-values--a-target-or-a--to-describe--route-description)).
|
|
121
|
+
A driver relaying a human gate's message has the routing (and each edge's
|
|
122
|
+
human-readable `describe`) alongside the rendered text. **Omitted entirely**
|
|
123
|
+
when the state has no `on` (a commit state); a per-edge `describe` is likewise
|
|
124
|
+
omitted when that edge declares none.
|
|
116
125
|
|
|
117
126
|
## `gtd run`
|
|
118
127
|
|
|
@@ -159,10 +168,12 @@ after `Awaits:` when the resolved state declares a `model:` hint, and
|
|
|
159
168
|
declared — each independently, only when set.
|
|
160
169
|
|
|
161
170
|
`--json` emits
|
|
162
|
-
`{state, actor, changes: [{status, path, pattern}], model?, file?, mode?}`
|
|
163
|
-
`pattern` is `null` when no declared row matches that change; `model`/`file`/
|
|
171
|
+
`{state, actor, changes: [{status, path, pattern}], model?, file?, mode?, edges?}`
|
|
172
|
+
— `pattern` is `null` when no declared row matches that change; `model`/`file`/
|
|
164
173
|
`mode` are present only when the resolved state declares them (omitted entirely,
|
|
165
|
-
never `null`, otherwise)
|
|
174
|
+
never `null`, otherwise); `edges` is the resting state's `on` edges as
|
|
175
|
+
`[{ pattern, target, describe? }]` (same as `gtd next --json`), omitted when the
|
|
176
|
+
state has no `on`:
|
|
166
177
|
|
|
167
178
|
```json
|
|
168
179
|
{
|
package/docs/configuration.md
CHANGED
|
@@ -54,7 +54,8 @@ workflow:
|
|
|
54
54
|
message: <string>
|
|
55
55
|
commit: <string>
|
|
56
56
|
on: # a mapping, DECLARATION ORDER PRESERVED
|
|
57
|
-
"<pattern>": <targetState>
|
|
57
|
+
"<pattern>": <targetState> # short form
|
|
58
|
+
"<pattern>": { to: <targetState>, describe: <sentence> } # with a human-readable route description
|
|
58
59
|
initial: true # exactly one state across the whole workflow
|
|
59
60
|
retry:
|
|
60
61
|
max: <number>
|
|
@@ -90,6 +91,51 @@ A `vars:` key sibling to `states:` inside `workflow:` declares the workflow's
|
|
|
90
91
|
own defaults for `it.vars` — see ["Variables"](#variables) below for the full
|
|
91
92
|
three-layer picture.
|
|
92
93
|
|
|
94
|
+
### `on:` values — a target, or a `{ to, describe }` route description
|
|
95
|
+
|
|
96
|
+
An `on` row's value is normally the target state name. It may instead be an
|
|
97
|
+
object `{ to: <target>, describe: <sentence> }`, attaching a human-readable
|
|
98
|
+
`describe` — a plain sentence explaining what making that kind of change does
|
|
99
|
+
next. `describe` is **inert to the engine**: it plays no part in matching, is
|
|
100
|
+
never Eta-rendered (exactly like the pattern key itself — see the "Known
|
|
101
|
+
limitation" note below), and never affects a decision. It exists only to be
|
|
102
|
+
surfaced. A state's own edges are handed to its content template as `it.edges`
|
|
103
|
+
(an array of `{ pattern, target, describe? }`, in declaration order), so a human
|
|
104
|
+
gate's `message:` can render a "what each change does next" list from the same
|
|
105
|
+
routing the engine uses — one source of truth:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
workflow:
|
|
109
|
+
states:
|
|
110
|
+
await-review:
|
|
111
|
+
actor: human
|
|
112
|
+
message: |
|
|
113
|
+
Review the diff, then run `gtd step human`.
|
|
114
|
+
|
|
115
|
+
What each change does next:
|
|
116
|
+
<% it.edges.forEach(function (e) { if (e.describe) { %>
|
|
117
|
+
<%~ "- " + e.describe + "\n" %>
|
|
118
|
+
<% } }) %>
|
|
119
|
+
on:
|
|
120
|
+
"D .gtd/REVIEW.md":
|
|
121
|
+
to: idle
|
|
122
|
+
describe:
|
|
123
|
+
"Delete `.gtd/REVIEW.md` to approve the whole cycle and rest at
|
|
124
|
+
idle."
|
|
125
|
+
"* **":
|
|
126
|
+
to: grilling
|
|
127
|
+
describe:
|
|
128
|
+
"Change any source file to leave feedback and start another round."
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `<%~ "- " + e.describe + "\n" %>` idiom keeps each bullet's own newline: Eta
|
|
132
|
+
strips the template's line breaks around `<% %>` control tags (its `autoTrim`
|
|
133
|
+
default), not text inside an interpolation. Edges without a `describe` are
|
|
134
|
+
skipped by the `if (e.describe)` guard. `gtd next --json` and
|
|
135
|
+
`gtd status --json` also emit the `edges` array; both omit it when the state has
|
|
136
|
+
no `on` (a commit state), and omit a per-edge `describe` when that edge declares
|
|
137
|
+
none.
|
|
138
|
+
|
|
93
139
|
### `model:` — the opaque harness hint, template-rendered
|
|
94
140
|
|
|
95
141
|
A state may declare `model: <string>` — an OPAQUE label (e.g. `smart`, `fast`,
|
|
@@ -172,6 +218,49 @@ change to that path MEANS keeps matching the old literal path. The vars are a
|
|
|
172
218
|
DRY mechanism inside templates and the state↔file association, not a rename
|
|
173
219
|
switch. (Making pattern keys var-aware at compile time is possible future work.)
|
|
174
220
|
|
|
221
|
+
### `reviewWindow:`/`reviewBase:` — the review checkout window
|
|
222
|
+
|
|
223
|
+
A state may declare `reviewWindow: true`. While the machine RESTS there, gtd
|
|
224
|
+
opens a **review checkout window**: it rewinds HEAD and the index to the review
|
|
225
|
+
base (`git reset --mixed`) with the working tree untouched, so the whole
|
|
226
|
+
`base..HEAD` diff surfaces as ordinary uncommitted changes in the editor's git
|
|
227
|
+
integration (SCM panel, gutters, per-file diff, discard-hunk). The window closes
|
|
228
|
+
automatically on the next invocation, once the machine rests anywhere else — and
|
|
229
|
+
a reviewer's own edits, made while it was open, become the resting state's
|
|
230
|
+
ordinary pending changes, captured by its `on` patterns like any other diff.
|
|
231
|
+
Forbidden on a commit state (never at rest).
|
|
232
|
+
|
|
233
|
+
The diff base defaults to the current process's start. To narrow it, mark an
|
|
234
|
+
earlier state `reviewBase: true`: the most-recent in-process commit that entered
|
|
235
|
+
such a state becomes the base, so only work committed after that milestone
|
|
236
|
+
surfaces.
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
workflow:
|
|
240
|
+
states:
|
|
241
|
+
# …
|
|
242
|
+
building:
|
|
243
|
+
actor: agent
|
|
244
|
+
reviewBase: true # the diff shown at review starts here…
|
|
245
|
+
prompt: implement the plan
|
|
246
|
+
on:
|
|
247
|
+
"* **": review
|
|
248
|
+
review:
|
|
249
|
+
actor: human
|
|
250
|
+
reviewWindow: true # …and is surfaced while resting here
|
|
251
|
+
message: review the diff in your editor, then `gtd step human`
|
|
252
|
+
on:
|
|
253
|
+
"C": done
|
|
254
|
+
"* **": building
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The bundled default enables `reviewWindow: true` on `await-review` (no
|
|
258
|
+
`reviewBase` state, so the base is the whole cycle). The pure engine never
|
|
259
|
+
observes an open window — it is opened/closed entirely at the edge; the real
|
|
260
|
+
head is preserved under `refs/gtd/review-head` (the base under
|
|
261
|
+
`refs/gtd/review-base`) for the window's lifetime. See
|
|
262
|
+
[STATES.md §11](../STATES.md) for the full lifecycle.
|
|
263
|
+
|
|
175
264
|
### Template variables
|
|
176
265
|
|
|
177
266
|
Every `script`/`prompt`/`message`/`commit`/`model` template is rendered as an
|
package/docs/upgrading.md
CHANGED
|
@@ -49,8 +49,14 @@ upgrading.
|
|
|
49
49
|
tiering — a workflow author is free to build any of that shape back with
|
|
50
50
|
states of their own, but gtd no longer bakes it in.
|
|
51
51
|
- **The review checkout window.** v2 rewound HEAD/index to the review base while
|
|
52
|
-
a human review rested, so editors would show the diff directly.
|
|
53
|
-
|
|
52
|
+
a human review rested, so editors would show the diff directly. Dropped in the
|
|
53
|
+
v3.0 rewrite, then RE-INTRODUCED as the explicit state property it was always
|
|
54
|
+
meant to become: a state declaring `reviewWindow: true` opens the window while
|
|
55
|
+
the machine rests there (base = the process start, or a state marked
|
|
56
|
+
`reviewBase: true`). The bundled default enables it on `await-review`. Unlike
|
|
57
|
+
v2's hardwired gate, it is pure workflow DATA and the engine stays oblivious —
|
|
58
|
+
see [STATES.md §11](../STATES.md) and the `file:`/`mode:` neighbours in
|
|
59
|
+
[configuration.md](configuration.md).
|
|
54
60
|
- **`forceApprove`, content-inspection verdicts.** FEEDBACK.md emptiness,
|
|
55
61
|
checkbox-only REVIEW.md diffs, and doc-structure validation are gone as ENGINE
|
|
56
62
|
mechanisms — verdicts are now expressed purely by which file a turn writes or
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -47,9 +47,29 @@
|
|
|
47
47
|
},
|
|
48
48
|
"on": {
|
|
49
49
|
"type": "object",
|
|
50
|
-
"description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise).",
|
|
50
|
+
"description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe } object whose describe is a human-readable sentence templates can surface as it.edges (e.g. in a human gate's message).",
|
|
51
51
|
"additionalProperties": {
|
|
52
|
-
"
|
|
52
|
+
"oneOf": [
|
|
53
|
+
{
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "The target state name."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": false,
|
|
60
|
+
"required": ["to"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"to": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "The target state name."
|
|
65
|
+
},
|
|
66
|
+
"describe": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
53
73
|
}
|
|
54
74
|
},
|
|
55
75
|
"initial": {
|
|
@@ -85,6 +105,14 @@
|
|
|
85
105
|
"type": "string",
|
|
86
106
|
"enum": ["qa", "review"],
|
|
87
107
|
"description": "The steering file's format, dispatched on by the LSP. Requires a sibling `file:`. Forbidden on a commit state."
|
|
108
|
+
},
|
|
109
|
+
"reviewWindow": {
|
|
110
|
+
"type": "boolean",
|
|
111
|
+
"description": "When true, gtd opens a review checkout window while the machine rests here — HEAD/index are rewound to the review base so the whole base..HEAD diff surfaces as uncommitted changes in the editor. Forbidden on a commit state."
|
|
112
|
+
},
|
|
113
|
+
"reviewBase": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "Marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. Forbidden on a commit state."
|
|
88
116
|
}
|
|
89
117
|
}
|
|
90
118
|
}
|