@pmelab/gtd 24.0.0 → 24.1.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/.claude-plugin/plugin.json +1 -1
- package/README.md +31 -5
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43339 -55575
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +8 -6
- package/package.json +69 -8
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflows/feature/feature.test.ts +27 -33
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +58 -78
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +2 -10
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +85 -29
- package/src/workflows/review/review.ts +3 -1
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/shared/workflows.steps.ts +7 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// `MINIMAL_DRIVER` is pinned equal to https://github.com/pmelab/gtd/blob/main/docs/driver.md's "A complete minimal driver" fenced bash block by `Install.test.ts`, so the two can never drift.
|
|
2
|
+
import { GTD_VERSION } from "./Version.js"
|
|
3
|
+
|
|
4
|
+
export const MINIMAL_DRIVER = `#!/usr/bin/env sh
|
|
5
|
+
set -eu
|
|
6
|
+
|
|
7
|
+
# A turn ends when \`claude -p\` returns, so work it backgrounds dies unread
|
|
8
|
+
# and stalls the process. Keep every command in the foreground, with room to
|
|
9
|
+
# finish a long suite (Claude Code's defaults: 2 min, capped at 10).
|
|
10
|
+
export CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1
|
|
11
|
+
export BASH_DEFAULT_TIMEOUT_MS=1800000
|
|
12
|
+
export BASH_MAX_TIMEOUT_MS=7200000
|
|
13
|
+
|
|
14
|
+
beat=1
|
|
15
|
+
while :; do
|
|
16
|
+
# One value per invocation: \`gtd next --json=<path>\` is a pure read (peek
|
|
17
|
+
# or dispatch, indistinguishable, always safe to call again), so nothing is
|
|
18
|
+
# lost by reading \`kind\`/\`idle\` as two separate calls rather than one
|
|
19
|
+
# combined document.
|
|
20
|
+
kind="$(gtd next --json=kind)"
|
|
21
|
+
idle="$(gtd next --json=idle)"
|
|
22
|
+
|
|
23
|
+
# \`idle\` (true iff the initial state, clean tree) is the one shape that
|
|
24
|
+
# means the process is genuinely done — EXCEPT on the run's opening beat:
|
|
25
|
+
# land it anyway, so a workflow whose first step accepts a clean landing
|
|
26
|
+
# (acceptClean) still gets a chance to fire.
|
|
27
|
+
if [ "$beat" -gt 1 ] && [ "\${idle:-}" = true ]; then
|
|
28
|
+
gtd next
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
case "$kind" in
|
|
33
|
+
stalled)
|
|
34
|
+
gtd next --json=content >&2
|
|
35
|
+
exit 1 ;;
|
|
36
|
+
# you re-ran us resting here: you either edited something or accepted by
|
|
37
|
+
# editing nothing, so land the opening beat either way. Later beats are
|
|
38
|
+
# gates we just produced and you have not read yet — hand off. A judge
|
|
39
|
+
# gate's own \`--json=judge\` is non-empty here: pipe it through
|
|
40
|
+
# \`gtd judge run\` and land the verdict via \`gtd judge answer\`. POSIX sh has
|
|
41
|
+
# no pipefail, so each step is captured first and only piped on when it
|
|
42
|
+
# succeeded; if \`gtd judge run\` fails or \`gtd judge answer\` rejects the
|
|
43
|
+
# verdict, its stderr reaches you and we fall through to the human hand-off.
|
|
44
|
+
message)
|
|
45
|
+
if [ -n "$(gtd next --json=judge)" ] &&
|
|
46
|
+
verdict="$(gtd judge --json | gtd judge run)" &&
|
|
47
|
+
script="$(printf '%s\\n' "$verdict" | gtd judge answer --json=script)"; then
|
|
48
|
+
printf '%s\\n' "$script" | sh
|
|
49
|
+
beat=$((beat + 1))
|
|
50
|
+
continue
|
|
51
|
+
fi
|
|
52
|
+
[ "$beat" = 1 ] || {
|
|
53
|
+
gtd next
|
|
54
|
+
exit 0
|
|
55
|
+
} ;;
|
|
56
|
+
capture) ;; # the human already acted — just land it
|
|
57
|
+
script)
|
|
58
|
+
# A \`script\` rest's plain output is prose, not pipeable into \`sh\` — the
|
|
59
|
+
# raw script comes from \`--json=content\` instead.
|
|
60
|
+
content="$(gtd next --json=content)"
|
|
61
|
+
log="$(gtd next --json=log)"
|
|
62
|
+
sh -c "$content" >>"$log" 2>&1 || true ;;
|
|
63
|
+
prompt)
|
|
64
|
+
session_id="$(gtd next --json=session.id)"
|
|
65
|
+
resume="$(gtd next --json=session.resume)"
|
|
66
|
+
model="$(gtd next --json=model)"
|
|
67
|
+
system="$(gtd next --json=system)"
|
|
68
|
+
validate="$(gtd next --json=validate)"
|
|
69
|
+
log="$(gtd next --json=log)"
|
|
70
|
+
# \`model\` is a hint, not a model name — the bundled workflow ships
|
|
71
|
+
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
72
|
+
# with the variables \`gtd loop\` reads; unset, the hint is dropped and
|
|
73
|
+
# the agent runs its own default. Any other value is a real model name.
|
|
74
|
+
case "$model" in
|
|
75
|
+
smart) model="\${GTD_LOOP_MODEL_SMART:-}" ;;
|
|
76
|
+
base) model="\${GTD_LOOP_MODEL_BASE:-}" ;;
|
|
77
|
+
esac
|
|
78
|
+
# The prompt embeds a full diff, so it goes to the agent over stdin,
|
|
79
|
+
# never as an argv positional — argv is capped (~1 MB on macOS, and
|
|
80
|
+
# POSIX guarantees only 4 KB, ARG_MAX), and a diff crosses that far
|
|
81
|
+
# sooner than you'd expect. Piping plain \`gtd next\` (not
|
|
82
|
+
# --json=content) means the diff is rendered once, for this input,
|
|
83
|
+
# rather than twice.
|
|
84
|
+
agent_turn() { gtd next | claude -p "$1" "$session_id" \\
|
|
85
|
+
\${model:+--model "$model"} \\
|
|
86
|
+
\${system:+--system-prompt "$system"} \\
|
|
87
|
+
--dangerously-skip-permissions >>"$log" 2>&1; }
|
|
88
|
+
if [ "\${resume:-}" = true ]
|
|
89
|
+
then agent_turn --resume || agent_turn --session-id
|
|
90
|
+
else agent_turn --session-id || agent_turn --resume
|
|
91
|
+
fi
|
|
92
|
+
n=0
|
|
93
|
+
while [ -n "\${validate:-}" ] && ! fix="$(sh -c "$validate" 2>&1)"; do
|
|
94
|
+
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
|
|
95
|
+
# $fix IS the fix prompt, verbatim — piped for the same reason the
|
|
96
|
+
# agent's own turn above is. Whether \`claude --resume\` re-applies the
|
|
97
|
+
# original session's model/system prompt is a harness detail gtd
|
|
98
|
+
# cannot verify from outside, so this passes the identical
|
|
99
|
+
# $model/$system on both calls rather than assume it does —
|
|
100
|
+
# otherwise this fix turn might silently fall back to Claude Code's
|
|
101
|
+
# own defaults while the turn that produced the file ran under the
|
|
102
|
+
# workflow's own model and persona.
|
|
103
|
+
printf '%s' "$fix" | claude -p --resume "$session_id" \\
|
|
104
|
+
\${model:+--model "$model"} \\
|
|
105
|
+
\${system:+--system-prompt "$system"} \\
|
|
106
|
+
--dangerously-skip-permissions >>"$log" 2>&1
|
|
107
|
+
done ;;
|
|
108
|
+
esac
|
|
109
|
+
|
|
110
|
+
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
111
|
+
# \`gtd land\` itself never mutates — it only plans and prints — so
|
|
112
|
+
# \`settled\`/\`idle\`/\`script\` planned against the same untouched tree all
|
|
113
|
+
# agree, whatever order they're read in, as long as all three are read
|
|
114
|
+
# before the script runs and changes the tree out from under them.
|
|
115
|
+
settled="$(gtd land --json=settled)"
|
|
116
|
+
idle="$(gtd land --json=idle)"
|
|
117
|
+
gtd land --json=script | sh
|
|
118
|
+
if [ "\${settled:-}" = true ]; then
|
|
119
|
+
[ "\${idle:-}" = true ] && gtd next
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
beat=$((beat + 1))
|
|
123
|
+
done`
|
|
124
|
+
|
|
125
|
+
export const EDIT_COMMAND = `#!/usr/bin/env sh
|
|
126
|
+
set -eu
|
|
127
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
128
|
+
f="$(gtd next --json=file)"
|
|
129
|
+
f="\${f:-.gtd/TODO.md}"
|
|
130
|
+
mkdir -p "$(dirname "$f")"
|
|
131
|
+
exec "\${EDITOR:-vi}" "$f"`
|
|
132
|
+
|
|
133
|
+
export const DOOR_COMMAND = `#!/usr/bin/env sh
|
|
134
|
+
set -eu
|
|
135
|
+
GTD_BUILD=~/.local/bin/gtd-build
|
|
136
|
+
if [ -z "\${1:-}" ]; then
|
|
137
|
+
echo "usage: gtd-door <name> [args...]" >&2
|
|
138
|
+
exit 2
|
|
139
|
+
fi
|
|
140
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
141
|
+
script="$(gtd door "$@")"
|
|
142
|
+
sh -c "$script"
|
|
143
|
+
exec "$GTD_BUILD"`
|
|
144
|
+
|
|
145
|
+
const HEADER = (): string =>
|
|
146
|
+
`gtd ${GTD_VERSION} — driver protocol\n` +
|
|
147
|
+
`\n` +
|
|
148
|
+
`gtd decides and prints; the driver executes. gtd never runs git, never runs\n` +
|
|
149
|
+
`your agent, never runs a check script, and never runs a steering mode's own\n` +
|
|
150
|
+
`format:/validate: command either — that pair is text in the script gtd next\n` +
|
|
151
|
+
`--json's validate field (or gtd validate) prints, for the driver to run\n` +
|
|
152
|
+
`itself ahead of gtd land.\n`
|
|
153
|
+
|
|
154
|
+
const BEAT_PROTOCOL = `
|
|
155
|
+
## The beat protocol
|
|
156
|
+
|
|
157
|
+
Two reads and one write, repeated: ask -> act -> land.
|
|
158
|
+
|
|
159
|
+
gtd next --json=<path> which kind is next, and any other field, one pure
|
|
160
|
+
scalar read per value (kind, content, idle,
|
|
161
|
+
session.id, model, validate, log, ...)
|
|
162
|
+
gtd next what to run/show, in plain text — the SAME rest
|
|
163
|
+
the --json=<path> reads above are peeking at
|
|
164
|
+
gtd land --json=<path> how to record what happened, and whether anything
|
|
165
|
+
is still owed (script to run, settled to check)
|
|
166
|
+
|
|
167
|
+
Every beat that acts, lands. Every beat that rests, exits.
|
|
168
|
+
|
|
169
|
+
Every \`gtd next --json=<path>\`/\`gtd land --json=<path>\` read is a pure PEEK —
|
|
170
|
+
every field it reports (\`session.id\`/\`session.resume\` included) is DERIVED
|
|
171
|
+
from history, never stored, so looking is free: nothing distinguishes a peek
|
|
172
|
+
from a dispatch, and calling either one twice in a row is always safe. There
|
|
173
|
+
is no separate claiming form.
|
|
174
|
+
|
|
175
|
+
\`gtd next\` has one structured encoding, \`--json\`: bare \`--json\` prints the
|
|
176
|
+
whole document, \`--json=<path>\` (a dotted key path, e.g. \`kind\`, \`content\`,
|
|
177
|
+
\`session.id\`) prints just that one value — a scalar raw and unquoted, a
|
|
178
|
+
boolean as \`true\`/\`false\`, a list one JSON entry per line. An absent
|
|
179
|
+
optional field prints nothing and exits 0; an unknown path is a usage error
|
|
180
|
+
(exit 2). An AI driver reads \`kind\`/\`session.id\` and the rest straight off
|
|
181
|
+
\`--json=<path>\`, one call per value, without parsing anything — no JSON
|
|
182
|
+
library, no \`jq\`; this briefing's own reference driver reads every field this
|
|
183
|
+
way. Plain \`gtd next\` (no flag) is NOT a parsing surface: \`content\` is the
|
|
184
|
+
bare step in every encoding; plain adds the self-validation instruction at a
|
|
185
|
+
validatable \`prompt\` rest, and adds a status-summary header at every kind
|
|
186
|
+
EXCEPT \`prompt\` (those bytes are the agent's own input, so no header is
|
|
187
|
+
prefixed there). A driver that wants \`content\` programmatically reads it off
|
|
188
|
+
\`--json=content\`, never by scraping plain text.
|
|
189
|
+
|
|
190
|
+
The \`kind\` field (\`--json=kind\`) selects what to do:
|
|
191
|
+
|
|
192
|
+
- \`stalled\` — print \`--json=content\` (the diagnosis, already in hand) to
|
|
193
|
+
stderr and exit non-zero. Terminal: another dispatch would just repeat the
|
|
194
|
+
same fruitless turn.
|
|
195
|
+
- \`message\` — print plain \`gtd next\`'s output (the richer status header,
|
|
196
|
+
not bare \`content\`) and exit 0. This is a human gate. EXCEPT on the run's
|
|
197
|
+
opening beat: the human invoked you while resting there, so land it
|
|
198
|
+
instead of printing. Some gates route a \`"C"\` (clean-tree) pattern
|
|
199
|
+
onward, making "change nothing" a real decision that looks identical to a
|
|
200
|
+
gate nobody has read yet; landing an opening beat at a gate without one is
|
|
201
|
+
a benign no-op, and it prints on the next beat.
|
|
202
|
+
- \`capture\` — a human gate the human already acted on (the tree is dirty).
|
|
203
|
+
Land it immediately, no display needed.
|
|
204
|
+
- \`script\` — run \`--json=content\` with \`sh -c\`, appending its output to
|
|
205
|
+
\`--json=log\`, and ignore its exit code — the outcome lives in the tree,
|
|
206
|
+
not in the exit code.
|
|
207
|
+
- \`prompt\` — send plain \`gtd next\`'s own output to your agent CLI over
|
|
208
|
+
stdin (it's the bare content at this kind, so no separate \`--json=content\`
|
|
209
|
+
read is needed), using the accompanying \`session.id\`/\`session.resume\`/
|
|
210
|
+
\`model\`/\`validate\` fields, each its own \`--json=<path>\` read (see
|
|
211
|
+
below).
|
|
212
|
+
`
|
|
213
|
+
|
|
214
|
+
const JSON_FIELD_REFERENCE = `
|
|
215
|
+
## Field reference
|
|
216
|
+
|
|
217
|
+
\`gtd next\` and \`gtd land\` each carry one field set, read through \`--json\`
|
|
218
|
+
(bare for the whole document, \`--json=<path>\` for one value) — this driver's
|
|
219
|
+
own beats read fields one at a time via \`--json=<path>\` throughout (see the
|
|
220
|
+
beat protocol above). Each document carries \`schema\`, an integer bumped only
|
|
221
|
+
when a field is removed or renamed — never for an added one, and never tied to
|
|
222
|
+
gtd's package version. A driver that can meet a gtd other than the one it was
|
|
223
|
+
written against checks it and stops on a value it does not read. Every field below
|
|
224
|
+
is always present unless marked "when set" — omitted under bare \`--json\`
|
|
225
|
+
(never \`null\`); under \`--json=<path>\` an absent field prints nothing and
|
|
226
|
+
exits 0, so read it in a shell variable and guard on emptiness
|
|
227
|
+
(\`\${x:-}\`/\`\${x:+...}\`) rather than assuming it is always populated.
|
|
228
|
+
|
|
229
|
+
\`gtd next\` fields — Always: \`schema\`, \`kind\` (\`capture\`|\`message\`|\`script\`|\`prompt\`|
|
|
230
|
+
\`stalled\`), \`content\` (the bare step in every encoding — plain \`gtd next\`
|
|
231
|
+
adds the self-validation instruction at a validatable \`prompt\` rest, and a
|
|
232
|
+
status-summary header at every OTHER kind; \`content\` itself never carries
|
|
233
|
+
either), \`idle\` (\`true\` iff the resolved rest is the workflow's initial
|
|
234
|
+
state with a clean tree), \`initial\` (\`true\` iff no process is underway —
|
|
235
|
+
the default workflow's first step, first visit — whatever the tree holds), \`log\`, \`state\`, \`actor\`, \`changes\` (which
|
|
236
|
+
declared \`on\` pattern, if any, each pending change matches), \`next\` (the
|
|
237
|
+
first declared \`on\` edge that would fire right now — \`{action?, pattern,
|
|
238
|
+
target}\` — or \`null\` on no match). Only at \`kind: "prompt"\` (the DISPATCH
|
|
239
|
+
BLOCK — absent at every other kind, a \`stalled\` beat included, by
|
|
240
|
+
construction): \`session.id\`/\`session.resume\` — both DERIVED from history,
|
|
241
|
+
never stored, so a plain peek is exactly as safe to call as a dispatch would
|
|
242
|
+
be — and, when the state declares a validatable steering file, \`validate\`
|
|
243
|
+
(the script that formats then validates it), and, when the turn's scope declares
|
|
244
|
+
skills, \`skills\` (an array of the skill names, already trimmed and split —
|
|
245
|
+
\`--json=skills.0\` reads the first), and always \`access\`
|
|
246
|
+
(\`{read, write}\`, each an array of globs or \`null\` for unrestricted). When set: \`model\`, \`memory\`,
|
|
247
|
+
\`label\`, \`file\`, \`mode\`, \`edges\`, \`judge\` (the pending judgment's rendered
|
|
248
|
+
document). When a cost has been recorded (a prior \`gtd land --cost=<n>\`):
|
|
249
|
+
\`cost\`, \`costByModel\`.
|
|
250
|
+
|
|
251
|
+
\`gtd land\` fields, in fixed order: \`schema\`, \`script\` (the POSIX sh to run), \`settled\`
|
|
252
|
+
(true for a no-op at a \`script\` rest — stop immediately, nothing more to
|
|
253
|
+
read), \`idle\` (true iff the state landing rests at is the workflow's initial
|
|
254
|
+
state), \`state\`, \`subject\`, \`cost\`, \`model\` (the last three \`null\`, never
|
|
255
|
+
omitted, for a genuine no-op).
|
|
256
|
+
|
|
257
|
+
### The error envelope
|
|
258
|
+
|
|
259
|
+
stdout is either the complete artifact or byte-empty — never a partial write
|
|
260
|
+
followed by an error. On any failure — a usage error, a refusal, a defect —
|
|
261
|
+
stdout stays byte-empty: \`{"state":"error","prompt":"<message>"}\` rides
|
|
262
|
+
**stderr** instead, followed by a single \`gtd: \` line — exit 1 for a refusal
|
|
263
|
+
or defect, exit 2 for a usage error (nothing was even attempted). This is
|
|
264
|
+
true of every command, not only \`gtd next\` — an invocation that carries (or
|
|
265
|
+
misuses) \`--json\` still gets this envelope on its own failure. A driver
|
|
266
|
+
reading stdout on a failed run reads nothing; read stderr or the exit code
|
|
267
|
+
to learn why.
|
|
268
|
+
`
|
|
269
|
+
|
|
270
|
+
const DRIVER_OBLIGATIONS = `
|
|
271
|
+
## Driver obligations, in order
|
|
272
|
+
|
|
273
|
+
1. Read the resolved rest's \`kind\` off \`gtd next --json=kind\` once per
|
|
274
|
+
iteration. Exit code is uniformly \`0\` on success now — it never says
|
|
275
|
+
whose turn is next; only \`1\`/\`2\` means gtd itself failed. There is no
|
|
276
|
+
opening move: a human's pending edit arrives as a \`kind: "capture"\` beat,
|
|
277
|
+
which you land immediately without executing anything. EXCEPT: don't
|
|
278
|
+
trust \`idle\` on the run's very first iteration — land that beat first, so
|
|
279
|
+
a workflow whose first step accepts a clean landing (\`acceptClean\`)
|
|
280
|
+
still gets one chance to advance before you conclude nothing is owed.
|
|
281
|
+
2. Read the content to run/show off \`--json=content\` — never off plain
|
|
282
|
+
\`gtd next\`, which is not a parsing surface (it wraps \`content\` in a
|
|
283
|
+
status-summary header at every kind but \`prompt\`; see the beat protocol
|
|
284
|
+
above). At a \`prompt\` beat, though, pipe plain \`gtd next\`'s own output to
|
|
285
|
+
the agent instead of reading \`--json=content\` separately — it IS the bare
|
|
286
|
+
content at that kind, so a second read would only render the same diff
|
|
287
|
+
twice. Whichever form you read, hand it to the agent CLI over STDIN, never
|
|
288
|
+
as a command-line argument — argv is capped (roughly 1 MB on macOS, and
|
|
289
|
+
POSIX guarantees only 4 KB, \`ARG_MAX\`), both reachable by an ordinary
|
|
290
|
+
diff, and an argv-passing driver fails on the first large one in a way
|
|
291
|
+
that looks like an agent error rather than a driver bug.
|
|
292
|
+
3. A \`kind: "stalled"\` beat prints \`--json=content\` to stderr and halts with
|
|
293
|
+
a non-zero exit; a \`kind: "message"\` beat prints plain \`gtd next\`'s
|
|
294
|
+
output and exits 0, unless it is the run's opening beat — land that one,
|
|
295
|
+
since the human's re-invocation while resting there is itself their
|
|
296
|
+
decision.
|
|
297
|
+
4. Run scripts with their output appended to \`--json=log\` — gtd never
|
|
298
|
+
creates or truncates that file itself; truncate it once per run.
|
|
299
|
+
\`$GTD_LOOP_LOG\` overrides its path.
|
|
300
|
+
5. Map \`--json=session.id\`/\`--json=session.resume\` onto the agent CLI's own
|
|
301
|
+
session flags — try \`resume\`'s hinted flag first and fall back to the
|
|
302
|
+
other on failure (\`resume\` is a hint, not a contract: nothing is stored,
|
|
303
|
+
so a crashed prior turn or an expired agent session recovers by itself).
|
|
304
|
+
6. After a \`prompt\` beat, run the same beat's own \`--json=validate\` script
|
|
305
|
+
(when set) and re-prompt its output verbatim on failure — the DRIVER
|
|
306
|
+
owns the retry cap, not gtd.
|
|
307
|
+
7. Land only a beat you acted on (\`capture\`/\`script\`/\`prompt\`) — a stray
|
|
308
|
+
\`gtd land\` at a clean \`prompt\` rest authors an empty attempt on purpose
|
|
309
|
+
(that IS the stall bookkeeping), so don't land beats you didn't dispatch.
|
|
310
|
+
8. Read \`--json=settled\`/\`--json=idle\` from \`gtd land\` BEFORE running the
|
|
311
|
+
script — \`gtd land\` never mutates, it only plans and prints, so these
|
|
312
|
+
reads and the \`--json=script\` read that follows all agree, planned
|
|
313
|
+
against the same untouched tree. Pipe \`--json=script\`'s own output into
|
|
314
|
+
\`sh\` — never a bare \`gtd land | sh\`, which would hand an empty script to
|
|
315
|
+
\`sh\` on a refusal instead of stopping first.
|
|
316
|
+
9. Check the settled/idle values read in step 8 — no second read needed to
|
|
317
|
+
decide. \`settled = true\` means stop right there (a no-op at a \`script\`
|
|
318
|
+
rest settles in place; nothing more to read); otherwise keep looping, and
|
|
319
|
+
the run finishes when a LATER \`gtd next --json=idle\` reports \`true\`
|
|
320
|
+
(read once more with plain \`gtd next\` only to show that gate's message,
|
|
321
|
+
the decision to stop already made). Exit code carries none of this any
|
|
322
|
+
more — every command exits \`0\` on success uniformly.
|
|
323
|
+
10. A \`message\` beat whose \`--json=judge\` is non-empty is a JUDGE GATE — an
|
|
324
|
+
UNAWARE driver (obligation 3, above) already handles it correctly by
|
|
325
|
+
doing nothing special: display the message (which itself tells a human
|
|
326
|
+
to run \`gtd judge answer\`) and stop. An AWARE driver reads
|
|
327
|
+
\`--json=judge\`, pipes that rendered \`{ schema, state, questions }\` document to a
|
|
328
|
+
judge model, and pipes ITS verdict (one \`{ id, answer, p }\` per pending
|
|
329
|
+
question, as JSON on stdin) to \`gtd judge answer\` — the FIFTH command
|
|
330
|
+
that changes anything, alongside \`gtd land\`/\`gtd --workflow\`/\`gtd
|
|
331
|
+
abandon\`/\`gtd restore\`. It follows the exact same required-half /
|
|
332
|
+
optional-half script contract as \`gtd land\` in obligation 8 above:
|
|
333
|
+
read \`--json=script\` (and, if you want them, \`--json=settled\`/
|
|
334
|
+
\`--json=idle\` — \`gtd judge answer --json\` emits the same pinned 8-key
|
|
335
|
+
document \`gtd land --json=<path>\` does) and pipe the script into \`sh\`.
|
|
336
|
+
A malformed verdict (stdin isn't
|
|
337
|
+
JSON, or it names a question id this judgment never asked) exits 2 — a
|
|
338
|
+
caller-input error, retry with a corrected verdict, not obligation 3's
|
|
339
|
+
"stop" — before anything is planned.
|
|
340
|
+
11. When the chosen agent CLI can restrict which skills a turn sees (\`pi\`
|
|
341
|
+
can; \`claude -p\` cannot), the built driver MUST pass each prompt turn's
|
|
342
|
+
\`--json=skills\` (read per entry, \`skills.0\`, \`skills.1\`, …) so the
|
|
343
|
+
turn sees only those. An absent field means none. A CLI that cannot
|
|
344
|
+
restrict relies on the preamble gtd already puts in the prompt — the
|
|
345
|
+
reference body's \`claude -p\` does exactly that.
|
|
346
|
+
12. Map each prompt turn's \`--json=access.read\` / \`--json=access.write\`
|
|
347
|
+
(arrays of globs, \`null\` = unrestricted) onto the chosen CLI's sandbox
|
|
348
|
+
(Codex \`workspace-write\` plus writable roots) or a container with
|
|
349
|
+
\`:ro\` / \`:rw\` bind mounts. \`access\` is a prompt-only field. A CLI
|
|
350
|
+
that cannot restrict relies on the preamble and gtd's landing check. When
|
|
351
|
+
\`gtd land\` refuses naming an access violation, re-prompt the turn with
|
|
352
|
+
the refusal text and land again, or stop.
|
|
353
|
+
`
|
|
354
|
+
|
|
355
|
+
const RECOVERY = `
|
|
356
|
+
## Recovery
|
|
357
|
+
|
|
358
|
+
gtd exiting 2 means nothing was even attempted — a usage error; exiting 1
|
|
359
|
+
means a refusal — nothing was emitted either. An emitted script exiting
|
|
360
|
+
non-zero when YOU run it means something MAY have partially happened. Both
|
|
361
|
+
recover the same way: re-invoke gtd (\`gtd next --json=kind\`, \`gtd next\`,
|
|
362
|
+
then \`gtd land --json=<path>\`). It re-reads the real repository state fresh
|
|
363
|
+
every time — never a cached plan — and emits whatever still needs to happen
|
|
364
|
+
from there. This works because every emitted script asserts its own HEAD
|
|
365
|
+
precondition, so a script generated against a repository state that has
|
|
366
|
+
since moved refuses loudly instead of corrupting anything. Scripts are
|
|
367
|
+
re-runnable and assert their own HEAD precondition — a driver needs no retry
|
|
368
|
+
logic beyond "ask gtd again".
|
|
369
|
+
`
|
|
370
|
+
|
|
371
|
+
const commandSuite = (): string =>
|
|
372
|
+
`
|
|
373
|
+
## Building the user's command suite: interview first, then adapt
|
|
374
|
+
|
|
375
|
+
The obligations above are the contract; the three command bodies below are one
|
|
376
|
+
WORKED EXAMPLE of it. Do not copy them blindly, and do not guess the user's
|
|
377
|
+
setup — INVESTIGATE first, then INTERVIEW, then build a suite shaped by both.
|
|
378
|
+
Hold ONE conversation, not three: the setup steps below come first, because a
|
|
379
|
+
repo that is not set up yet cannot be driven, and the suite-shape questions
|
|
380
|
+
follow in the same numbered list.
|
|
381
|
+
|
|
382
|
+
1. **Investigate the repository and ask the user what they want before
|
|
383
|
+
driving anything.** Look at what is already there (a \`.gtd/\` directory,
|
|
384
|
+
existing config, the project's own README/docs) and ask the user what
|
|
385
|
+
they're trying to accomplish with gtd here — don't assume a fresh
|
|
386
|
+
install or silently start a process on their behalf.
|
|
387
|
+
2. **Make sure a \`gtd.config.ts\` governs the repository.** Run at the
|
|
388
|
+
repository root, \`gtd install\` writes one importing gtd's shipped
|
|
389
|
+
workflows when none exists yet (its own message says so). Repository
|
|
390
|
+
settings — steering-file modes, \`gtd ui\`, per-scope skills and access —
|
|
391
|
+
go in its \`config\` export; workflow settings are options of the
|
|
392
|
+
workflow factory it exports.
|
|
393
|
+
3. **Commit the config before the first drive.** \`gtd install\` leaves the
|
|
394
|
+
file it writes uncommitted on purpose — an uncommitted config is a
|
|
395
|
+
pending change, so starting a process without committing it first turns
|
|
396
|
+
the config into the very first thing the process plans against. Commit
|
|
397
|
+
it, THEN start driving.
|
|
398
|
+
4. **Which agent should run the turns?** Probe \`PATH\` for the known
|
|
399
|
+
coding-agent CLIs — \`claude\`, \`codex\`, \`gemini\`, \`cursor-agent\`,
|
|
400
|
+
\`aider\`, \`opencode\`, \`amp\` — show the user which are actually
|
|
401
|
+
installed, and ask which one to drive with (default: \`claude\`). Accept
|
|
402
|
+
a name that was not on the list; the probe is a convenience, not a
|
|
403
|
+
restriction. The chosen CLI's own flags then replace the \`claude\` lines
|
|
404
|
+
in the reference body below — its session flags (obligation 5), its
|
|
405
|
+
skill-restriction flag when it has one (obligation 11) and its
|
|
406
|
+
permission model (question 5). Carry over the body's \`export\` block too:
|
|
407
|
+
whatever the chosen CLI's own way is to **stop backgrounding commands and
|
|
408
|
+
let a foreground command run for hours** — a turn ends when the CLI
|
|
409
|
+
returns, so anything it leaves running dies unread and stalls the
|
|
410
|
+
process.
|
|
411
|
+
5. **Under which permission model, and should the workflow's model hints be
|
|
412
|
+
honored at all?** Fully autonomous turns (e.g.
|
|
413
|
+
\`--dangerously-skip-permissions\`), a sandbox, or the agent's default
|
|
414
|
+
prompting — their risk tolerance, their call. Also ask whether the
|
|
415
|
+
workflow's own \`model\` hints (resolved in question 6 below) should be
|
|
416
|
+
honored (default: yes) — answering no means dropping \`--model\` entirely
|
|
417
|
+
from every prompt line and writing no model exports, so every turn runs
|
|
418
|
+
on the chosen CLI's own default tier.
|
|
419
|
+
6. **If model hints are honored, resolve them to real model identifiers.**
|
|
420
|
+
The bundled workflow ships \`plannerModel: smart\` and \`coderModel: base\`.
|
|
421
|
+
**Those are opaque hints, not model names** — gtd never interprets them,
|
|
422
|
+
and passing \`--model smart\` to a real CLI fails on the first prompt
|
|
423
|
+
beat. Derive the chosen CLI's own available models at install time — its
|
|
424
|
+
\`--help\`, its docs, its own config — never a hardcoded table (a vendor
|
|
425
|
+
renames a model and a baked-in list goes stale the day it ships). **When
|
|
426
|
+
the heavier/cheaper mapping is not obvious, ask**: list the CLI's models
|
|
427
|
+
and let the user pick one per tier — the heavier tier for
|
|
428
|
+
\`plannerModel\` (triage, design, review), the cheaper tier for
|
|
429
|
+
\`coderModel\` (build, fix). Write the resolved names as
|
|
430
|
+
\`GTD_PLANNERMODEL\`/\`GTD_CODERMODEL\` exports at the top of \`gtd-build\`,
|
|
431
|
+
wrapped in \`# gtd-install: model exports\` / \`# gtd-install: end\`
|
|
432
|
+
markers — NOT into \`gtd.config.ts\`. They stay per machine and are never
|
|
433
|
+
committed, so one person's model choice binds nobody else on the repo.
|
|
434
|
+
Two consequences follow, both worth stating to the user: \`gtd-door\`
|
|
435
|
+
inherits these exports only because it \`exec\`s
|
|
436
|
+
\`gtd-build\` — anything that drives beats without going through
|
|
437
|
+
\`gtd-build\` sees the raw \`smart\`/\`base\` hints and fails; and \`GTD_*\` is
|
|
438
|
+
the highest-precedence config layer, so these exports silently win over
|
|
439
|
+
a \`plannerModel\`/\`coderModel\` a teammate later commits to
|
|
440
|
+
\`gtd.config.ts\`.
|
|
441
|
+
7. **What should happen at the boundaries?** Where the log goes (obligation
|
|
442
|
+
4), and whether halting at a human gate should do anything richer than
|
|
443
|
+
print — desktop notification, terminal-multiplexer status, editor focus —
|
|
444
|
+
which belongs in their wrapper, never in gtd.
|
|
445
|
+
8. **How do they want to invoke it?** A command on PATH (default:
|
|
446
|
+
\`~/.local/bin/gtd-build\`), a project task-runner entry, a CI job step —
|
|
447
|
+
or no artifact at all: YOU drive the beats yourself, following the
|
|
448
|
+
obligations directly. Pick the runtime to match: bash, their language of
|
|
449
|
+
choice, anything that reads \`--json=<path>\` and spawns subprocesses.
|
|
450
|
+
9. **Where should the suite live, which editor should \`gtd-edit\` spawn, and
|
|
451
|
+
does anything need renaming?** Suggest \`~/.local/bin\` for all three —
|
|
452
|
+
\`gtd-build\`, \`gtd-edit\`, \`gtd-door\` — and \`$EDITOR\` (never
|
|
453
|
+
a hardcoded editor) for the one that opens files. The interview can
|
|
454
|
+
rename any of the three; whatever name is chosen for \`gtd-build\` is the
|
|
455
|
+
RESOLVED path baked into \`GTD_BUILD\` in the \`gtd-door\` body below —
|
|
456
|
+
never the literal string \`gtd-build\`.
|
|
457
|
+
|
|
458
|
+
Then build it, and verify safely before the first real drive:
|
|
459
|
+
\`gtd next --json=<path>\`/\`gtd next\` are both pure reads — read/parse them,
|
|
460
|
+
check your kind dispatch against the table above, call them as often as you
|
|
461
|
+
like. Nothing happens until you run an emitted script.
|
|
462
|
+
|
|
463
|
+
The reference rendering in sh (no \`jq\`, no JSON parser at all — each
|
|
464
|
+
\`--json=<path>\` call prints its one value directly). \`gtd-door\` takes the
|
|
465
|
+
\`gtd-build\` path from one convention: \`GTD_BUILD\` is set once, at the top, to
|
|
466
|
+
the suite's resolved \`gtd-build\` path.
|
|
467
|
+
|
|
468
|
+
### \`gtd-build\` — the driver loop
|
|
469
|
+
|
|
470
|
+
The \`claude\` lines are what interview answers 4 and 6 replace. Default path:
|
|
471
|
+
\`~/.local/bin/gtd-build\`.
|
|
472
|
+
|
|
473
|
+
\`\`\`bash
|
|
474
|
+
${MINIMAL_DRIVER}
|
|
475
|
+
\`\`\`
|
|
476
|
+
|
|
477
|
+
### \`gtd-edit\` — open the steering file the process is resting at
|
|
478
|
+
|
|
479
|
+
\`gtd next --json=file\` already reports the file for every state that
|
|
480
|
+
declares one — the edit command needs no new engine surface, just a read and
|
|
481
|
+
an editor spawn. \`file\` is an OPTIONAL field: \`--json=file\` prints nothing
|
|
482
|
+
(not the string \`null\`) at a state that declares no \`file:\`, so read it into
|
|
483
|
+
a shell variable and fall back explicitly — \`f="$(gtd next --json=file)"\`
|
|
484
|
+
then \`f="\${f:-.gtd/TODO.md}"\`, never assume the read populated anything. This
|
|
485
|
+
is also the general rule for every optional field \`--json=<path>\` exposes:
|
|
486
|
+
an absent field is silence, not the string \`null\` a \`jq .field\` recipe would
|
|
487
|
+
print on the same input — code that treats empty output as "field genuinely
|
|
488
|
+
absent" is correct, code that expects a literal \`null\` string is not.
|
|
489
|
+
|
|
490
|
+
This is also how a human starts a process at all: at the initial \`idle\`
|
|
491
|
+
state the resting file IS \`.gtd/TODO.md\`, so on a clean repository the edit
|
|
492
|
+
command opens the empty scratch file, and whatever the human writes there is
|
|
493
|
+
the first sketch the whole process gets planned from. The same command does
|
|
494
|
+
the same thing at every rest, whether or not a state declares a \`file:\`.
|
|
495
|
+
|
|
496
|
+
The edit command stops when the editor exits — it never drives the loop. It
|
|
497
|
+
opens a file and returns; the human runs \`gtd-build\` themselves when they
|
|
498
|
+
are ready. Default path: \`~/.local/bin/gtd-edit\`.
|
|
499
|
+
|
|
500
|
+
\`\`\`bash
|
|
501
|
+
${EDIT_COMMAND}
|
|
502
|
+
\`\`\`
|
|
503
|
+
|
|
504
|
+
### \`gtd-door <name> [args...]\` — start a process through a door and drive it
|
|
505
|
+
|
|
506
|
+
Runs \`gtd door <name> [args...]\`, captured by command substitution — never a
|
|
507
|
+
pipe: a pipeline reports only its LAST command's exit status, so
|
|
508
|
+
\`gtd door ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
|
|
509
|
+
loop over a process that was never started. It runs the captured script, then
|
|
510
|
+
\`exec\`s the suite's RESOLVED \`gtd-build\` path — never the literal string
|
|
511
|
+
\`gtd-build\`. This is one invocation that carries the process all the way to
|
|
512
|
+
its next human gate, not just a starter: \`gtd-door review\` on a RED baseline
|
|
513
|
+
hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
|
|
514
|
+
rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
515
|
+
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
516
|
+
\`gtd doors\` lists the doors a repository offers (those of the workflows its
|
|
517
|
+
config exports — the bundled \`fix\` and \`review [base]\` — plus any it
|
|
518
|
+
declares). Refuses with
|
|
519
|
+
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
520
|
+
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
521
|
+
|
|
522
|
+
\`\`\`bash
|
|
523
|
+
${DOOR_COMMAND}
|
|
524
|
+
\`\`\`
|
|
525
|
+
${REINSTALL}`
|
|
526
|
+
|
|
527
|
+
const REINSTALL = `
|
|
528
|
+
## Re-installing: detect and adapt, don't blindly overwrite
|
|
529
|
+
|
|
530
|
+
A second install on a machine that already has a suite is not a fresh
|
|
531
|
+
install — read each of the three suite paths before writing anything
|
|
532
|
+
(\`gtd-build\`, \`gtd-edit\`, \`gtd-door\`), and branch per path:
|
|
533
|
+
|
|
534
|
+
- Absent — install it, as in a fresh install.
|
|
535
|
+
- Present and content-equal to what this gtd version would emit — say so,
|
|
536
|
+
change nothing, and skip that command's interview questions entirely.
|
|
537
|
+
- Present and different — show the difference, name the likely cause (a gtd
|
|
538
|
+
upgrade, or the user's own edit), and ask before overwriting. Never
|
|
539
|
+
silently replace a file the user may have customised.
|
|
540
|
+
- Unreadable, or present as a directory instead of a file — report it and
|
|
541
|
+
ask about it, the same as a differing file. Never overwritten.
|
|
542
|
+
|
|
543
|
+
Drift is detected by comparing CONTENT, never by parsing a version out of
|
|
544
|
+
the installed file — an installed command carries no version marker to
|
|
545
|
+
parse.
|
|
546
|
+
|
|
547
|
+
\`gtd-build\` needs one exemption before that comparison: the region between
|
|
548
|
+
\`# gtd-install: model exports\` and \`# gtd-install: end\` gets stripped from
|
|
549
|
+
the installed file first. A \`gtd-build\` differing only inside those
|
|
550
|
+
markers is unchanged, and re-asks nothing — the resolved model names are
|
|
551
|
+
per machine, so without this exemption every single re-install would
|
|
552
|
+
report drift on the one command everybody has.
|
|
553
|
+
|
|
554
|
+
This check scopes to exactly the three suite paths above, plus the superseded
|
|
555
|
+
pair below. \`gtd-loop\` is
|
|
556
|
+
outside it entirely — never read, never diffed, never touched by it in any
|
|
557
|
+
way — an existing \`gtd-loop\` survives untouched beside the new
|
|
558
|
+
\`gtd-build\`, and cleaning it up is the human's own call.
|
|
559
|
+
|
|
560
|
+
\`gtd-fix\` and \`gtd-review\` are superseded by \`gtd-door\` (\`gtd-door fix\`,
|
|
561
|
+
\`gtd-door review [base]\`). When either is still present beside the suite,
|
|
562
|
+
name it to the user as superseded and ask before removing it — never remove
|
|
563
|
+
one unasked.
|
|
564
|
+
`
|
|
565
|
+
|
|
566
|
+
const EDITOR_INTEGRATION = `
|
|
567
|
+
## Editor integration: offer it, don't bury it
|
|
568
|
+
|
|
569
|
+
\`gtd lsp\` runs a language server, started by \`gtd lsp\` itself, speaking LSP
|
|
570
|
+
over \`stdio\`, applying to files under \`.gtd/\`. That is the whole integration
|
|
571
|
+
contract, identical for every editor — this briefing names no specific editor
|
|
572
|
+
and carries no per-editor config recipe.
|
|
573
|
+
|
|
574
|
+
Find the user's editor from their OWN shell configuration, never a guess:
|
|
575
|
+
check \`$EDITOR\`/\`$VISUAL\` first, then look for an alias or export in their
|
|
576
|
+
shell rc files (\`.zshrc\`, \`.bashrc\`, \`.config/fish/config.fish\`). Ask the
|
|
577
|
+
user outright when that turns up nothing or turns up more than one. When the
|
|
578
|
+
shell configuration names no editor and the user names none either, say
|
|
579
|
+
nothing and move on — don't pitch editor integration to somebody with no
|
|
580
|
+
editor to integrate.
|
|
581
|
+
|
|
582
|
+
Once an editor is identified, look ITS OWN LSP configuration format up
|
|
583
|
+
yourself — there is no bundled recipe for it. If it is LSP-capable, offer to
|
|
584
|
+
wire \`gtd lsp\` up with a brief explanation (three or four lines, not a copy
|
|
585
|
+
of \`docs/setup.md\`): live diagnostics on \`.gtd/\` steering files, an outline
|
|
586
|
+
of what is left to review, click-to-check review hunks, pick-an-option
|
|
587
|
+
actions on open questions, and a \`gtd.openSteeringFile\` command that jumps
|
|
588
|
+
to the current state's file. Two facts belong in that offer because both
|
|
589
|
+
bite on a fresh repo: \`gtd lsp\` never creates \`.gtd/\`, so
|
|
590
|
+
\`gtd.openSteeringFile\` may point at a path that does not exist yet before
|
|
591
|
+
the first sketch; and \`gtd lsp\` needs no repository root — it is one of the
|
|
592
|
+
commands that skips the root guard.
|
|
593
|
+
|
|
594
|
+
On yes, edit the editor's own config file yourself — never print a snippet
|
|
595
|
+
and walk away. Integration works without the human touching anything. Three
|
|
596
|
+
guards apply to that write, because it lands outside the repository in a
|
|
597
|
+
file the user shares across every project:
|
|
598
|
+
|
|
599
|
+
- **Ask first, per editor, naming the exact file** about to change. Silence
|
|
600
|
+
is not consent for a machine-wide config.
|
|
601
|
+
- **Merge, never overwrite.** Read the existing config, add only the gtd
|
|
602
|
+
language-server entry, and leave every other key byte-identical.
|
|
603
|
+
- **Skip and report when the entry is already present** — a second install
|
|
604
|
+
must not append a duplicate server registration. A malformed existing
|
|
605
|
+
config (unparseable JSON, a TOML syntax error) is a stop-and-report, not a
|
|
606
|
+
rewrite: name the file that could not be parsed and leave it untouched.
|
|
607
|
+
`
|
|
608
|
+
|
|
609
|
+
const PREREQUISITES = `
|
|
610
|
+
## Prerequisites and portability
|
|
611
|
+
|
|
612
|
+
- A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
|
|
613
|
+
scripts (\`gtd land --json=script\`, \`gtd --workflow <name>\`, \`gtd abandon\`,
|
|
614
|
+
\`gtd restore\`, \`gtd judge answer --json=script\`) are POSIX sh; captured,
|
|
615
|
+
then piped into it (see obligation 8 above). Reading
|
|
616
|
+
\`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own output needs
|
|
617
|
+
nothing beyond the same POSIX \`sh\` — no \`eval\`, no parser.
|
|
618
|
+
- \`gtd\` on \`PATH\` — a seeded mode \`validate:\` command is literally the
|
|
619
|
+
string \`gtd check <mode> '<file>'\`, resolved by NAME at script-run time.
|
|
620
|
+
Keep one \`gtd\` on \`PATH\`, consistently.
|
|
621
|
+
|
|
622
|
+
Any runtime works: the contract is \`--json\`/\`--json=<path>\` in, subprocesses
|
|
623
|
+
out.
|
|
624
|
+
`
|
|
625
|
+
|
|
626
|
+
export const renderBriefing = (): string =>
|
|
627
|
+
HEADER() +
|
|
628
|
+
BEAT_PROTOCOL +
|
|
629
|
+
JSON_FIELD_REFERENCE +
|
|
630
|
+
DRIVER_OBLIGATIONS +
|
|
631
|
+
RECOVERY +
|
|
632
|
+
commandSuite() +
|
|
633
|
+
EDITOR_INTEGRATION +
|
|
634
|
+
PREREQUISITES
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { createRequire } from "node:module"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
const _require = createRequire(import.meta.url)
|
|
7
|
+
|
|
8
|
+
// A source-relative literal (`../../package.json`) breaks the built single-file
|
|
9
|
+
// bundle: bundling doesn't rewrite `import.meta.url` per original module, so a
|
|
10
|
+
// hardcoded depth only matches whichever depth this file happens to sit at in
|
|
11
|
+
// dist/ vs src/ — they differ (dist/gtd.bundle.mjs is always one level below
|
|
12
|
+
// repo root; src/install/Version.ts is two). Walking up from wherever this module
|
|
13
|
+
// actually runs from resolves correctly in both the unbundled (vitest/tsc) and
|
|
14
|
+
// bundled (dist/gtd.bundle.mjs) case.
|
|
15
|
+
const findPackageJson = (fromUrl: string): string => {
|
|
16
|
+
let dir = dirname(fileURLToPath(fromUrl))
|
|
17
|
+
for (let i = 0; i < 5; i++) {
|
|
18
|
+
const candidate = join(dir, "package.json")
|
|
19
|
+
if (existsSync(candidate)) return candidate
|
|
20
|
+
dir = dirname(dir)
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`package.json not found by walking up from ${fromUrl}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const GTD_VERSION: string = (
|
|
26
|
+
_require(findPackageJson(import.meta.url)) as { version: string }
|
|
27
|
+
).version
|