@pmelab/gtd 59.0.0 → 59.0.1
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 +5 -5
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-07Z_ENch.mjs} +431 -446
- package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +410 -440
- package/dist/testkit/gherkin.mjs +145 -65
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -2
- package/src/cli/Cli.test.ts +133 -52
- package/src/cli/Cli.ts +384 -461
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/Program.test.ts +67 -4
- package/src/cli/Program.ts +65 -18
- package/src/edge/Edge.ts +1 -1
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.ts +3 -1
- package/src/gherkin/index.ts +135 -71
- package/src/gherkin/messages.test.ts +90 -0
- package/src/gherkin/messages.ts +84 -0
- package/src/testkit/TestRepo.test.ts +47 -2
- package/src/testkit/TestRepo.ts +51 -6
- package/src/twins/EmittedScriptRecognizer.ts +5 -8
- package/src/wire/Land.ts +1 -1
- package/src/workflow/load.ts +6 -6
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
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,8 +78,9 @@ 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
|
}
|
|
@@ -115,20 +117,10 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
115
117
|
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd history judges`/`gtd strays` — " +
|
|
116
118
|
"every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
117
119
|
valueHint: "<path>",
|
|
118
|
-
help:
|
|
119
|
-
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
"kind, content, session.id) prints just that value: a",
|
|
123
|
-
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
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
|
-
],
|
|
120
|
+
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",
|
|
121
|
+
helpFor: {
|
|
122
|
+
strays: "Print one entry of the array (--json=0), nothing past its end",
|
|
123
|
+
},
|
|
132
124
|
},
|
|
133
125
|
{
|
|
134
126
|
name: "--port",
|
|
@@ -143,11 +135,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
143
135
|
},
|
|
144
136
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
145
137
|
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
|
-
],
|
|
138
|
+
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
139
|
},
|
|
152
140
|
{
|
|
153
141
|
name: "--host",
|
|
@@ -160,11 +148,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
160
148
|
: Either.right(raw),
|
|
161
149
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
162
150
|
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
|
-
],
|
|
151
|
+
help: "Bind this address directly instead of publishing through tailscale serve",
|
|
168
152
|
},
|
|
169
153
|
{
|
|
170
154
|
name: "--self-signed",
|
|
@@ -174,10 +158,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
174
158
|
decode: () => Either.right(true),
|
|
175
159
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
176
160
|
valueHint: "",
|
|
177
|
-
help:
|
|
178
|
-
"(gtd ui only) generate a throwaway self-signed TLS",
|
|
179
|
-
"certificate instead of the configured ui.cert/ui.key",
|
|
180
|
-
],
|
|
161
|
+
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key",
|
|
181
162
|
},
|
|
182
163
|
{
|
|
183
164
|
name: "--dev",
|
|
@@ -187,7 +168,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
187
168
|
decode: () => Either.right(true),
|
|
188
169
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
189
170
|
valueHint: "",
|
|
190
|
-
help:
|
|
171
|
+
help: "Serve local development sources instead of the packaged build",
|
|
191
172
|
},
|
|
192
173
|
{
|
|
193
174
|
name: "--provider",
|
|
@@ -200,11 +181,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
200
181
|
: Either.left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
201
182
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
202
183
|
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
|
-
],
|
|
184
|
+
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
185
|
},
|
|
209
186
|
{
|
|
210
187
|
name: "--answers",
|
|
@@ -217,11 +194,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
217
194
|
: Either.right(raw),
|
|
218
195
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
219
196
|
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
|
-
],
|
|
197
|
+
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)",
|
|
225
198
|
},
|
|
226
199
|
{
|
|
227
200
|
name: "--cost",
|
|
@@ -231,7 +204,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
231
204
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
232
205
|
scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
|
|
233
206
|
valueHint: "<n>",
|
|
234
|
-
help:
|
|
207
|
+
help: "Record the token cost of the invocation that just finished on the commit",
|
|
235
208
|
},
|
|
236
209
|
{
|
|
237
210
|
name: "--model",
|
|
@@ -244,11 +217,12 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
244
217
|
: Either.right(raw),
|
|
245
218
|
scopeError: "gtd: --model is only valid for `gtd land` (with --cost) or `gtd judge run`",
|
|
246
219
|
valueHint: "<name>",
|
|
247
|
-
help:
|
|
248
|
-
|
|
249
|
-
"
|
|
250
|
-
|
|
251
|
-
|
|
220
|
+
help: "",
|
|
221
|
+
helpFor: {
|
|
222
|
+
land: "With --cost: the model that cost was spent on",
|
|
223
|
+
judgeRun:
|
|
224
|
+
"The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)",
|
|
225
|
+
},
|
|
252
226
|
},
|
|
253
227
|
{
|
|
254
228
|
name: "--workflow",
|
|
@@ -260,10 +234,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
260
234
|
"gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` " +
|
|
261
235
|
"form; landing and starting are different verbs",
|
|
262
236
|
valueHint: "<name>",
|
|
263
|
-
help:
|
|
264
|
-
"(with no command at all) start a brand new process on",
|
|
265
|
-
"the named workflow — authenticated as human",
|
|
266
|
-
],
|
|
237
|
+
help: "Start a new process on the named workflow",
|
|
267
238
|
},
|
|
268
239
|
{
|
|
269
240
|
name: "--var",
|
|
@@ -308,14 +279,12 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
308
279
|
},
|
|
309
280
|
scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
|
|
310
281
|
valueHint: "<name>=<value>",
|
|
311
|
-
help:
|
|
312
|
-
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"qualityTriageMinP",
|
|
318
|
-
],
|
|
282
|
+
help: "",
|
|
283
|
+
helpFor: {
|
|
284
|
+
start: "Pin one of the workflow's declared settings for the new process (repeatable)",
|
|
285
|
+
historyJudges:
|
|
286
|
+
"Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)",
|
|
287
|
+
},
|
|
319
288
|
},
|
|
320
289
|
{
|
|
321
290
|
name: "--jsonl",
|
|
@@ -325,7 +294,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
325
294
|
decode: () => Either.right(true),
|
|
326
295
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
327
296
|
valueHint: "",
|
|
328
|
-
help:
|
|
297
|
+
help: "Print one JSON line per verdict instead of the report",
|
|
329
298
|
conflicts: ["--json", "--var"],
|
|
330
299
|
},
|
|
331
300
|
{
|
|
@@ -336,12 +305,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
336
305
|
decode: () => Either.right(true),
|
|
337
306
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
338
307
|
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
|
-
],
|
|
308
|
+
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain",
|
|
345
309
|
},
|
|
346
310
|
{
|
|
347
311
|
name: "--open-threads",
|
|
@@ -351,15 +315,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
351
315
|
decode: () => Either.right(true),
|
|
352
316
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
353
317
|
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
|
-
],
|
|
318
|
+
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
319
|
},
|
|
364
320
|
{
|
|
365
321
|
name: "--verbose",
|
|
@@ -369,11 +325,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
369
325
|
decode: () => Either.right(true),
|
|
370
326
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
371
327
|
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
|
-
],
|
|
328
|
+
help: "Narrate on stderr what gtd resolved and where its config came from",
|
|
377
329
|
},
|
|
378
330
|
]
|
|
379
331
|
|
|
@@ -409,446 +361,373 @@ type Arity =
|
|
|
409
361
|
// Any number of positionals, none included.
|
|
410
362
|
| { readonly name: string; readonly many: true }
|
|
411
363
|
|
|
364
|
+
type Group = "Core loop" | "Start and stop" | "Inspect" | "Driver plumbing"
|
|
365
|
+
|
|
412
366
|
interface CommandRow {
|
|
413
367
|
readonly token: string
|
|
414
368
|
readonly kind: Command["kind"]
|
|
415
369
|
readonly arity: Arity
|
|
416
|
-
readonly
|
|
370
|
+
readonly group: Group
|
|
371
|
+
/** The top-level help's one line — short enough for its column. */
|
|
372
|
+
readonly summary: string
|
|
373
|
+
/** The command's own help: prose paragraphs, wrapped when rendered (see `wrap`). */
|
|
374
|
+
readonly details: string
|
|
417
375
|
}
|
|
418
376
|
|
|
419
377
|
const COMMAND_ROWS: readonly CommandRow[] = [
|
|
420
378
|
{
|
|
421
|
-
token: "
|
|
422
|
-
kind: "
|
|
423
|
-
arity: "none",
|
|
424
|
-
details: [
|
|
425
|
-
"Land whatever the tree now shows at the currently resolved",
|
|
426
|
-
"rest — a human capture, an agent/check turn, or an empty",
|
|
427
|
-
"attempt (a fruitless prompt turn). Pass",
|
|
428
|
-
"--cost=<n> (optionally --model=<name>) to record the",
|
|
429
|
-
"just-finished invocation's token cost and model on the",
|
|
430
|
-
"turn commit (summed into it.processCost/",
|
|
431
|
-
"processCostByModel). Plain (the default) prints one",
|
|
432
|
-
"human-readable sentence naming the commit subject (or a",
|
|
433
|
-
"no-op note) plus a pointer to `gtd land --json=script | sh`",
|
|
434
|
-
"to get the landing script — never the script itself.",
|
|
435
|
-
"--json emits script (the actual POSIX sh",
|
|
436
|
-
"a driver runs) alongside settled, idle, state (the",
|
|
437
|
-
"post-land target), subject, cost and model. Exits 0 on",
|
|
438
|
-
"success, 1 on any refusal — see the Exit codes section",
|
|
439
|
-
"below",
|
|
440
|
-
],
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
token: "abandon",
|
|
444
|
-
kind: "abandon",
|
|
445
|
-
arity: "none",
|
|
446
|
-
details: [
|
|
447
|
-
"End the process currently underway without completing it:",
|
|
448
|
-
"rewind HEAD to the commit the process started from,",
|
|
449
|
-
"keeping everything it produced as uncommitted changes. A",
|
|
450
|
-
"no-op when no process is underway",
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
token: "restore",
|
|
455
|
-
kind: "restore",
|
|
379
|
+
token: "next",
|
|
380
|
+
kind: "next",
|
|
456
381
|
arity: "none",
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
],
|
|
382
|
+
group: "Core loop",
|
|
383
|
+
summary: "Print what should happen next",
|
|
384
|
+
details: `Print what should happen next in the process, changing nothing — safe to poll.
|
|
385
|
+
|
|
386
|
+
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.
|
|
387
|
+
|
|
388
|
+
--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).`,
|
|
465
389
|
},
|
|
466
390
|
{
|
|
467
|
-
token: "
|
|
468
|
-
kind: "
|
|
391
|
+
token: "land",
|
|
392
|
+
kind: "land",
|
|
469
393
|
arity: "none",
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
],
|
|
394
|
+
group: "Core loop",
|
|
395
|
+
summary: "Record what the tree now shows",
|
|
396
|
+
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.
|
|
397
|
+
|
|
398
|
+
gtd writes nothing itself. Plain output names the commit it would make; --json=script prints the script that makes it:
|
|
399
|
+
|
|
400
|
+
s=$(gtd land --json=script) && sh -c "$s"
|
|
401
|
+
|
|
402
|
+
--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.`,
|
|
490
403
|
},
|
|
491
404
|
{
|
|
492
405
|
token: "validate",
|
|
493
406
|
kind: "validate",
|
|
494
407
|
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
|
-
],
|
|
408
|
+
group: "Core loop",
|
|
409
|
+
summary: "Print the script that checks the steering file",
|
|
410
|
+
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.`,
|
|
506
411
|
},
|
|
507
412
|
{
|
|
508
|
-
token: "
|
|
509
|
-
kind: "
|
|
510
|
-
arity: "
|
|
511
|
-
|
|
413
|
+
token: "door",
|
|
414
|
+
kind: "door",
|
|
415
|
+
arity: { name: "name", variadic: "args" },
|
|
416
|
+
group: "Start and stop",
|
|
417
|
+
summary: "Start a process through a named shortcut",
|
|
418
|
+
details: `Start a process through a door: a named shortcut to a workflow that maps its positional args to the workflow's settings. It prints the same start script \`gtd --workflow\` does. Each workflow the config exports brings its own doors, plus the config's \`doors\` export.
|
|
419
|
+
|
|
420
|
+
With no <name>, an unknown one, or the wrong number of args, it lists the doors and exits 2.`,
|
|
512
421
|
},
|
|
513
422
|
{
|
|
514
|
-
token: "
|
|
515
|
-
kind: "
|
|
423
|
+
token: "doors",
|
|
424
|
+
kind: "doors",
|
|
516
425
|
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
|
-
],
|
|
426
|
+
group: "Start and stop",
|
|
427
|
+
summary: "List the doors",
|
|
428
|
+
details: `List every door, sorted: one \`name [args] → workflow\` line each. --json prints a JSON array of { name, workflow, args: [{ name, optional }] }. Writes nothing.`,
|
|
535
429
|
},
|
|
536
430
|
{
|
|
537
|
-
token: "
|
|
538
|
-
kind: "
|
|
539
|
-
arity:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
],
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
token: "uncheck",
|
|
557
|
-
kind: "uncheck",
|
|
558
|
-
arity: { name: "file" },
|
|
559
|
-
details: [
|
|
560
|
-
"Read <file> and reset every review-mode `- [x]`/`- [X]`",
|
|
561
|
-
"file-pointer box back to `- [ ]`, writing the result back",
|
|
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
|
-
],
|
|
431
|
+
token: "abandon",
|
|
432
|
+
kind: "abandon",
|
|
433
|
+
arity: "none",
|
|
434
|
+
group: "Start and stop",
|
|
435
|
+
summary: "Stop the process, keeping its changes",
|
|
436
|
+
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.
|
|
437
|
+
|
|
438
|
+
s=$(gtd abandon) && sh -c "$s"`,
|
|
571
439
|
},
|
|
572
440
|
{
|
|
573
|
-
token: "
|
|
574
|
-
kind: "
|
|
441
|
+
token: "restore",
|
|
442
|
+
kind: "restore",
|
|
575
443
|
arity: "none",
|
|
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
|
-
],
|
|
444
|
+
group: "Start and stop",
|
|
445
|
+
summary: "Bring back the last abandoned process",
|
|
446
|
+
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.`,
|
|
584
447
|
},
|
|
585
448
|
{
|
|
586
449
|
token: "summary",
|
|
587
450
|
kind: "summary",
|
|
588
451
|
arity: "none",
|
|
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
|
-
],
|
|
452
|
+
group: "Inspect",
|
|
453
|
+
summary: "Print the prompt for the closing message",
|
|
454
|
+
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.
|
|
455
|
+
|
|
456
|
+
Refuses when the workflow declares no summary template, or when the process has no commits.`,
|
|
603
457
|
},
|
|
604
458
|
{
|
|
605
459
|
token: "base",
|
|
606
460
|
kind: "base",
|
|
607
461
|
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
|
-
],
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
token: "door",
|
|
620
|
-
kind: "door",
|
|
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",
|
|
636
|
-
arity: "none",
|
|
637
|
-
details: [
|
|
638
|
-
"List every door, sorted: one `name [args] → workflow` line",
|
|
639
|
-
"each. --json prints one JSON array of { name, workflow,",
|
|
640
|
-
"args: [{ name, optional }] } instead; --json=<path> reads one",
|
|
641
|
-
"value off it. Writes nothing",
|
|
642
|
-
],
|
|
643
|
-
},
|
|
644
|
-
{
|
|
645
|
-
token: "judge",
|
|
646
|
-
kind: "judge",
|
|
647
|
-
arity: "none",
|
|
648
|
-
details: [
|
|
649
|
-
"Print the resolved rest's pending judgment — the prepared",
|
|
650
|
-
"state, its typed questions, and their criteria — the same",
|
|
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
|
-
],
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
token: "judge answer",
|
|
658
|
-
kind: "judgeAnswer",
|
|
659
|
-
arity: "none",
|
|
660
|
-
details: [
|
|
661
|
-
"Read a verdict off stdin — one { id, answer, p } entry per",
|
|
662
|
-
"question the pending judgment declared — and decode it",
|
|
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
|
-
],
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
token: "judge run",
|
|
672
|
-
kind: "judgeRun",
|
|
673
|
-
arity: "none",
|
|
674
|
-
details: [
|
|
675
|
-
"Answer a judgment: read the `gtd judge --json` document off",
|
|
676
|
-
"stdin, write a verdict [{ id, answer, p }] on stdout — what",
|
|
677
|
-
"`gtd judge answer` decodes. --provider fixed answers from",
|
|
678
|
-
"--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
|
|
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
|
-
],
|
|
462
|
+
group: "Inspect",
|
|
463
|
+
summary: "Print the commit a review diff starts from",
|
|
464
|
+
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.`,
|
|
689
465
|
},
|
|
690
466
|
{
|
|
691
467
|
token: "strays",
|
|
692
468
|
kind: "strays",
|
|
693
469
|
arity: { name: "range", variadic: "glob" },
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
],
|
|
470
|
+
group: "Inspect",
|
|
471
|
+
summary: "List changed files outside the globs",
|
|
472
|
+
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.
|
|
473
|
+
|
|
474
|
+
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.`,
|
|
708
475
|
},
|
|
709
476
|
{
|
|
710
477
|
token: "history list",
|
|
711
478
|
kind: "historyList",
|
|
712
479
|
arity: "none",
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
],
|
|
480
|
+
group: "Inspect",
|
|
481
|
+
summary: "List finished processes",
|
|
482
|
+
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\`.`,
|
|
720
483
|
},
|
|
721
484
|
{
|
|
722
485
|
token: "history show",
|
|
723
486
|
kind: "historyShow",
|
|
724
487
|
arity: { name: "ref" },
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
],
|
|
488
|
+
group: "Inspect",
|
|
489
|
+
summary: "Replay one finished process",
|
|
490
|
+
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.`,
|
|
733
491
|
},
|
|
734
492
|
{
|
|
735
493
|
token: "history metrics",
|
|
736
494
|
kind: "historyMetrics",
|
|
737
495
|
arity: { name: "ref", many: true },
|
|
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
|
-
],
|
|
496
|
+
group: "Inspect",
|
|
497
|
+
summary: "Report cost and rework of finished processes",
|
|
498
|
+
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.`,
|
|
750
499
|
},
|
|
751
500
|
{
|
|
752
501
|
token: "history judges",
|
|
753
502
|
kind: "historyJudges",
|
|
754
503
|
arity: { name: "ref", many: true },
|
|
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
|
-
],
|
|
504
|
+
group: "Inspect",
|
|
505
|
+
summary: "Calibrate the judges of finished processes",
|
|
506
|
+
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.`,
|
|
767
507
|
},
|
|
768
508
|
{
|
|
769
509
|
token: "history fetch",
|
|
770
510
|
kind: "historyFetch",
|
|
771
511
|
arity: { name: "remote", optional: true },
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
512
|
+
group: "Inspect",
|
|
513
|
+
summary: "Print a script that fetches finished processes",
|
|
514
|
+
details: `Print a script that fetches every finished process kept on <remote> (default: origin) and reports how many arrived. gtd runs no git itself:
|
|
515
|
+
|
|
516
|
+
s=$(gtd history fetch) && sh -c "$s"`,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
token: "ui",
|
|
520
|
+
kind: "ui",
|
|
521
|
+
arity: "none",
|
|
522
|
+
group: "Inspect",
|
|
523
|
+
summary: "Serve the web and phone client",
|
|
524
|
+
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.`,
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
token: "install",
|
|
528
|
+
kind: "install",
|
|
529
|
+
arity: "none",
|
|
530
|
+
group: "Driver plumbing",
|
|
531
|
+
summary: "Print a briefing for writing a driver",
|
|
532
|
+
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.`,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
token: "judge",
|
|
536
|
+
kind: "judge",
|
|
537
|
+
arity: "none",
|
|
538
|
+
group: "Driver plumbing",
|
|
539
|
+
summary: "Print the pending judgment",
|
|
540
|
+
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.`,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
token: "judge answer",
|
|
544
|
+
kind: "judgeAnswer",
|
|
545
|
+
arity: "none",
|
|
546
|
+
group: "Driver plumbing",
|
|
547
|
+
summary: "Land a verdict read from stdin",
|
|
548
|
+
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).`,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
token: "judge run",
|
|
552
|
+
kind: "judgeRun",
|
|
553
|
+
arity: "none",
|
|
554
|
+
group: "Driver plumbing",
|
|
555
|
+
summary: "Answer a judgment with a model",
|
|
556
|
+
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.
|
|
557
|
+
|
|
558
|
+
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.`,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
token: "check",
|
|
562
|
+
kind: "check",
|
|
563
|
+
arity: { names: ["mode", "file"] },
|
|
564
|
+
group: "Driver plumbing",
|
|
565
|
+
summary: "Check a steering file's format",
|
|
566
|
+
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. Reads no workflow and runs from any directory — emitted validation scripts call it.`,
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
token: "uncheck",
|
|
570
|
+
kind: "uncheck",
|
|
571
|
+
arity: { name: "file" },
|
|
572
|
+
group: "Driver plumbing",
|
|
573
|
+
summary: "Untick a review file's boxes",
|
|
574
|
+
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.`,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
token: "lsp",
|
|
578
|
+
kind: "lsp",
|
|
579
|
+
arity: "none",
|
|
580
|
+
group: "Driver plumbing",
|
|
581
|
+
summary: "Start the steering-file language server",
|
|
582
|
+
details: `Start the language server for .gtd/ steering files on stdio.`,
|
|
777
583
|
},
|
|
778
584
|
]
|
|
779
585
|
|
|
780
586
|
const commandByToken = (token: string): CommandRow | undefined =>
|
|
781
587
|
COMMAND_ROWS.find((r) => r.token === token)
|
|
782
588
|
|
|
589
|
+
/** Every word that may follow `gtd`, for "did you mean". */
|
|
590
|
+
const COMMAND_WORDS: readonly string[] = [
|
|
591
|
+
...new Set([...COMMAND_ROWS.map((r) => r.token.split(" ")[0]!), "help", "version"]),
|
|
592
|
+
]
|
|
593
|
+
|
|
783
594
|
// ---------------------------------------------------------------------------
|
|
784
595
|
// Help rendering
|
|
785
596
|
// ---------------------------------------------------------------------------
|
|
786
597
|
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
598
|
+
const TOP_COLUMN = 28
|
|
599
|
+
const OPTION_COLUMN = 22
|
|
600
|
+
|
|
601
|
+
const synopsis = (row: CommandRow): string => {
|
|
602
|
+
const { token, arity } = row
|
|
603
|
+
if (arity === "none") return token
|
|
604
|
+
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`
|
|
605
|
+
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`
|
|
606
|
+
if ("optional" in arity) return `${token} [${arity.name}]`
|
|
607
|
+
if ("many" in arity) return `${token} [${arity.name}...]`
|
|
608
|
+
return `${token} <${arity.name}>`
|
|
797
609
|
}
|
|
798
610
|
|
|
799
|
-
const START_SHORT_FORM = renderBlock("(no command) --workflow <name>", [
|
|
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
611
|
const SPACE_FORM_FLAGS = new Set(["--workflow", "--var"])
|
|
808
612
|
|
|
809
613
|
const flagHeader = (row: FlagRow): string => {
|
|
810
614
|
if (row.valueHint === "") return row.name
|
|
615
|
+
if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`
|
|
811
616
|
return SPACE_FORM_FLAGS.has(row.name)
|
|
812
617
|
? `${row.name} ${row.valueHint}`
|
|
813
618
|
: `${row.name}=${row.valueHint}`
|
|
814
619
|
}
|
|
815
620
|
|
|
621
|
+
const START_ROW = {
|
|
622
|
+
term: "--workflow <name>",
|
|
623
|
+
summary: "Start a process on a workflow; --var <name>=<value> pins one of its settings",
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const GLOBAL_OPTIONS: readonly (readonly [string, string])[] = [
|
|
627
|
+
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
628
|
+
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
629
|
+
["-V, --version", "Print the version"],
|
|
630
|
+
]
|
|
631
|
+
|
|
632
|
+
const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
633
|
+
[
|
|
634
|
+
"help [command]",
|
|
635
|
+
"Print help for gtd or one command",
|
|
636
|
+
"Print gtd's overview, or with <command> that command's own help.",
|
|
637
|
+
],
|
|
638
|
+
["version", "Print the version", "Print gtd's version."],
|
|
639
|
+
]
|
|
640
|
+
|
|
641
|
+
const EXAMPLES = ` $ gtd --workflow <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
|
-
|
|
827
|
-
|
|
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 start =
|
|
656
|
+
group === "Start and stop" ? definition(START_ROW.term, START_ROW.summary, TOP_COLUMN) : []
|
|
657
|
+
const extra =
|
|
658
|
+
group === "Driver plumbing"
|
|
659
|
+
? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN))
|
|
660
|
+
: []
|
|
661
|
+
return `${group}:\n${lines([...start, ...rows, ...extra])}`
|
|
831
662
|
})
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
663
|
+
return (
|
|
664
|
+
"Usage: gtd <command> [options]\n\n" +
|
|
665
|
+
lines(
|
|
666
|
+
wrap(
|
|
667
|
+
"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.",
|
|
668
|
+
),
|
|
669
|
+
) +
|
|
670
|
+
"\n" +
|
|
671
|
+
groups.join("\n") +
|
|
672
|
+
"\nOptions:\n" +
|
|
673
|
+
lines(GLOBAL_OPTIONS.flatMap(([term, text]) => definition(term, text, TOP_COLUMN))) +
|
|
674
|
+
`\nExamples:\n${EXAMPLES}\n\n` +
|
|
675
|
+
lines(
|
|
676
|
+
wrap(
|
|
677
|
+
"Run `gtd help <command>` for a command's details and options, and `gtd install` to learn how to drive gtd.",
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
684
|
+
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
685
|
+
|
|
686
|
+
gtd --workflow <name> start a process
|
|
687
|
+
gtd next print what should happen next
|
|
688
|
+
gtd land record what the tree now shows
|
|
689
|
+
|
|
690
|
+
`
|
|
844
691
|
|
|
692
|
+
const renderCommandHelp = (row: CommandRow): string => {
|
|
693
|
+
const options = FLAGS.filter((f) => f.name !== "--verbose" && f.scope(row.kind)).flatMap((f) =>
|
|
694
|
+
definition(flagHeader(f), f.helpFor?.[row.kind] ?? f.help, OPTION_COLUMN),
|
|
695
|
+
)
|
|
696
|
+
const global = definition("-v, --verbose", "Narrate on stderr what gtd resolved", OPTION_COLUMN)
|
|
845
697
|
return (
|
|
846
|
-
`Usage: gtd
|
|
847
|
-
|
|
848
|
-
|
|
698
|
+
`Usage: gtd ${synopsis(row)}${options.length > 0 ? " [options]" : ""}\n\n` +
|
|
699
|
+
lines(wrap(row.details)) +
|
|
700
|
+
`\nOptions:\n${lines([...options, ...global])}`
|
|
849
701
|
)
|
|
850
702
|
}
|
|
851
703
|
|
|
704
|
+
/** `gtd help <topic…>`'s text: one command's help, every `history` subcommand's for `history`, or undefined for an unknown topic. */
|
|
705
|
+
const renderTopicHelp = (topic: readonly string[]): string | undefined => {
|
|
706
|
+
const two = commandByToken(topic.slice(0, 2).join(" "))
|
|
707
|
+
if (two !== undefined && topic.length >= 2) return renderCommandHelp(two)
|
|
708
|
+
const [first = ""] = topic
|
|
709
|
+
const one = commandByToken(first)
|
|
710
|
+
if (one !== undefined) return renderCommandHelp(one)
|
|
711
|
+
const extra = EXTRA_TOPICS.find(([term]) => term.split(" ")[0] === first)
|
|
712
|
+
if (extra !== undefined) return `Usage: gtd ${extra[0]}\n\n${lines(wrap(extra[2]))}`
|
|
713
|
+
const family = COMMAND_ROWS.filter((r) => r.token.startsWith(`${first} `))
|
|
714
|
+
if (first !== "" && family.length > 0) return family.map(renderCommandHelp).join("\n")
|
|
715
|
+
return undefined
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** A usage error's tail: the nearest candidate when `word` looks like a typo of one, and where the full list is. */
|
|
719
|
+
const didYouMean = (
|
|
720
|
+
word: string,
|
|
721
|
+
candidates: readonly string[],
|
|
722
|
+
prefix = "",
|
|
723
|
+
pointer = "`gtd help` for every command",
|
|
724
|
+
): string => {
|
|
725
|
+
const guess = closest(word, candidates)
|
|
726
|
+
return guess === undefined
|
|
727
|
+
? ` — run ${pointer}`
|
|
728
|
+
: ` — did you mean \`${prefix}${guess}\`? Run ${pointer}`
|
|
729
|
+
}
|
|
730
|
+
|
|
852
731
|
// ---------------------------------------------------------------------------
|
|
853
732
|
// Tokenizer
|
|
854
733
|
// ---------------------------------------------------------------------------
|
|
@@ -865,6 +744,17 @@ interface TokenizeOk {
|
|
|
865
744
|
readonly byFlag: ReadonlyMap<string, readonly string[]>
|
|
866
745
|
readonly present: ReadonlySet<string>
|
|
867
746
|
readonly jsonSeen: boolean
|
|
747
|
+
/** The positional right after a bare `--json` — most likely a path meant as `--json=<path>`. */
|
|
748
|
+
readonly afterBareJson?: string
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const unknownOption = (tok: string, jsonSeen: boolean): TokenizeError => {
|
|
752
|
+
const candidates = tok.startsWith("--") ? FLAGS.map((f) => f.name) : []
|
|
753
|
+
return {
|
|
754
|
+
kind: "error",
|
|
755
|
+
message: `gtd: unknown option '${tok}'${didYouMean(tok.split("=")[0]!, candidates, "", "`gtd help <command>` for its options")}`,
|
|
756
|
+
jsonSeen,
|
|
757
|
+
}
|
|
868
758
|
}
|
|
869
759
|
|
|
870
760
|
// fallow-ignore-next-line complexity
|
|
@@ -873,35 +763,25 @@ const tokenize = (tail: readonly string[]): TokenizeError | TokenizeOk => {
|
|
|
873
763
|
const byFlag = new Map<string, string[]>()
|
|
874
764
|
const present = new Set<string>()
|
|
875
765
|
let jsonSeen = false
|
|
766
|
+
let afterBareJson: string | undefined
|
|
876
767
|
|
|
877
768
|
for (let i = 0; i < tail.length; i++) {
|
|
878
769
|
const original = tail[i]!
|
|
879
770
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original
|
|
880
|
-
if (!tok.startsWith("
|
|
771
|
+
if (!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
|
// ---------------------------------------------------------------------------
|
|
@@ -1054,24 +941,43 @@ const buildLandCommand = (bag: {
|
|
|
1054
941
|
...(bag["--model"] !== undefined ? { model: bag["--model"] } : {}),
|
|
1055
942
|
})
|
|
1056
943
|
|
|
944
|
+
const HELP_FLAGS = new Set(["--help", "-h"])
|
|
945
|
+
|
|
946
|
+
/** `--help`/`-h` anywhere: the named command's own help, else the overview — never a usage error, so a typo'd command still gets help. */
|
|
947
|
+
const helpPlan = (argv: readonly string[]): CliPlan => {
|
|
948
|
+
const tokenized = tokenize(argv.slice(2).filter((tok) => !HELP_FLAGS.has(tok)))
|
|
949
|
+
const topic = tokenized.kind === "ok" ? tokenized.positionals : []
|
|
950
|
+
return { kind: "output", stdout: renderTopicHelp(topic) ?? renderHelp() }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** `gtd help [topic…]`: an unknown topic is a usage error naming the nearest command. */
|
|
954
|
+
const helpCommandPlan = (topic: readonly string[], json: boolean): CliPlan => {
|
|
955
|
+
if (topic.length === 0) return { kind: "output", stdout: renderHelp() }
|
|
956
|
+
const text = renderTopicHelp(topic)
|
|
957
|
+
if (text !== undefined) return { kind: "output", stdout: text }
|
|
958
|
+
return usagePlan(
|
|
959
|
+
`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0]!, COMMAND_WORDS, "gtd help ")}`,
|
|
960
|
+
json,
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
|
|
1057
964
|
// fallow-ignore-next-line complexity
|
|
1058
965
|
export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
1059
|
-
if (argv.
|
|
1060
|
-
return { kind: "output", stdout: renderHelp() }
|
|
966
|
+
if (argv.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv)
|
|
1061
967
|
if (argv.includes("--version") || argv.includes("-V")) {
|
|
1062
968
|
return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
1063
969
|
}
|
|
1064
970
|
|
|
1065
971
|
const tokenized = tokenize(argv.slice(2))
|
|
1066
972
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen)
|
|
1067
|
-
const { positionals, byFlag, present, jsonSeen } = tokenized
|
|
973
|
+
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized
|
|
1068
974
|
|
|
1069
975
|
const first = positionals[0]
|
|
1070
976
|
const workflowRaw = byFlag.get("--workflow")?.[0]
|
|
1071
977
|
const workflowPresent = workflowRaw !== undefined
|
|
1072
978
|
|
|
1073
979
|
if (first === "version") return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
1074
|
-
if (first === "help") return
|
|
980
|
+
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen)
|
|
1075
981
|
|
|
1076
982
|
const isHistory = first === "history"
|
|
1077
983
|
const row =
|
|
@@ -1080,8 +986,14 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1080
986
|
: commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first)
|
|
1081
987
|
|
|
1082
988
|
if (isHistory && row === undefined) {
|
|
989
|
+
const subcommands = COMMAND_ROWS.filter((r) => r.token.startsWith("history ")).map(
|
|
990
|
+
(r) => r.token.split(" ")[1]!,
|
|
991
|
+
)
|
|
992
|
+
const typo = closest(positionals[1] ?? "", subcommands)
|
|
1083
993
|
return usagePlan(
|
|
1084
|
-
|
|
994
|
+
typo !== undefined
|
|
995
|
+
? `gtd history: unknown subcommand '${positionals[1]}' — did you mean \`gtd history ${typo}\`?`
|
|
996
|
+
: "gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]",
|
|
1085
997
|
jsonSeen,
|
|
1086
998
|
)
|
|
1087
999
|
}
|
|
@@ -1090,7 +1002,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1090
1002
|
// "answer" arriving as `positionals[1]`, a second token after "judge" —
|
|
1091
1003
|
// detected below via `judgeAnswer`, not through a second dispatchable
|
|
1092
1004
|
// `COMMAND_ROWS` row: the `"judge answer"`-tokened row above exists for
|
|
1093
|
-
//
|
|
1005
|
+
// its help. It CAN also be matched by
|
|
1094
1006
|
// `commandByToken` directly, but only if a caller passes the single
|
|
1095
1007
|
// (space-containing) argv token `"judge answer"` — an unusual shell quoting
|
|
1096
1008
|
// choice, not the normal two-argv-token invocation `gtd judge answer` — and
|
|
@@ -1124,16 +1036,15 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1124
1036
|
if (violation !== undefined) return usagePlan(violation, jsonSeen)
|
|
1125
1037
|
if (first === undefined) {
|
|
1126
1038
|
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`)",
|
|
1039
|
+
"gtd: missing command — run `gtd help` for every command, or `gtd install` to learn how to drive gtd",
|
|
1132
1040
|
jsonSeen,
|
|
1133
|
-
|
|
1041
|
+
SHORT_USAGE,
|
|
1134
1042
|
)
|
|
1135
1043
|
}
|
|
1136
|
-
return usagePlan(
|
|
1044
|
+
return usagePlan(
|
|
1045
|
+
`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`,
|
|
1046
|
+
jsonSeen,
|
|
1047
|
+
)
|
|
1137
1048
|
}
|
|
1138
1049
|
|
|
1139
1050
|
// From here, `kind` is a genuine `Command["kind"]`. `judgeAnswer` consumes
|
|
@@ -1153,12 +1064,20 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1153
1064
|
} else {
|
|
1154
1065
|
// `kind === "start"` is unreachable here: `selectsStart` only fires when
|
|
1155
1066
|
// `first === undefined`, which the `if` branch above already handled.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1067
|
+
// A bare `gtd door` lists the doors — which only the loaded config knows.
|
|
1068
|
+
const bare =
|
|
1069
|
+
(kind === "check" && restPositionals.length === 0 && present.has("--open-threads")) ||
|
|
1070
|
+
(kind === "door" && restPositionals.length === 0)
|
|
1071
|
+
const arityMsg = bare
|
|
1159
1072
|
? undefined
|
|
1160
1073
|
: arityError(isHistory ? row!.token : first!, restPositionals, row!.arity)
|
|
1161
|
-
if (arityMsg !== undefined)
|
|
1074
|
+
if (arityMsg !== undefined) {
|
|
1075
|
+
const hint =
|
|
1076
|
+
afterBareJson !== undefined && restPositionals.includes(afterBareJson)
|
|
1077
|
+
? ` — to print one field, write --json=${afterBareJson}`
|
|
1078
|
+
: ""
|
|
1079
|
+
return usagePlan(arityMsg + hint, jsonSeen)
|
|
1080
|
+
}
|
|
1162
1081
|
}
|
|
1163
1082
|
|
|
1164
1083
|
const violation = scopeViolation(kind, present)
|
|
@@ -1224,7 +1143,11 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1224
1143
|
if (kind === "door") {
|
|
1225
1144
|
return {
|
|
1226
1145
|
kind: "command",
|
|
1227
|
-
command: {
|
|
1146
|
+
command: {
|
|
1147
|
+
kind: "door",
|
|
1148
|
+
...(restPositionals[0] !== undefined ? { name: restPositionals[0] } : {}),
|
|
1149
|
+
args: restPositionals.slice(1),
|
|
1150
|
+
},
|
|
1228
1151
|
json,
|
|
1229
1152
|
verbose,
|
|
1230
1153
|
}
|
|
@@ -1539,7 +1462,7 @@ export const runCli = (argv: readonly string[], io: CliIo): Effect.Effect<void,
|
|
|
1539
1462
|
|
|
1540
1463
|
if (plan.kind === "usage") {
|
|
1541
1464
|
return Effect.sync(() => {
|
|
1542
|
-
// The
|
|
1465
|
+
// The short usage (missing-command only) and the `--json`
|
|
1543
1466
|
// envelope both move to stderr — stdout stays byte-empty on a usage
|
|
1544
1467
|
// error exactly like every other failure surface.
|
|
1545
1468
|
if (plan.stdout.length > 0) io.stderr(plan.stdout)
|