@pmelab/gtd 59.0.0 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
package/src/cli/Cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "./Program.js"
|
|
37
37
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
38
38
|
import { THRESHOLDS } from "./Judges.js"
|
|
39
|
+
import { closest, definition, wrap } from "./Help.js"
|
|
39
40
|
import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
|
|
40
41
|
|
|
41
42
|
export type { Command, CommandRequirements } from "./Program.js"
|
|
@@ -77,12 +78,45 @@ interface FlagRow {
|
|
|
77
78
|
readonly decode: (raws: readonly string[]) => Either.Either<unknown, string>
|
|
78
79
|
readonly scopeError: string
|
|
79
80
|
readonly valueHint: string
|
|
80
|
-
/**
|
|
81
|
-
readonly help:
|
|
81
|
+
/** One sentence for a command's help, wrapped when rendered; `helpFor` overrides it for a command the flag means something else on. */
|
|
82
|
+
readonly help: string
|
|
83
|
+
readonly helpFor?: Partial<Record<Command["kind"], string>>
|
|
82
84
|
/** Flag names this one may never appear alongside — checked generically after parsing (see `conflictViolation`), never as a bespoke `if`. Defaults to none. */
|
|
83
85
|
readonly conflicts?: readonly string[]
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/** `<name>=<value>` occurrences of `flag` → one record, each name a setting name given at most once. */
|
|
89
|
+
const decodeAssignments = (
|
|
90
|
+
flag: string,
|
|
91
|
+
raws: readonly string[],
|
|
92
|
+
): Either.Either<Readonly<Record<string, string>>, string> => {
|
|
93
|
+
const vars: Record<string, string> = {}
|
|
94
|
+
for (const raw of raws) {
|
|
95
|
+
const eq = raw.indexOf("=")
|
|
96
|
+
if (eq <= 0) {
|
|
97
|
+
return Either.left(`gtd: ${flag} must be <name>=<value> with a non-empty name — got "${raw}"`)
|
|
98
|
+
}
|
|
99
|
+
const name = raw.slice(0, eq)
|
|
100
|
+
const value = raw.slice(eq + 1)
|
|
101
|
+
if (!isSettingName(name)) {
|
|
102
|
+
return Either.left(
|
|
103
|
+
`gtd: ${flag} ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (/[\r\n]/.test(value)) return Either.left(`gtd: ${flag} ${name} must be a single-line value`)
|
|
107
|
+
if (Object.hasOwn(vars, name))
|
|
108
|
+
return Either.left(`gtd: ${flag} ${name} specified more than once`)
|
|
109
|
+
if (!name.startsWith("flag_")) {
|
|
110
|
+
vars[name] = value
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const pinned = flagSettingValue(value)
|
|
114
|
+
if (pinned === undefined) return Either.left(`gtd: ${flag} ${name} must be 1, 0, true or false`)
|
|
115
|
+
vars[name] = pinned
|
|
116
|
+
}
|
|
117
|
+
return Either.right(vars)
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, string> => {
|
|
87
121
|
const n = Number(raw)
|
|
88
122
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) {
|
|
@@ -91,12 +125,23 @@ const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, str
|
|
|
91
125
|
return Either.right(n)
|
|
92
126
|
}
|
|
93
127
|
|
|
128
|
+
/** The commands whose only output is a script: `--json` prints `{ schema, script }`. */
|
|
129
|
+
const SCRIPT_KINDS: ReadonlySet<Command["kind"]> = new Set<Command["kind"]>([
|
|
130
|
+
"start",
|
|
131
|
+
"abandon",
|
|
132
|
+
"restore",
|
|
133
|
+
"validate",
|
|
134
|
+
"historyFetch",
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const SCRIPT_JSON_HELP = "Print { schema, script }, or with --json=script the script alone"
|
|
138
|
+
|
|
94
139
|
const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["kind"]>([
|
|
140
|
+
...SCRIPT_KINDS,
|
|
95
141
|
"next",
|
|
96
142
|
"land",
|
|
97
143
|
"judge",
|
|
98
144
|
"judgeAnswer",
|
|
99
|
-
"doors",
|
|
100
145
|
"historyList",
|
|
101
146
|
"historyShow",
|
|
102
147
|
"historyMetrics",
|
|
@@ -112,23 +157,15 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
112
157
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
113
158
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
114
159
|
scopeError:
|
|
115
|
-
"gtd: --json is only valid for `gtd next`/`gtd land
|
|
116
|
-
"every other command prints plain text; see `gtd
|
|
160
|
+
"gtd: --json is only valid for `gtd next`/`gtd land`, the commands that print a script, and the listings whose help names it (e.g. `gtd strays`) — " +
|
|
161
|
+
"every other command prints plain text; see `gtd guide` for the driver protocol briefing",
|
|
117
162
|
valueHint: "<path>",
|
|
118
|
-
help:
|
|
119
|
-
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"one JSON entry per line — a numeric segment (e.g.",
|
|
125
|
-
"skills.0) indexes into one entry of a list field instead.",
|
|
126
|
-
"An absent optional field prints nothing and exits 0 —",
|
|
127
|
-
"including when an earlier segment of <path> is itself",
|
|
128
|
-
"absent/null (e.g. session.id at a non-prompt rest), which",
|
|
129
|
-
"never counts as unknown; an unknown path is a usage error",
|
|
130
|
-
"(exit 2).",
|
|
131
|
-
],
|
|
163
|
+
help: "Print the JSON document, or with --json=<path> one field of it (e.g. --json=kind): a scalar bare, a list one entry per line, nothing for an absent field",
|
|
164
|
+
helpFor: {
|
|
165
|
+
strays: "Print one entry of the array (--json=0), nothing past its end",
|
|
166
|
+
...Object.fromEntries([...SCRIPT_KINDS].map((kind) => [kind, SCRIPT_JSON_HELP])),
|
|
167
|
+
start: `${SCRIPT_JSON_HELP}. Bare, print the list as a JSON array of { kind, name } (a door adds workflow and args)`,
|
|
168
|
+
},
|
|
132
169
|
},
|
|
133
170
|
{
|
|
134
171
|
name: "--port",
|
|
@@ -143,11 +180,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
143
180
|
},
|
|
144
181
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
145
182
|
valueHint: "<n>",
|
|
146
|
-
help:
|
|
147
|
-
"(gtd ui only) the tailscale serve port (default: the first",
|
|
148
|
-
"free of 8443, 10000, 443), or the bind port when --host",
|
|
149
|
-
"opts out of serve (default: a free port)",
|
|
150
|
-
],
|
|
183
|
+
help: "The tailscale serve port (default: the first free of 8443, 10000, 443), or with --host the port to bind (default: any free one)",
|
|
151
184
|
},
|
|
152
185
|
{
|
|
153
186
|
name: "--host",
|
|
@@ -160,11 +193,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
160
193
|
: Either.right(raw),
|
|
161
194
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
162
195
|
valueHint: "<addr>",
|
|
163
|
-
help:
|
|
164
|
-
"(gtd ui only) opt out of the default tailscale serve front",
|
|
165
|
-
"door and bind this address directly instead, showing it in",
|
|
166
|
-
"the printed URL",
|
|
167
|
-
],
|
|
196
|
+
help: "Bind this address directly instead of publishing through tailscale serve",
|
|
168
197
|
},
|
|
169
198
|
{
|
|
170
199
|
name: "--self-signed",
|
|
@@ -174,10 +203,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
174
203
|
decode: () => Either.right(true),
|
|
175
204
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
176
205
|
valueHint: "",
|
|
177
|
-
help:
|
|
178
|
-
"(gtd ui only) generate a throwaway self-signed TLS",
|
|
179
|
-
"certificate instead of the configured ui.cert/ui.key",
|
|
180
|
-
],
|
|
206
|
+
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key",
|
|
181
207
|
},
|
|
182
208
|
{
|
|
183
209
|
name: "--dev",
|
|
@@ -187,7 +213,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
187
213
|
decode: () => Either.right(true),
|
|
188
214
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
189
215
|
valueHint: "",
|
|
190
|
-
help:
|
|
216
|
+
help: "Serve local development sources instead of the packaged build",
|
|
191
217
|
},
|
|
192
218
|
{
|
|
193
219
|
name: "--provider",
|
|
@@ -200,11 +226,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
200
226
|
: Either.left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
201
227
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
202
228
|
valueHint: "<name>",
|
|
203
|
-
help:
|
|
204
|
-
"(gtd judge run only) the answerer: fixed | jev | llm;",
|
|
205
|
-
"omitted: GTD_JUDGE_PROVIDER, then judge.json, then jev",
|
|
206
|
-
"when TYPESAFE_API_KEY is set, else llm",
|
|
207
|
-
],
|
|
229
|
+
help: "The answerer: fixed, jev or llm (default: $GTD_JUDGE_PROVIDER, then ~/.config/gtd/judge.json, then jev when $TYPESAFE_API_KEY is set, else llm)",
|
|
208
230
|
},
|
|
209
231
|
{
|
|
210
232
|
name: "--answers",
|
|
@@ -217,11 +239,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
217
239
|
: Either.right(raw),
|
|
218
240
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
219
241
|
valueHint: "<path>",
|
|
220
|
-
help: [
|
|
221
|
-
"(gtd judge run --provider fixed only) a JSON verdict array",
|
|
222
|
-
"[{ id, answer, p }]; else the GTD_JUDGE_ANSWERS env var",
|
|
223
|
-
"(inline JSON) is read",
|
|
224
|
-
],
|
|
242
|
+
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)",
|
|
225
243
|
},
|
|
226
244
|
{
|
|
227
245
|
name: "--cost",
|
|
@@ -231,7 +249,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
231
249
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
232
250
|
scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
|
|
233
251
|
valueHint: "<n>",
|
|
234
|
-
help:
|
|
252
|
+
help: "Record the token cost of the invocation that just finished on the commit",
|
|
235
253
|
},
|
|
236
254
|
{
|
|
237
255
|
name: "--model",
|
|
@@ -242,80 +260,35 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
242
260
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
243
261
|
? Either.left("gtd: --model must be a non-empty, single-line value")
|
|
244
262
|
: Either.right(raw),
|
|
245
|
-
scopeError:
|
|
263
|
+
scopeError:
|
|
264
|
+
"gtd: --model is only valid for `gtd land` (with --cost: the model to record the cost against) or `gtd judge run` (the model that answers)",
|
|
246
265
|
valueHint: "<name>",
|
|
247
|
-
help:
|
|
248
|
-
|
|
249
|
-
"
|
|
250
|
-
|
|
251
|
-
|
|
266
|
+
help: "",
|
|
267
|
+
helpFor: {
|
|
268
|
+
land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
|
|
269
|
+
judgeRun:
|
|
270
|
+
"The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)",
|
|
271
|
+
},
|
|
252
272
|
},
|
|
253
273
|
{
|
|
254
|
-
name: "--
|
|
274
|
+
name: "--threshold",
|
|
255
275
|
arity: 1,
|
|
256
|
-
repeatable:
|
|
257
|
-
scope: (kind) => kind === "
|
|
258
|
-
decode: (
|
|
259
|
-
scopeError:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
valueHint: "<name>",
|
|
263
|
-
help: [
|
|
264
|
-
"(with no command at all) start a brand new process on",
|
|
265
|
-
"the named workflow — authenticated as human",
|
|
266
|
-
],
|
|
276
|
+
repeatable: true,
|
|
277
|
+
scope: (kind) => kind === "historyJudges",
|
|
278
|
+
decode: (raws) => decodeAssignments("--threshold", raws),
|
|
279
|
+
scopeError: "gtd: --threshold is only valid for `gtd history judges`",
|
|
280
|
+
valueHint: "<name>=<value>",
|
|
281
|
+
help: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)",
|
|
267
282
|
},
|
|
268
283
|
{
|
|
269
284
|
name: "--var",
|
|
270
285
|
arity: 1,
|
|
271
286
|
repeatable: true,
|
|
272
|
-
scope: (kind) => kind === "start"
|
|
273
|
-
decode: (raws) =>
|
|
274
|
-
|
|
275
|
-
const seen = new Set<string>()
|
|
276
|
-
for (const raw of raws) {
|
|
277
|
-
const eq = raw.indexOf("=")
|
|
278
|
-
if (eq <= 0) {
|
|
279
|
-
return Either.left(
|
|
280
|
-
`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`,
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
const name = raw.slice(0, eq)
|
|
284
|
-
const value = raw.slice(eq + 1)
|
|
285
|
-
if (!isSettingName(name)) {
|
|
286
|
-
return Either.left(
|
|
287
|
-
`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`,
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
if (/[\r\n]/.test(value)) {
|
|
291
|
-
return Either.left(`gtd: --var ${name} must be a single-line value`)
|
|
292
|
-
}
|
|
293
|
-
if (seen.has(name)) {
|
|
294
|
-
return Either.left(`gtd: --var ${name} specified more than once`)
|
|
295
|
-
}
|
|
296
|
-
seen.add(name)
|
|
297
|
-
if (name.startsWith("flag_")) {
|
|
298
|
-
const pinned = flagSettingValue(value)
|
|
299
|
-
if (pinned === undefined) {
|
|
300
|
-
return Either.left(`gtd: --var ${name} must be 1, 0, true or false`)
|
|
301
|
-
}
|
|
302
|
-
vars[name] = pinned
|
|
303
|
-
continue
|
|
304
|
-
}
|
|
305
|
-
vars[name] = value
|
|
306
|
-
}
|
|
307
|
-
return Either.right(vars)
|
|
308
|
-
},
|
|
309
|
-
scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
|
|
287
|
+
scope: (kind) => kind === "start",
|
|
288
|
+
decode: (raws) => decodeAssignments("--var", raws),
|
|
289
|
+
scopeError: "gtd: --var is only valid for `gtd start <name>`",
|
|
310
290
|
valueHint: "<name>=<value>",
|
|
311
|
-
help:
|
|
312
|
-
"(with --workflow; repeatable) pin a process setting for the",
|
|
313
|
-
"new process; the name must already be declared by the",
|
|
314
|
-
"workflow's defaults (environment settings are not",
|
|
315
|
-
"pinnable). (gtd history judges) re-decide past verdicts",
|
|
316
|
-
"under this threshold: judgeIdenticalMinP or",
|
|
317
|
-
"qualityTriageMinP",
|
|
318
|
-
],
|
|
291
|
+
help: "Pin one of the workflow's declared settings for the new process (repeatable); through a door, on top of the settings its args set",
|
|
319
292
|
},
|
|
320
293
|
{
|
|
321
294
|
name: "--jsonl",
|
|
@@ -325,8 +298,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
325
298
|
decode: () => Either.right(true),
|
|
326
299
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
327
300
|
valueHint: "",
|
|
328
|
-
help:
|
|
329
|
-
conflicts: ["--json", "--
|
|
301
|
+
help: "Print one JSON line per verdict instead of the report",
|
|
302
|
+
conflicts: ["--json", "--threshold"],
|
|
330
303
|
},
|
|
331
304
|
{
|
|
332
305
|
name: "--open-questions",
|
|
@@ -336,12 +309,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
336
309
|
decode: () => Either.right(true),
|
|
337
310
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
338
311
|
valueHint: "",
|
|
339
|
-
help:
|
|
340
|
-
"(gtd check only) ignore <mode>'s structural findings and",
|
|
341
|
-
"instead run the qa open-questions predicate over <file>,",
|
|
342
|
-
"printing each unanswered question one per line and exiting",
|
|
343
|
-
"non-zero when any remain",
|
|
344
|
-
],
|
|
312
|
+
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain",
|
|
345
313
|
},
|
|
346
314
|
{
|
|
347
315
|
name: "--open-threads",
|
|
@@ -351,15 +319,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
351
319
|
decode: () => Either.right(true),
|
|
352
320
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
353
321
|
valueHint: "",
|
|
354
|
-
help:
|
|
355
|
-
"(gtd check only) ignore <mode>'s structural findings and",
|
|
356
|
-
"instead list every open thread (a footnote whose last",
|
|
357
|
-
"entry is the agent's) over <file>, one per line with its",
|
|
358
|
-
"first H entry, exiting non-zero when any are open. With no",
|
|
359
|
-
"<mode>/<file>, lists code-comment threads in the files the",
|
|
360
|
-
"current process changed (read from its diff base, unlike",
|
|
361
|
-
"the rest of `gtd check`)",
|
|
362
|
-
],
|
|
322
|
+
help: "Instead of <mode>'s findings, list the open threads in <file>; exits 1 when any are open. Alone, lists the open code-comment threads in the files the current process changed",
|
|
363
323
|
},
|
|
364
324
|
{
|
|
365
325
|
name: "--verbose",
|
|
@@ -369,11 +329,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
369
329
|
decode: () => Either.right(true),
|
|
370
330
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
371
331
|
valueHint: "",
|
|
372
|
-
help:
|
|
373
|
-
"enable stderr narration for this invocation: which rest",
|
|
374
|
-
"resolved and how config resolved across layers. Aliased",
|
|
375
|
-
"to -v",
|
|
376
|
-
],
|
|
332
|
+
help: "Narrate on stderr what gtd resolved and where its config came from",
|
|
377
333
|
},
|
|
378
334
|
]
|
|
379
335
|
|
|
@@ -404,451 +360,372 @@ type Arity =
|
|
|
404
360
|
| { readonly names: readonly [string, string] }
|
|
405
361
|
// One required leading positional, then any number the command itself judges.
|
|
406
362
|
| { readonly name: string; readonly variadic: string }
|
|
363
|
+
// The same, the leading one optional too.
|
|
364
|
+
| { readonly name: string; readonly variadic: string; readonly bare: true }
|
|
407
365
|
// At most one trailing positional.
|
|
408
366
|
| { readonly name: string; readonly optional: true }
|
|
409
367
|
// Any number of positionals, none included.
|
|
410
368
|
| { readonly name: string; readonly many: true }
|
|
411
369
|
|
|
370
|
+
type Group = "Core loop" | "Start and stop" | "Inspect" | "Driver plumbing"
|
|
371
|
+
|
|
412
372
|
interface CommandRow {
|
|
413
373
|
readonly token: string
|
|
414
374
|
readonly kind: Command["kind"]
|
|
415
375
|
readonly arity: Arity
|
|
416
|
-
readonly
|
|
376
|
+
readonly group: Group
|
|
377
|
+
/** The top-level help's one line — short enough for its column. */
|
|
378
|
+
readonly summary: string
|
|
379
|
+
/** The command's own help: prose paragraphs, wrapped when rendered (see `wrap`). */
|
|
380
|
+
readonly details: string
|
|
417
381
|
}
|
|
418
382
|
|
|
419
383
|
const COMMAND_ROWS: readonly CommandRow[] = [
|
|
384
|
+
{
|
|
385
|
+
token: "next",
|
|
386
|
+
kind: "next",
|
|
387
|
+
arity: "none",
|
|
388
|
+
group: "Core loop",
|
|
389
|
+
summary: "Print what should happen next",
|
|
390
|
+
details: `Print what should happen next in the process, changing nothing — safe to poll.
|
|
391
|
+
|
|
392
|
+
Plain output is for a person: a status header (the step, who acts, the pending changes, where landing them would go), then the step's content. At a prompt the header goes to stderr, so stdout is exactly the agent's input.
|
|
393
|
+
|
|
394
|
+
--json prints one JSON document for a driver: its kind (capture, message, script, prompt or stalled) says what to do, its content what to run or show. --json=<path> prints one field of it; an unknown path is a usage error (exit 2).`,
|
|
395
|
+
},
|
|
420
396
|
{
|
|
421
397
|
token: "land",
|
|
422
398
|
kind: "land",
|
|
423
399
|
arity: "none",
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
400
|
+
group: "Core loop",
|
|
401
|
+
summary: "Record what the tree now shows",
|
|
402
|
+
details: `Record what the working tree now shows at the current step: a person's edit, an agent's or a check's turn, or an empty attempt when an agent changed nothing.
|
|
403
|
+
|
|
404
|
+
gtd writes nothing itself. Plain output names the commit it would make; --json=script prints the script that makes it:
|
|
405
|
+
|
|
406
|
+
s=$(gtd land --json=script) && sh -c "$s"
|
|
407
|
+
|
|
408
|
+
--json prints the whole document: the script, the step the process lands at, the commit subject, and the recorded cost and model. A refusal exits 1.`,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
token: "validate",
|
|
412
|
+
kind: "validate",
|
|
413
|
+
arity: "none",
|
|
414
|
+
group: "Core loop",
|
|
415
|
+
summary: "Print the script that checks the steering file",
|
|
416
|
+
details: `Print the script that formats, then validates, the current step's steering file with its mode's commands. gtd runs nothing: the script's exit code is the verdict, and on a failure it prints a ready-to-send fix prompt. Prints "nothing to validate" when the step declares no file to check; --json=script then prints an empty script.`,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
token: "start",
|
|
420
|
+
kind: "start",
|
|
421
|
+
arity: { name: "name", variadic: "args", bare: true },
|
|
422
|
+
group: "Start and stop",
|
|
423
|
+
summary: "Start a workflow or door; bare, list them",
|
|
424
|
+
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
425
|
+
|
|
426
|
+
<name> is a door — a named shortcut whose positional args set the workflow's settings — or else a workflow the config exports, which takes no args. A door shadows a workflow of the same name. --var pins a setting either way.
|
|
427
|
+
|
|
428
|
+
s=$(gtd start <name> --json=script) && sh -c "$s"
|
|
429
|
+
|
|
430
|
+
With no <name>, list every workflow and door it can start. An unknown <name>, the wrong number of args, or a --var the workflow does not declare is a usage error (exit 2); a process already underway is a refusal (exit 1).`,
|
|
441
431
|
},
|
|
442
432
|
{
|
|
443
433
|
token: "abandon",
|
|
444
434
|
kind: "abandon",
|
|
445
435
|
arity: "none",
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
],
|
|
436
|
+
group: "Start and stop",
|
|
437
|
+
summary: "Stop the process, keeping its changes",
|
|
438
|
+
details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from and keeps everything it produced as uncommitted changes; \`gtd restore\` undoes it. With no process underway the script does nothing.
|
|
439
|
+
|
|
440
|
+
s=$(gtd abandon --json=script) && sh -c "$s"`,
|
|
452
441
|
},
|
|
453
442
|
{
|
|
454
443
|
token: "restore",
|
|
455
444
|
kind: "restore",
|
|
456
445
|
arity: "none",
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"abandoned process's turns. Refuses on a dirty working",
|
|
461
|
-
"tree, when there is no retained history, or when HEAD has",
|
|
462
|
-
"advanced past the retained tip with commits that would be",
|
|
463
|
-
"lost",
|
|
464
|
-
],
|
|
446
|
+
group: "Start and stop",
|
|
447
|
+
summary: "Bring back the last abandoned process",
|
|
448
|
+
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script resets HEAD to that process's last commit. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`,
|
|
465
449
|
},
|
|
466
450
|
{
|
|
467
|
-
token: "
|
|
468
|
-
kind: "
|
|
451
|
+
token: "summary",
|
|
452
|
+
kind: "summary",
|
|
469
453
|
arity: "none",
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"script:' / 'The edit is already made — run `gtd land` to",
|
|
476
|
-
"land it.') precedes the status summary; at a prompt rest",
|
|
477
|
-
"it's the bare step (plus the self-validation instruction",
|
|
478
|
-
"when applicable) with no header at all, since those bytes",
|
|
479
|
-
"are the agent's own input. --json emits the one structured",
|
|
480
|
-
"surface gtd has: kind (capture|message|script|prompt|",
|
|
481
|
-
"stalled) selects what a driver does, content is what it",
|
|
482
|
-
"runs or shows, idle marks the workflow's initial state with",
|
|
483
|
-
"a clean tree, plus the prompt session, model, validate",
|
|
484
|
-
"script, log path, changes, next and the resting state's own",
|
|
485
|
-
"fields. --json=<path> (a dotted key path into that same",
|
|
486
|
-
"document, e.g. kind, content, session.id) prints just that",
|
|
487
|
-
"value instead of the whole document — see --json's own help",
|
|
488
|
-
"above. Exits 0 — see the Exit codes section below",
|
|
489
|
-
],
|
|
454
|
+
group: "Inspect",
|
|
455
|
+
summary: "Print the prompt for the closing message",
|
|
456
|
+
details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside: its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
|
|
457
|
+
|
|
458
|
+
Refuses when the workflow declares no summary template, or when the process has no commits.`,
|
|
490
459
|
},
|
|
491
460
|
{
|
|
492
|
-
token: "
|
|
493
|
-
kind: "
|
|
461
|
+
token: "base",
|
|
462
|
+
kind: "base",
|
|
494
463
|
arity: "none",
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
"mode's commands (its file:/mode:), instead of running it —",
|
|
499
|
-
"a driver runs the script and reads the findings from its",
|
|
500
|
-
'own exit code/output. Always exits 0; prints "nothing to',
|
|
501
|
-
'validate" when the resolved rest declares nothing to run.',
|
|
502
|
-
"On a non-zero validate exit the emitted script prints a",
|
|
503
|
-
"ready-to-send fix prompt (instruction + findings) and",
|
|
504
|
-
"exits with the validator's own code",
|
|
505
|
-
],
|
|
464
|
+
group: "Inspect",
|
|
465
|
+
summary: "Print the commit a review diff starts from",
|
|
466
|
+
details: `Print the commit a review diff should start from, bare: the process's diff base before the first review round, the latest round's boundary after it. Writes nothing; refuses when no process is underway.`,
|
|
506
467
|
},
|
|
507
468
|
{
|
|
508
|
-
token: "
|
|
509
|
-
kind: "
|
|
510
|
-
arity: "
|
|
511
|
-
|
|
469
|
+
token: "strays",
|
|
470
|
+
kind: "strays",
|
|
471
|
+
arity: { name: "range", variadic: "glob" },
|
|
472
|
+
group: "Inspect",
|
|
473
|
+
summary: "List changed files outside the globs",
|
|
474
|
+
details: `List the files changed in <range> (<base>..<tip>, an empty side is HEAD) that match none of the globs. Globs are anchored at the repository root: * stays within one path segment, ** crosses them, everything else is literal — quote them in the shell.
|
|
475
|
+
|
|
476
|
+
Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose adds one stray: line per path. Runs in any git repository, no process needed. Exits 10 when strays were found, 1 on an unknown revision, 2 on a bad range.`,
|
|
512
477
|
},
|
|
513
478
|
{
|
|
514
|
-
token: "
|
|
515
|
-
kind: "
|
|
479
|
+
token: "history list",
|
|
480
|
+
kind: "historyList",
|
|
516
481
|
arity: "none",
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
"refuses outside a repository like every other state command.",
|
|
521
|
-
"By default, publishes through `tailscale serve` (reachable",
|
|
522
|
-
"from anywhere on the tailnet, including over a DERP relay)",
|
|
523
|
-
"with tailscaled terminating TLS, walking 8443, then 10000,",
|
|
524
|
-
"then 443 until one publishes; falls back to binding a local",
|
|
525
|
-
"HTTPS server directly, never refusing, when none of the",
|
|
526
|
-
"three works. --host <addr> opts out of serve and binds that",
|
|
527
|
-
"address directly instead; --port <n> overrides the serve",
|
|
528
|
-
"port (default: the first free of 8443, 10000, 443), or the",
|
|
529
|
-
"bind port when --host is given (default: a free port);",
|
|
530
|
-
"--self-signed also opts out of serve, generating a",
|
|
531
|
-
"throwaway TLS certificate instead of the configured",
|
|
532
|
-
"ui.cert/ui.key; --dev runs against local development sources",
|
|
533
|
-
"instead of the packaged build",
|
|
534
|
-
],
|
|
482
|
+
group: "Inspect",
|
|
483
|
+
summary: "List finished processes",
|
|
484
|
+
details: `List the finished processes this clone keeps, newest first: entry, branch, tip, date and subject. --json prints an array of { ref, entry, branch, tip, date, subject }. Writes nothing; with none kept it suggests \`gtd history fetch\`.`,
|
|
535
485
|
},
|
|
536
486
|
{
|
|
537
|
-
token: "
|
|
538
|
-
kind: "
|
|
539
|
-
arity: {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
"over its contents, printing each finding one per line and",
|
|
544
|
-
"exiting non-zero when there are any. Resolves no workflow",
|
|
545
|
-
"state and reads no config — standalone, runnable from any",
|
|
546
|
-
"directory with <mode>/<file> given explicitly. This is what a",
|
|
547
|
-
"workflow's emitted validation script invokes as a leaf step.",
|
|
548
|
-
"--open-questions runs the qa unanswered-questions predicate",
|
|
549
|
-
"instead (see --help); --open-threads lists open threads.",
|
|
550
|
-
"`gtd check --open-threads` alone lists open code-comment",
|
|
551
|
-
"threads (`// H: ...`) in the files the process changed;",
|
|
552
|
-
"it prints nothing when no process is underway.",
|
|
553
|
-
],
|
|
487
|
+
token: "history show",
|
|
488
|
+
kind: "historyShow",
|
|
489
|
+
arity: { name: "ref" },
|
|
490
|
+
group: "Inspect",
|
|
491
|
+
summary: "Replay one finished process",
|
|
492
|
+
details: `Replay one finished process under today's config and print its steps, judge verdicts and approvals. <ref> is a full ref, an entry hash or a unique prefix of one, or a branch name. Refuses when a config file changed since; a different gtd version only warns. --json prints the trail as one document. Writes nothing.`,
|
|
554
493
|
},
|
|
555
494
|
{
|
|
556
|
-
token: "
|
|
557
|
-
kind: "
|
|
558
|
-
arity: { name: "
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
"only when the bytes actually changed. Resolves no workflow",
|
|
563
|
-
"state and reads no config — standalone, runnable from any",
|
|
564
|
-
"directory with <file> given explicitly. This is what the",
|
|
565
|
-
"landing script runs ahead of the commit at the human review",
|
|
566
|
-
"gate, so no tick ever reaches a commit. Takes no <mode> —",
|
|
567
|
-
"it means review-mode file pointers and nothing else, never",
|
|
568
|
-
"qa-mode's answered-question boxes. A missing file writes",
|
|
569
|
-
"nothing and exits 0",
|
|
570
|
-
],
|
|
495
|
+
token: "history metrics",
|
|
496
|
+
kind: "historyMetrics",
|
|
497
|
+
arity: { name: "ref", many: true },
|
|
498
|
+
group: "Inspect",
|
|
499
|
+
summary: "Report cost and rework of finished processes",
|
|
500
|
+
details: `Report how finished processes went: builds that landed first time or were rebuilt, cost by model and phase, review findings per lens, and red checks that passed when re-run — per process and in total. <ref> is read as by \`gtd history show\`, but a branch name means every process kept from it; no <ref> means all of them. Never replays, so a changed config is no reason to refuse. --json prints { processes, total }. Writes nothing.`,
|
|
571
501
|
},
|
|
572
502
|
{
|
|
573
|
-
token: "
|
|
574
|
-
kind: "
|
|
575
|
-
arity: "
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"runtime — the self-serve version of",
|
|
580
|
-
"https://github.com/pmelab/gtd/blob/main/docs/driver.md's",
|
|
581
|
-
"'Writing your own driver', and how to get a workflow: gtd",
|
|
582
|
-
"ships none. It writes nothing.",
|
|
583
|
-
],
|
|
503
|
+
token: "history judges",
|
|
504
|
+
kind: "historyJudges",
|
|
505
|
+
arity: { name: "ref", many: true },
|
|
506
|
+
group: "Inspect",
|
|
507
|
+
summary: "Calibrate the judges of finished processes",
|
|
508
|
+
details: `Calibrate the judges of finished processes from their recorded verdicts: per judge and confidence bucket, how many verdicts later events overturned. <ref> is read as by \`gtd history metrics\`. Never replays and asks no model. --json prints { processes, total, whatIf }. Writes nothing.`,
|
|
584
509
|
},
|
|
585
510
|
{
|
|
586
|
-
token: "
|
|
587
|
-
kind: "
|
|
588
|
-
arity: "
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
"it.processCost/processCostByModel. Writes nothing: no git,",
|
|
595
|
-
"no state transition, no file, no session identity — the",
|
|
596
|
-
"driver pipes the output to a cold agent and does what it",
|
|
597
|
-
"wants with the result (a squash, an amend, a PR body).",
|
|
598
|
-
"Refuses (exit 1) when the workflow declares no summary:",
|
|
599
|
-
"template, or when the resolved run has no commits to name —",
|
|
600
|
-
"it is runnable any time before the next thing lands on the",
|
|
601
|
-
"branch",
|
|
602
|
-
],
|
|
511
|
+
token: "history fetch",
|
|
512
|
+
kind: "historyFetch",
|
|
513
|
+
arity: { name: "remote", optional: true },
|
|
514
|
+
group: "Inspect",
|
|
515
|
+
summary: "Print a script that fetches finished processes",
|
|
516
|
+
details: `Print a script that fetches every finished process kept on <remote> (default: origin) and reports how many arrived. gtd runs no git itself:
|
|
517
|
+
|
|
518
|
+
s=$(gtd history fetch --json=script) && sh -c "$s"`,
|
|
603
519
|
},
|
|
604
520
|
{
|
|
605
|
-
token: "
|
|
606
|
-
kind: "
|
|
521
|
+
token: "ui",
|
|
522
|
+
kind: "ui",
|
|
607
523
|
arity: "none",
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
"bare, newline-terminated, and nothing else. Writes nothing:",
|
|
612
|
-
"no git, no state transition, no session identity. Before",
|
|
613
|
-
"the first review round it's the process's diff base;",
|
|
614
|
-
"afterward it's the most-recent review round's boundary.",
|
|
615
|
-
"Refuses (exit 1) when no process is underway.",
|
|
616
|
-
],
|
|
524
|
+
group: "Inspect",
|
|
525
|
+
summary: "Serve the web and phone client",
|
|
526
|
+
details: `Serve gtd's web and phone client for this worktree. By default it publishes through \`tailscale serve\`, reachable from anywhere on the tailnet, on the first of ports 8443, 10000 and 443 that works; when none does it serves HTTPS locally instead of refusing.`,
|
|
617
527
|
},
|
|
618
528
|
{
|
|
619
|
-
token: "
|
|
620
|
-
kind: "
|
|
621
|
-
arity: { name: "name", variadic: "args" },
|
|
622
|
-
details: [
|
|
623
|
-
"Start a process through a named shortcut, mapping the",
|
|
624
|
-
"positional args to that workflow's process settings — the",
|
|
625
|
-
"same start `gtd --workflow` makes, printed the same way.",
|
|
626
|
-
"Each workflow the config exports brings its own doors (gtd's",
|
|
627
|
-
"`fix`, `review [base]`, `rollout <flag>` and",
|
|
628
|
-
"`cleanup <flag>`), plus the config's `doors` export. An",
|
|
629
|
-
"unknown door, or too few or too many args, is a usage error",
|
|
630
|
-
"(exit 2) naming the doors or the door's synopsis",
|
|
631
|
-
],
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
token: "doors",
|
|
635
|
-
kind: "doors",
|
|
529
|
+
token: "guide",
|
|
530
|
+
kind: "guide",
|
|
636
531
|
arity: "none",
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
"args: [{ name, optional }] } instead; --json=<path> reads one",
|
|
641
|
-
"value off it. Writes nothing",
|
|
642
|
-
],
|
|
532
|
+
group: "Driver plumbing",
|
|
533
|
+
summary: "Print a briefing for writing a driver",
|
|
534
|
+
details: `Print a self-contained briefing that teaches an agent or a person to build a gtd driver in any shell or runtime, and where to get a workflow — gtd ships none. Writes nothing.`,
|
|
643
535
|
},
|
|
644
536
|
{
|
|
645
537
|
token: "judge",
|
|
646
538
|
kind: "judge",
|
|
647
539
|
arity: "none",
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"judge field `gtd next --json` already carries. Read-only:",
|
|
652
|
-
"resolves no session, writes nothing. Refuses (exit 1) when",
|
|
653
|
-
"the resolved rest declares no judge:",
|
|
654
|
-
],
|
|
540
|
+
group: "Driver plumbing",
|
|
541
|
+
summary: "Print the pending judgment",
|
|
542
|
+
details: `Print the current step's pending judgment: the prepared state, its questions and their criteria — the judge field \`gtd next --json\` carries. Writes nothing; refuses when the step declares no judge.`,
|
|
655
543
|
},
|
|
656
544
|
{
|
|
657
545
|
token: "judge answer",
|
|
658
546
|
kind: "judgeAnswer",
|
|
659
547
|
arity: "none",
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
"against an Effect Schema built from those same question",
|
|
664
|
-
"ids. Refuses (exit 1) when the resolved rest declares no",
|
|
665
|
-
"judge:. Exits 2 (a usage error, like an unknown --json",
|
|
666
|
-
"selector) when stdin isn't valid JSON or the verdict",
|
|
667
|
-
"doesn't decode against the pending question ids.",
|
|
668
|
-
],
|
|
548
|
+
group: "Driver plumbing",
|
|
549
|
+
summary: "Land a verdict read from stdin",
|
|
550
|
+
details: `Read a verdict from stdin — one { id, answer, p } entry per pending question — and land it the way \`gtd land\` does: plain output names the commit, --json=script prints the script. Refuses when the step declares no judge; a verdict that is not JSON, or names a question that was not asked, is a usage error (exit 2).`,
|
|
669
551
|
},
|
|
670
552
|
{
|
|
671
553
|
token: "judge run",
|
|
672
554
|
kind: "judgeRun",
|
|
673
555
|
arity: "none",
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
"does not cover are left out. --provider jev asks TypeSafe",
|
|
680
|
-
"(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
|
|
681
|
-
"on PATH (Claude Code only), model sonnet unless --model.",
|
|
682
|
-
"No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
|
|
683
|
-
"judge.json, else jev when TYPESAFE_API_KEY is set and no",
|
|
684
|
-
"model is given, else llm; --model likewise falls back to",
|
|
685
|
-
"GTD_JUDGE_MODEL, then the file's model. Needs no",
|
|
686
|
-
"repository. jev and llm exit 1 when they cannot answer",
|
|
687
|
-
"every question, with nothing on stdout",
|
|
688
|
-
],
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
token: "strays",
|
|
692
|
-
kind: "strays",
|
|
693
|
-
arity: { name: "range", variadic: "glob" },
|
|
694
|
-
details: [
|
|
695
|
-
"List the files <range> (<base>..<tip>, an empty side is HEAD)",
|
|
696
|
-
"touched in its committed net diff that match none of the",
|
|
697
|
-
"globs. Anchored at the repo root: * stays within one path",
|
|
698
|
-
"segment, ** crosses them, everything else is literal — quote",
|
|
699
|
-
"globs in the shell. stdout: a sorted JSON array, [] when",
|
|
700
|
-
"clean; stderr: a summary, plus one stray: line per path with",
|
|
701
|
-
"--verbose. --json=<n> picks one entry, nothing past the end;",
|
|
702
|
-
"a symlink is a path like any other, never followed; a",
|
|
703
|
-
"submodule counts as its path, whatever diff.ignoreSubmodules",
|
|
704
|
-
"or .gitmodules ignore say. Runs in any git repository, no",
|
|
705
|
-
"process needed. Exits 10 when",
|
|
706
|
-
"strays were found, 1 on an unknown revision, 2 on a bad range",
|
|
707
|
-
],
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
token: "history list",
|
|
711
|
-
kind: "historyList",
|
|
712
|
-
arity: "none",
|
|
713
|
-
details: [
|
|
714
|
-
"List the finished processes kept behind refs/gtd/history/*,",
|
|
715
|
-
"newest first: entry, branch, tip, date and subject per line.",
|
|
716
|
-
"--json prints one array of { ref, entry, branch, tip, date,",
|
|
717
|
-
"subject }. Writes nothing and never replays; with no refs it",
|
|
718
|
-
"hints at `gtd history fetch | sh` and still exits 0",
|
|
719
|
-
],
|
|
720
|
-
},
|
|
721
|
-
{
|
|
722
|
-
token: "history show",
|
|
723
|
-
kind: "historyShow",
|
|
724
|
-
arity: { name: "ref" },
|
|
725
|
-
details: [
|
|
726
|
-
"Replay one kept process under today's config and print its",
|
|
727
|
-
"steps, judge verdicts and approvals. <ref> is a full ref, an",
|
|
728
|
-
"entry hash (or unique prefix) or a branch name. Refuses",
|
|
729
|
-
"(exit 1) when a config file differs from its content at the",
|
|
730
|
-
"ref's tip. A version mismatch only warns on stderr. --json",
|
|
731
|
-
"prints the trail as one document; writes nothing",
|
|
732
|
-
],
|
|
556
|
+
group: "Driver plumbing",
|
|
557
|
+
summary: "Answer a judgment with a model",
|
|
558
|
+
details: `Answer a judgment: read the \`gtd judge --json\` document from stdin and print the verdict [{ id, answer, p }] \`gtd judge answer\` reads. Needs no repository.
|
|
559
|
+
|
|
560
|
+
The fixed answerer reads --answers and leaves out the questions it does not cover; jev asks TypeSafe ($TYPESAFE_API_KEY); llm asks the \`claude\` CLI on PATH. jev and llm exit 1, printing nothing, when they cannot answer every question. A stdin that is not a judgment document is a usage error (exit 2).`,
|
|
733
561
|
},
|
|
734
562
|
{
|
|
735
|
-
token: "
|
|
736
|
-
kind: "
|
|
737
|
-
arity: {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
"model and phase with the double work of a rebuild, quality-lap",
|
|
742
|
-
"findings per lens, and red checks that passed on their",
|
|
743
|
-
"re-check — per process and in total. Each <ref>",
|
|
744
|
-
"is read as by history show, but a branch name means every",
|
|
745
|
-
"process kept from it; none means every ref.",
|
|
746
|
-
"Reads commit subjects and trailers, never replays, so a",
|
|
747
|
-
"changed config is no refusal. --json prints { processes, total",
|
|
748
|
-
"} as one document; writes nothing",
|
|
749
|
-
],
|
|
563
|
+
token: "check",
|
|
564
|
+
kind: "check",
|
|
565
|
+
arity: { names: ["mode", "file"] },
|
|
566
|
+
group: "Driver plumbing",
|
|
567
|
+
summary: "Check a steering file's format",
|
|
568
|
+
details: `Run the steering format <mode> (qa, review or findings) over <file> and print each finding on a line of its own; exits 1 when there are any, or when <file> does not exist. An unknown <mode> is a usage error (exit 2). Reads no workflow and runs from any directory — emitted validation scripts call it.`,
|
|
750
569
|
},
|
|
751
570
|
{
|
|
752
|
-
token: "
|
|
753
|
-
kind: "
|
|
754
|
-
arity: { name: "
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
"and p bucket, how many verdicts later events overturned.",
|
|
759
|
-
"--var <name>=<value> re-decides every past verdict under",
|
|
760
|
-
"another threshold and prints what would have changed;",
|
|
761
|
-
"--jsonl prints one { process, judge, commit, step, question,",
|
|
762
|
-
"state, answer, p, outcome } line per verdict instead. <ref>",
|
|
763
|
-
"is read as by history metrics. Never replays and asks no",
|
|
764
|
-
"model. --json prints { processes, total, whatIf }; writes",
|
|
765
|
-
"nothing",
|
|
766
|
-
],
|
|
571
|
+
token: "uncheck",
|
|
572
|
+
kind: "uncheck",
|
|
573
|
+
arity: { name: "file" },
|
|
574
|
+
group: "Driver plumbing",
|
|
575
|
+
summary: "Untick a review file's boxes",
|
|
576
|
+
details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it, so no tick is ever committed.`,
|
|
767
577
|
},
|
|
768
578
|
{
|
|
769
|
-
token: "
|
|
770
|
-
kind: "
|
|
771
|
-
arity:
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
"it with `gtd history fetch | sh`. Never runs git itself",
|
|
776
|
-
],
|
|
579
|
+
token: "lsp",
|
|
580
|
+
kind: "lsp",
|
|
581
|
+
arity: "none",
|
|
582
|
+
group: "Driver plumbing",
|
|
583
|
+
summary: "Start the steering-file language server",
|
|
584
|
+
details: `Start the language server for .gtd/ steering files on stdio.`,
|
|
777
585
|
},
|
|
778
586
|
]
|
|
779
587
|
|
|
780
588
|
const commandByToken = (token: string): CommandRow | undefined =>
|
|
781
589
|
COMMAND_ROWS.find((r) => r.token === token)
|
|
782
590
|
|
|
591
|
+
/** Every word that may follow `gtd`, for "did you mean". */
|
|
592
|
+
const COMMAND_WORDS: readonly string[] = [
|
|
593
|
+
...new Set([...COMMAND_ROWS.map((r) => r.token.split(" ")[0]!), "help", "version"]),
|
|
594
|
+
]
|
|
595
|
+
|
|
783
596
|
// ---------------------------------------------------------------------------
|
|
784
597
|
// Help rendering
|
|
785
598
|
// ---------------------------------------------------------------------------
|
|
786
599
|
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
600
|
+
const TOP_COLUMN = 28
|
|
601
|
+
const OPTION_COLUMN = 22
|
|
602
|
+
|
|
603
|
+
const synopsis = (row: CommandRow): string => {
|
|
604
|
+
const { token, arity } = row
|
|
605
|
+
if (arity === "none") return token
|
|
606
|
+
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`
|
|
607
|
+
if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`
|
|
608
|
+
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`
|
|
609
|
+
if ("optional" in arity) return `${token} [${arity.name}]`
|
|
610
|
+
if ("many" in arity) return `${token} [${arity.name}...]`
|
|
611
|
+
return `${token} <${arity.name}>`
|
|
797
612
|
}
|
|
798
613
|
|
|
799
|
-
const
|
|
800
|
-
"Starts a new process authenticated as human, e.g.",
|
|
801
|
-
"'gtd --workflow <name>'",
|
|
802
|
-
])
|
|
803
|
-
|
|
804
|
-
const VERSION_BLOCK = renderBlock("version", ["Print version and exit"])
|
|
805
|
-
const HELP_BLOCK = renderBlock("help", ["Print this help and exit"])
|
|
806
|
-
|
|
807
|
-
const SPACE_FORM_FLAGS = new Set(["--workflow", "--var"])
|
|
614
|
+
const SPACE_FORM_FLAGS = new Set(["--var", "--threshold"])
|
|
808
615
|
|
|
809
616
|
const flagHeader = (row: FlagRow): string => {
|
|
810
617
|
if (row.valueHint === "") return row.name
|
|
618
|
+
if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`
|
|
811
619
|
return SPACE_FORM_FLAGS.has(row.name)
|
|
812
620
|
? `${row.name} ${row.valueHint}`
|
|
813
621
|
: `${row.name}=${row.valueHint}`
|
|
814
622
|
}
|
|
815
623
|
|
|
624
|
+
const GLOBAL_OPTIONS: readonly (readonly [string, string])[] = [
|
|
625
|
+
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
626
|
+
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
627
|
+
["-V, --version", "Print the version"],
|
|
628
|
+
["--", "End the options: every word after it is an argument"],
|
|
629
|
+
]
|
|
630
|
+
|
|
631
|
+
const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
632
|
+
[
|
|
633
|
+
"help [command]",
|
|
634
|
+
"Print help for gtd or one command",
|
|
635
|
+
"Print gtd's overview, or with <command> that command's own help.",
|
|
636
|
+
],
|
|
637
|
+
["version", "Print the version", "Print gtd's version."],
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
const EXAMPLES = ` $ gtd start list what can start
|
|
641
|
+
$ gtd start <name> start a process
|
|
642
|
+
$ gtd next see what happens next
|
|
643
|
+
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
644
|
+
$ gtd help land one command's details`
|
|
645
|
+
|
|
646
|
+
const lines = (block: readonly string[]): string => block.map((l) => `${l}\n`).join("")
|
|
647
|
+
|
|
648
|
+
const GROUPS: readonly Group[] = ["Core loop", "Start and stop", "Inspect", "Driver plumbing"]
|
|
649
|
+
|
|
816
650
|
export const renderHelp = (): string => {
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
? `${row.token} [${row.arity.name}]`
|
|
827
|
-
: "many" in row.arity
|
|
828
|
-
? `${row.token} [${row.arity.name}...]`
|
|
829
|
-
: `${row.token} <${row.arity.name}>`
|
|
830
|
-
return renderBlock(header, row.details)
|
|
651
|
+
const groups = GROUPS.map((group) => {
|
|
652
|
+
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) =>
|
|
653
|
+
definition(synopsis(r), r.summary, TOP_COLUMN),
|
|
654
|
+
)
|
|
655
|
+
const extra =
|
|
656
|
+
group === "Driver plumbing"
|
|
657
|
+
? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN))
|
|
658
|
+
: []
|
|
659
|
+
return `${group}:\n${lines([...rows, ...extra])}`
|
|
831
660
|
})
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
661
|
+
return (
|
|
662
|
+
"Usage: gtd <command> [options]\n\n" +
|
|
663
|
+
lines(
|
|
664
|
+
wrap(
|
|
665
|
+
"gtd reads a repository's git state and prints what should happen next in its workflow. It never acts on it: a driver, or you, runs what it prints.",
|
|
666
|
+
),
|
|
667
|
+
) +
|
|
668
|
+
"\n" +
|
|
669
|
+
groups.join("\n") +
|
|
670
|
+
"\nOptions:\n" +
|
|
671
|
+
lines(GLOBAL_OPTIONS.flatMap(([term, text]) => definition(term, text, TOP_COLUMN))) +
|
|
672
|
+
`\nExamples:\n${EXAMPLES}\n\n` +
|
|
673
|
+
lines(
|
|
674
|
+
wrap(
|
|
675
|
+
"Run `gtd help <command>` for a command's details and options, and `gtd guide` to learn how to drive gtd.",
|
|
676
|
+
),
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
682
|
+
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
844
683
|
|
|
684
|
+
gtd start <name> start a process
|
|
685
|
+
gtd next print what should happen next
|
|
686
|
+
gtd land record what the tree now shows
|
|
687
|
+
|
|
688
|
+
`
|
|
689
|
+
|
|
690
|
+
const renderCommandHelp = (row: CommandRow): string => {
|
|
691
|
+
const options = FLAGS.filter((f) => f.name !== "--verbose" && f.scope(row.kind)).flatMap((f) =>
|
|
692
|
+
definition(flagHeader(f), f.helpFor?.[row.kind] ?? f.help, OPTION_COLUMN),
|
|
693
|
+
)
|
|
694
|
+
const global = definition("-v, --verbose", "Narrate on stderr what gtd resolved", OPTION_COLUMN)
|
|
845
695
|
return (
|
|
846
|
-
`Usage: gtd
|
|
847
|
-
|
|
848
|
-
|
|
696
|
+
`Usage: gtd ${synopsis(row)}${options.length > 0 ? " [options]" : ""}\n\n` +
|
|
697
|
+
lines(wrap(row.details)) +
|
|
698
|
+
`\nOptions:\n${lines([...options, ...global])}`
|
|
849
699
|
)
|
|
850
700
|
}
|
|
851
701
|
|
|
702
|
+
/** `gtd help <topic…>`'s text: one command's help, every `history` subcommand's for `history`, or undefined for an unknown topic. */
|
|
703
|
+
const renderTopicHelp = (topic: readonly string[]): string | undefined => {
|
|
704
|
+
const two = commandByToken(topic.slice(0, 2).join(" "))
|
|
705
|
+
if (two !== undefined && topic.length >= 2) return renderCommandHelp(two)
|
|
706
|
+
const [first = ""] = topic
|
|
707
|
+
const one = commandByToken(first)
|
|
708
|
+
if (one !== undefined) return renderCommandHelp(one)
|
|
709
|
+
const extra = EXTRA_TOPICS.find(([term]) => term.split(" ")[0] === first)
|
|
710
|
+
if (extra !== undefined) return `Usage: gtd ${extra[0]}\n\n${lines(wrap(extra[2]))}`
|
|
711
|
+
const family = COMMAND_ROWS.filter((r) => r.token.startsWith(`${first} `))
|
|
712
|
+
if (first !== "" && family.length > 0) return family.map(renderCommandHelp).join("\n")
|
|
713
|
+
return undefined
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** A usage error's tail: the nearest candidate when `word` looks like a typo of one, and where the full list is. */
|
|
717
|
+
const didYouMean = (
|
|
718
|
+
word: string,
|
|
719
|
+
candidates: readonly string[],
|
|
720
|
+
prefix = "",
|
|
721
|
+
pointer = "`gtd help` for every command",
|
|
722
|
+
): string => {
|
|
723
|
+
const guess = closest(word, candidates)
|
|
724
|
+
return guess === undefined
|
|
725
|
+
? ` — run ${pointer}`
|
|
726
|
+
: ` — did you mean \`${prefix}${guess}\`? Run ${pointer}`
|
|
727
|
+
}
|
|
728
|
+
|
|
852
729
|
// ---------------------------------------------------------------------------
|
|
853
730
|
// Tokenizer
|
|
854
731
|
// ---------------------------------------------------------------------------
|
|
@@ -865,6 +742,17 @@ interface TokenizeOk {
|
|
|
865
742
|
readonly byFlag: ReadonlyMap<string, readonly string[]>
|
|
866
743
|
readonly present: ReadonlySet<string>
|
|
867
744
|
readonly jsonSeen: boolean
|
|
745
|
+
/** The positional right after a bare `--json` — most likely a path meant as `--json=<path>`. */
|
|
746
|
+
readonly afterBareJson?: string
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const unknownOption = (tok: string, jsonSeen: boolean): TokenizeError => {
|
|
750
|
+
const candidates = tok.startsWith("--") ? FLAGS.map((f) => f.name) : []
|
|
751
|
+
return {
|
|
752
|
+
kind: "error",
|
|
753
|
+
message: `gtd: unknown option '${tok}'${didYouMean(tok.split("=")[0]!, candidates, "", "`gtd help <command>` for its options")}`,
|
|
754
|
+
jsonSeen,
|
|
755
|
+
}
|
|
868
756
|
}
|
|
869
757
|
|
|
870
758
|
// fallow-ignore-next-line complexity
|
|
@@ -873,35 +761,27 @@ const tokenize = (tail: readonly string[]): TokenizeError | TokenizeOk => {
|
|
|
873
761
|
const byFlag = new Map<string, string[]>()
|
|
874
762
|
const present = new Set<string>()
|
|
875
763
|
let jsonSeen = false
|
|
764
|
+
let afterBareJson: string | undefined
|
|
765
|
+
const end = tail.indexOf("--")
|
|
876
766
|
|
|
877
767
|
for (let i = 0; i < tail.length; i++) {
|
|
878
768
|
const original = tail[i]!
|
|
769
|
+
if (i === end) continue
|
|
879
770
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original
|
|
880
|
-
if (!tok.startsWith("
|
|
771
|
+
if ((end !== -1 && i > end) || !tok.startsWith("-") || tok === "-") {
|
|
772
|
+
if (tail[i - 1] === "--json") afterBareJson ??= original
|
|
881
773
|
positionals.push(original)
|
|
882
774
|
continue
|
|
883
775
|
}
|
|
884
|
-
const row = flagByToken(tok)
|
|
885
|
-
if (row === undefined)
|
|
886
|
-
return {
|
|
887
|
-
kind: "error",
|
|
888
|
-
message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
|
|
889
|
-
jsonSeen,
|
|
890
|
-
}
|
|
891
|
-
}
|
|
776
|
+
const row = tok.startsWith("--") ? flagByToken(tok) : undefined
|
|
777
|
+
if (row === undefined) return unknownOption(tok, jsonSeen)
|
|
892
778
|
const eq = tok.indexOf("=")
|
|
893
779
|
const alreadyPresent = present.has(row.name)
|
|
894
780
|
present.add(row.name)
|
|
895
781
|
if (row.name === "--json") jsonSeen = true
|
|
896
782
|
|
|
897
783
|
if (row.arity === 0) {
|
|
898
|
-
if (eq !== -1)
|
|
899
|
-
return {
|
|
900
|
-
kind: "error",
|
|
901
|
-
message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
|
|
902
|
-
jsonSeen,
|
|
903
|
-
}
|
|
904
|
-
}
|
|
784
|
+
if (eq !== -1) return unknownOption(tok, jsonSeen)
|
|
905
785
|
if (!byFlag.has(row.name)) byFlag.set(row.name, [])
|
|
906
786
|
continue
|
|
907
787
|
}
|
|
@@ -959,7 +839,14 @@ const tokenize = (tail: readonly string[]): TokenizeError | TokenizeOk => {
|
|
|
959
839
|
byFlag.set(row.name, [...existing, raw])
|
|
960
840
|
}
|
|
961
841
|
|
|
962
|
-
return {
|
|
842
|
+
return {
|
|
843
|
+
kind: "ok",
|
|
844
|
+
positionals,
|
|
845
|
+
byFlag,
|
|
846
|
+
present,
|
|
847
|
+
jsonSeen,
|
|
848
|
+
...(afterBareJson !== undefined ? { afterBareJson } : {}),
|
|
849
|
+
}
|
|
963
850
|
}
|
|
964
851
|
|
|
965
852
|
// ---------------------------------------------------------------------------
|
|
@@ -1007,7 +894,7 @@ const tooMany = (
|
|
|
1007
894
|
const arityError = (cmd: string, rest: readonly string[], arity: Arity): string | undefined => {
|
|
1008
895
|
if (arity === "none") return tooMany(cmd, rest, "none", 0)
|
|
1009
896
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names)
|
|
1010
|
-
if ("many" in arity) return undefined
|
|
897
|
+
if ("many" in arity || "bare" in arity) return undefined
|
|
1011
898
|
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1)
|
|
1012
899
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`
|
|
1013
900
|
return "variadic" in arity ? undefined : tooMany(cmd, rest, `one ${arity.name}`, 1)
|
|
@@ -1044,6 +931,27 @@ const decodeFlags = (
|
|
|
1044
931
|
return Either.right(bag)
|
|
1045
932
|
}
|
|
1046
933
|
|
|
934
|
+
/** `gtd history judges`' thresholds: each a known judge threshold with a numeric value. */
|
|
935
|
+
const judgeThresholds = (
|
|
936
|
+
thresholds: Readonly<Record<string, string>>,
|
|
937
|
+
): Either.Either<Readonly<Record<string, string>>, string> => {
|
|
938
|
+
const unknown = Object.keys(thresholds).find((name) => !Object.hasOwn(THRESHOLDS, name))
|
|
939
|
+
if (unknown !== undefined) {
|
|
940
|
+
return Either.left(
|
|
941
|
+
`gtd history judges: --threshold ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
const nan = Object.entries(thresholds).find(
|
|
945
|
+
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
946
|
+
)
|
|
947
|
+
if (nan !== undefined) {
|
|
948
|
+
return Either.left(
|
|
949
|
+
`gtd history judges: --threshold ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
return Either.right(thresholds)
|
|
953
|
+
}
|
|
954
|
+
|
|
1047
955
|
/** Assembles a `land` `Command` from its already-scope-checked flag bag — split out of `parseArgv` so its two independent, omit-when-absent optional fields don't inflate that function's own branching. */
|
|
1048
956
|
const buildLandCommand = (bag: {
|
|
1049
957
|
readonly "--cost"?: number
|
|
@@ -1054,24 +962,52 @@ const buildLandCommand = (bag: {
|
|
|
1054
962
|
...(bag["--model"] !== undefined ? { model: bag["--model"] } : {}),
|
|
1055
963
|
})
|
|
1056
964
|
|
|
965
|
+
const HELP_FLAGS = new Set(["--help", "-h"])
|
|
966
|
+
|
|
967
|
+
/** The options part of argv: everything before a `--`, which ends the options. */
|
|
968
|
+
const optionsOf = (argv: readonly string[]): readonly string[] => {
|
|
969
|
+
const end = argv.indexOf("--", 2)
|
|
970
|
+
return end === -1 ? argv : argv.slice(0, end)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** `--help`/`-h` anywhere before `--`: the named command's own help, else the overview — never a usage error, so a typo'd command still gets help. */
|
|
974
|
+
const helpPlan = (argv: readonly string[]): CliPlan => {
|
|
975
|
+
const tokenized = tokenize(
|
|
976
|
+
optionsOf(argv)
|
|
977
|
+
.slice(2)
|
|
978
|
+
.filter((tok) => !HELP_FLAGS.has(tok)),
|
|
979
|
+
)
|
|
980
|
+
const topic = tokenized.kind === "ok" ? tokenized.positionals : []
|
|
981
|
+
return { kind: "output", stdout: renderTopicHelp(topic) ?? renderHelp() }
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** `gtd help [topic…]`: an unknown topic is a usage error naming the nearest command. */
|
|
985
|
+
const helpCommandPlan = (topic: readonly string[], json: boolean): CliPlan => {
|
|
986
|
+
if (topic.length === 0) return { kind: "output", stdout: renderHelp() }
|
|
987
|
+
const text = renderTopicHelp(topic)
|
|
988
|
+
if (text !== undefined) return { kind: "output", stdout: text }
|
|
989
|
+
return usagePlan(
|
|
990
|
+
`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0]!, COMMAND_WORDS, "gtd help ")}`,
|
|
991
|
+
json,
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
|
|
1057
995
|
// fallow-ignore-next-line complexity
|
|
1058
996
|
export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (
|
|
997
|
+
const options = optionsOf(argv)
|
|
998
|
+
if (options.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv)
|
|
999
|
+
if (options.includes("--version") || options.includes("-V")) {
|
|
1062
1000
|
return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
1063
1001
|
}
|
|
1064
1002
|
|
|
1065
1003
|
const tokenized = tokenize(argv.slice(2))
|
|
1066
1004
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen)
|
|
1067
|
-
const { positionals, byFlag, present, jsonSeen } = tokenized
|
|
1005
|
+
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized
|
|
1068
1006
|
|
|
1069
1007
|
const first = positionals[0]
|
|
1070
|
-
const workflowRaw = byFlag.get("--workflow")?.[0]
|
|
1071
|
-
const workflowPresent = workflowRaw !== undefined
|
|
1072
1008
|
|
|
1073
1009
|
if (first === "version") return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
1074
|
-
if (first === "help") return
|
|
1010
|
+
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen)
|
|
1075
1011
|
|
|
1076
1012
|
const isHistory = first === "history"
|
|
1077
1013
|
const row =
|
|
@@ -1080,8 +1016,14 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1080
1016
|
: commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first)
|
|
1081
1017
|
|
|
1082
1018
|
if (isHistory && row === undefined) {
|
|
1019
|
+
const subcommands = COMMAND_ROWS.filter((r) => r.token.startsWith("history ")).map(
|
|
1020
|
+
(r) => r.token.split(" ")[1]!,
|
|
1021
|
+
)
|
|
1022
|
+
const typo = closest(positionals[1] ?? "", subcommands)
|
|
1083
1023
|
return usagePlan(
|
|
1084
|
-
|
|
1024
|
+
typo !== undefined
|
|
1025
|
+
? `gtd history: unknown subcommand '${positionals[1]}' — did you mean \`gtd history ${typo}\`?`
|
|
1026
|
+
: "gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]",
|
|
1085
1027
|
jsonSeen,
|
|
1086
1028
|
)
|
|
1087
1029
|
}
|
|
@@ -1090,7 +1032,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1090
1032
|
// "answer" arriving as `positionals[1]`, a second token after "judge" —
|
|
1091
1033
|
// detected below via `judgeAnswer`, not through a second dispatchable
|
|
1092
1034
|
// `COMMAND_ROWS` row: the `"judge answer"`-tokened row above exists for
|
|
1093
|
-
//
|
|
1035
|
+
// its help. It CAN also be matched by
|
|
1094
1036
|
// `commandByToken` directly, but only if a caller passes the single
|
|
1095
1037
|
// (space-containing) argv token `"judge answer"` — an unusual shell quoting
|
|
1096
1038
|
// choice, not the normal two-argv-token invocation `gtd judge answer` — and
|
|
@@ -1100,65 +1042,42 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1100
1042
|
// `Arity` variant is needed for this one verb.
|
|
1101
1043
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer"
|
|
1102
1044
|
|
|
1103
|
-
// The `--workflow` selector: no command at all, with `--workflow` present,
|
|
1104
|
-
// resolves to the generic `start` command instead — landing and starting
|
|
1105
|
-
// are different verbs, so `gtd land --workflow <name>` is NOT a synonym (it
|
|
1106
|
-
// fails the scope check below instead).
|
|
1107
|
-
const selectsStart = workflowPresent && first === undefined
|
|
1108
1045
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run"
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
: judgeAnswer
|
|
1112
|
-
? "judgeAnswer"
|
|
1113
|
-
: judgeRun
|
|
1114
|
-
? "judgeRun"
|
|
1115
|
-
: row?.kind
|
|
1116
|
-
|
|
1117
|
-
if (row === undefined && !selectsStart) {
|
|
1046
|
+
|
|
1047
|
+
if (row === undefined || first === undefined) {
|
|
1118
1048
|
// No dispatchable row resolved (missing/unknown command) — a scoped flag
|
|
1119
|
-
// used here (e.g.
|
|
1120
|
-
//
|
|
1121
|
-
// the old parser, which validated --workflow/--var/--cost/--model ahead of
|
|
1122
|
-
// subcommand resolution).
|
|
1049
|
+
// used here (e.g. a bare `gtd --var x=1`) is a more specific error than
|
|
1050
|
+
// "missing command"/"unknown command", so it takes priority.
|
|
1123
1051
|
const violation = scopeViolation(undefined, present)
|
|
1124
1052
|
if (violation !== undefined) return usagePlan(violation, jsonSeen)
|
|
1125
1053
|
if (first === undefined) {
|
|
1126
1054
|
return usagePlan(
|
|
1127
|
-
"gtd: missing command — gtd
|
|
1128
|
-
"run `gtd install`, or copy one from " +
|
|
1129
|
-
"https://github.com/pmelab/gtd/blob/main/docs/driver.md's " +
|
|
1130
|
-
'"A complete minimal driver" section and run that, or see usage ' +
|
|
1131
|
-
"above (`gtd --help`)",
|
|
1055
|
+
"gtd: missing command — run `gtd help` for every command, or `gtd guide` to learn how to drive gtd",
|
|
1132
1056
|
jsonSeen,
|
|
1133
|
-
|
|
1057
|
+
SHORT_USAGE,
|
|
1134
1058
|
)
|
|
1135
1059
|
}
|
|
1136
|
-
return usagePlan(
|
|
1060
|
+
return usagePlan(
|
|
1061
|
+
`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`,
|
|
1062
|
+
jsonSeen,
|
|
1063
|
+
)
|
|
1137
1064
|
}
|
|
1138
1065
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
// `kind === "start"` is unreachable here: `selectsStart` only fires when
|
|
1155
|
-
// `first === undefined`, which the `if` branch above already handled.
|
|
1156
|
-
const bareCheck =
|
|
1157
|
-
kind === "check" && restPositionals.length === 0 && present.has("--open-threads")
|
|
1158
|
-
const arityMsg = bareCheck
|
|
1159
|
-
? undefined
|
|
1160
|
-
: arityError(isHistory ? row!.token : first!, restPositionals, row!.arity)
|
|
1161
|
-
if (arityMsg !== undefined) return usagePlan(arityMsg, jsonSeen)
|
|
1066
|
+
const kind: Command["kind"] = judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row.kind
|
|
1067
|
+
|
|
1068
|
+
// `judgeAnswer` consumes TWO leading positionals ("judge" and "answer"), everything else one.
|
|
1069
|
+
const restPositionals = positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1)
|
|
1070
|
+
|
|
1071
|
+
const bare = kind === "check" && restPositionals.length === 0 && present.has("--open-threads")
|
|
1072
|
+
const arityMsg = bare
|
|
1073
|
+
? undefined
|
|
1074
|
+
: arityError(isHistory ? row.token : first, restPositionals, row.arity)
|
|
1075
|
+
if (arityMsg !== undefined) {
|
|
1076
|
+
const hint =
|
|
1077
|
+
afterBareJson !== undefined && restPositionals.includes(afterBareJson)
|
|
1078
|
+
? ` — to print one field, write --json=${afterBareJson}`
|
|
1079
|
+
: ""
|
|
1080
|
+
return usagePlan(arityMsg + hint, jsonSeen)
|
|
1162
1081
|
}
|
|
1163
1082
|
|
|
1164
1083
|
const violation = scopeViolation(kind, present)
|
|
@@ -1175,6 +1094,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1175
1094
|
readonly "--cost"?: number
|
|
1176
1095
|
readonly "--model"?: string
|
|
1177
1096
|
readonly "--var"?: Readonly<Record<string, string>>
|
|
1097
|
+
readonly "--threshold"?: Readonly<Record<string, string>>
|
|
1178
1098
|
readonly "--open-questions"?: boolean
|
|
1179
1099
|
readonly "--open-threads"?: boolean
|
|
1180
1100
|
readonly "--host"?: string
|
|
@@ -1206,30 +1126,26 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1206
1126
|
}
|
|
1207
1127
|
|
|
1208
1128
|
if (kind === "start") {
|
|
1209
|
-
const
|
|
1129
|
+
const [name, ...args] = restPositionals
|
|
1130
|
+
if (name === undefined && bag["--var"] !== undefined) {
|
|
1131
|
+
return usagePlan(
|
|
1132
|
+
"gtd start: --var needs a <name> — run `gtd start` to list the workflows and doors",
|
|
1133
|
+
jsonSeen,
|
|
1134
|
+
)
|
|
1135
|
+
}
|
|
1210
1136
|
return {
|
|
1211
1137
|
kind: "command",
|
|
1212
1138
|
command: {
|
|
1213
1139
|
kind: "start",
|
|
1214
|
-
|
|
1215
|
-
|
|
1140
|
+
...(name !== undefined ? { name } : {}),
|
|
1141
|
+
args,
|
|
1216
1142
|
vars: bag["--var"] ?? {},
|
|
1217
|
-
label,
|
|
1218
1143
|
},
|
|
1219
1144
|
json,
|
|
1220
1145
|
verbose,
|
|
1221
1146
|
}
|
|
1222
1147
|
}
|
|
1223
1148
|
|
|
1224
|
-
if (kind === "door") {
|
|
1225
|
-
return {
|
|
1226
|
-
kind: "command",
|
|
1227
|
-
command: { kind: "door", name: restPositionals[0]!, args: restPositionals.slice(1) },
|
|
1228
|
-
json,
|
|
1229
|
-
verbose,
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
1149
|
if (kind === "ui") {
|
|
1234
1150
|
return {
|
|
1235
1151
|
kind: "command",
|
|
@@ -1314,23 +1230,9 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1314
1230
|
}
|
|
1315
1231
|
|
|
1316
1232
|
if (kind === "historyJudges") {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
return usagePlan(
|
|
1321
|
-
`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
1322
|
-
jsonSeen,
|
|
1323
|
-
)
|
|
1324
|
-
}
|
|
1325
|
-
const nan = Object.entries(vars).find(
|
|
1326
|
-
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
1327
|
-
)
|
|
1328
|
-
if (nan !== undefined) {
|
|
1329
|
-
return usagePlan(
|
|
1330
|
-
`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
1331
|
-
jsonSeen,
|
|
1332
|
-
)
|
|
1333
|
-
}
|
|
1233
|
+
const thresholds = judgeThresholds(bag["--threshold"] ?? {})
|
|
1234
|
+
if (Either.isLeft(thresholds)) return usagePlan(thresholds.left, jsonSeen)
|
|
1235
|
+
const vars = thresholds.right
|
|
1334
1236
|
return {
|
|
1335
1237
|
kind: "command",
|
|
1336
1238
|
command: {
|
|
@@ -1392,8 +1294,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1392
1294
|
| "restore"
|
|
1393
1295
|
| "next"
|
|
1394
1296
|
| "validate"
|
|
1395
|
-
| "
|
|
1396
|
-
| "doors"
|
|
1297
|
+
| "guide"
|
|
1397
1298
|
| "base"
|
|
1398
1299
|
| "judge"
|
|
1399
1300
|
| "judgeAnswer"
|
|
@@ -1539,7 +1440,7 @@ export const runCli = (argv: readonly string[], io: CliIo): Effect.Effect<void,
|
|
|
1539
1440
|
|
|
1540
1441
|
if (plan.kind === "usage") {
|
|
1541
1442
|
return Effect.sync(() => {
|
|
1542
|
-
// The
|
|
1443
|
+
// The short usage (missing-command only) and the `--json`
|
|
1543
1444
|
// envelope both move to stderr — stdout stays byte-empty on a usage
|
|
1544
1445
|
// error exactly like every other failure surface.
|
|
1545
1446
|
if (plan.stdout.length > 0) io.stderr(plan.stdout)
|