@pmelab/gtd 59.0.1 → 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 +37 -33
- 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-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- 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 +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- 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 +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- 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 +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- 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 +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- 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 +2 -6
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- 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 +169 -146
- 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
|
@@ -85,6 +85,38 @@ interface FlagRow {
|
|
|
85
85
|
readonly conflicts?: readonly string[]
|
|
86
86
|
}
|
|
87
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
|
+
|
|
88
120
|
const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, string> => {
|
|
89
121
|
const n = Number(raw)
|
|
90
122
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) {
|
|
@@ -93,12 +125,23 @@ const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, str
|
|
|
93
125
|
return Either.right(n)
|
|
94
126
|
}
|
|
95
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
|
+
|
|
96
139
|
const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["kind"]>([
|
|
140
|
+
...SCRIPT_KINDS,
|
|
97
141
|
"next",
|
|
98
142
|
"land",
|
|
99
143
|
"judge",
|
|
100
144
|
"judgeAnswer",
|
|
101
|
-
"doors",
|
|
102
145
|
"historyList",
|
|
103
146
|
"historyShow",
|
|
104
147
|
"historyMetrics",
|
|
@@ -114,12 +157,14 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
114
157
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
115
158
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
116
159
|
scopeError:
|
|
117
|
-
"gtd: --json is only valid for `gtd next`/`gtd land
|
|
118
|
-
"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",
|
|
119
162
|
valueHint: "<path>",
|
|
120
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",
|
|
121
164
|
helpFor: {
|
|
122
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)`,
|
|
123
168
|
},
|
|
124
169
|
},
|
|
125
170
|
{
|
|
@@ -215,76 +260,35 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
215
260
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
216
261
|
? Either.left("gtd: --model must be a non-empty, single-line value")
|
|
217
262
|
: Either.right(raw),
|
|
218
|
-
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)",
|
|
219
265
|
valueHint: "<name>",
|
|
220
266
|
help: "",
|
|
221
267
|
helpFor: {
|
|
222
|
-
land: "With --cost: the model that cost was spent on",
|
|
268
|
+
land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
|
|
223
269
|
judgeRun:
|
|
224
270
|
"The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)",
|
|
225
271
|
},
|
|
226
272
|
},
|
|
227
273
|
{
|
|
228
|
-
name: "--
|
|
274
|
+
name: "--threshold",
|
|
229
275
|
arity: 1,
|
|
230
|
-
repeatable:
|
|
231
|
-
scope: (kind) => kind === "
|
|
232
|
-
decode: (
|
|
233
|
-
scopeError:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
valueHint: "<name>",
|
|
237
|
-
help: "Start a new process on the named workflow",
|
|
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)",
|
|
238
282
|
},
|
|
239
283
|
{
|
|
240
284
|
name: "--var",
|
|
241
285
|
arity: 1,
|
|
242
286
|
repeatable: true,
|
|
243
|
-
scope: (kind) => kind === "start"
|
|
244
|
-
decode: (raws) =>
|
|
245
|
-
|
|
246
|
-
const seen = new Set<string>()
|
|
247
|
-
for (const raw of raws) {
|
|
248
|
-
const eq = raw.indexOf("=")
|
|
249
|
-
if (eq <= 0) {
|
|
250
|
-
return Either.left(
|
|
251
|
-
`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`,
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
const name = raw.slice(0, eq)
|
|
255
|
-
const value = raw.slice(eq + 1)
|
|
256
|
-
if (!isSettingName(name)) {
|
|
257
|
-
return Either.left(
|
|
258
|
-
`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
if (/[\r\n]/.test(value)) {
|
|
262
|
-
return Either.left(`gtd: --var ${name} must be a single-line value`)
|
|
263
|
-
}
|
|
264
|
-
if (seen.has(name)) {
|
|
265
|
-
return Either.left(`gtd: --var ${name} specified more than once`)
|
|
266
|
-
}
|
|
267
|
-
seen.add(name)
|
|
268
|
-
if (name.startsWith("flag_")) {
|
|
269
|
-
const pinned = flagSettingValue(value)
|
|
270
|
-
if (pinned === undefined) {
|
|
271
|
-
return Either.left(`gtd: --var ${name} must be 1, 0, true or false`)
|
|
272
|
-
}
|
|
273
|
-
vars[name] = pinned
|
|
274
|
-
continue
|
|
275
|
-
}
|
|
276
|
-
vars[name] = value
|
|
277
|
-
}
|
|
278
|
-
return Either.right(vars)
|
|
279
|
-
},
|
|
280
|
-
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>`",
|
|
281
290
|
valueHint: "<name>=<value>",
|
|
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
|
-
},
|
|
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",
|
|
288
292
|
},
|
|
289
293
|
{
|
|
290
294
|
name: "--jsonl",
|
|
@@ -295,7 +299,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
295
299
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
296
300
|
valueHint: "",
|
|
297
301
|
help: "Print one JSON line per verdict instead of the report",
|
|
298
|
-
conflicts: ["--json", "--
|
|
302
|
+
conflicts: ["--json", "--threshold"],
|
|
299
303
|
},
|
|
300
304
|
{
|
|
301
305
|
name: "--open-questions",
|
|
@@ -356,6 +360,8 @@ type Arity =
|
|
|
356
360
|
| { readonly names: readonly [string, string] }
|
|
357
361
|
// One required leading positional, then any number the command itself judges.
|
|
358
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 }
|
|
359
365
|
// At most one trailing positional.
|
|
360
366
|
| { readonly name: string; readonly optional: true }
|
|
361
367
|
// Any number of positionals, none included.
|
|
@@ -407,25 +413,21 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
407
413
|
arity: "none",
|
|
408
414
|
group: "Core loop",
|
|
409
415
|
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.`,
|
|
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.`,
|
|
411
417
|
},
|
|
412
418
|
{
|
|
413
|
-
token: "
|
|
414
|
-
kind: "
|
|
415
|
-
arity: { name: "name", variadic: "args" },
|
|
419
|
+
token: "start",
|
|
420
|
+
kind: "start",
|
|
421
|
+
arity: { name: "name", variadic: "args", bare: true },
|
|
416
422
|
group: "Start and stop",
|
|
417
|
-
summary: "Start a
|
|
418
|
-
details: `Start a process
|
|
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.
|
|
419
425
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
arity: "none",
|
|
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.`,
|
|
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).`,
|
|
429
431
|
},
|
|
430
432
|
{
|
|
431
433
|
token: "abandon",
|
|
@@ -435,7 +437,7 @@ With no <name>, an unknown one, or the wrong number of args, it lists the doors
|
|
|
435
437
|
summary: "Stop the process, keeping its changes",
|
|
436
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.
|
|
437
439
|
|
|
438
|
-
s=$(gtd abandon) && sh -c "$s"`,
|
|
440
|
+
s=$(gtd abandon --json=script) && sh -c "$s"`,
|
|
439
441
|
},
|
|
440
442
|
{
|
|
441
443
|
token: "restore",
|
|
@@ -513,7 +515,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
513
515
|
summary: "Print a script that fetches finished processes",
|
|
514
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:
|
|
515
517
|
|
|
516
|
-
s=$(gtd history fetch) && sh -c "$s"`,
|
|
518
|
+
s=$(gtd history fetch --json=script) && sh -c "$s"`,
|
|
517
519
|
},
|
|
518
520
|
{
|
|
519
521
|
token: "ui",
|
|
@@ -524,8 +526,8 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
524
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.`,
|
|
525
527
|
},
|
|
526
528
|
{
|
|
527
|
-
token: "
|
|
528
|
-
kind: "
|
|
529
|
+
token: "guide",
|
|
530
|
+
kind: "guide",
|
|
529
531
|
arity: "none",
|
|
530
532
|
group: "Driver plumbing",
|
|
531
533
|
summary: "Print a briefing for writing a driver",
|
|
@@ -555,7 +557,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
555
557
|
summary: "Answer a judgment with a model",
|
|
556
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.
|
|
557
559
|
|
|
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.`,
|
|
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).`,
|
|
559
561
|
},
|
|
560
562
|
{
|
|
561
563
|
token: "check",
|
|
@@ -563,7 +565,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
|
|
|
563
565
|
arity: { names: ["mode", "file"] },
|
|
564
566
|
group: "Driver plumbing",
|
|
565
567
|
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.`,
|
|
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.`,
|
|
567
569
|
},
|
|
568
570
|
{
|
|
569
571
|
token: "uncheck",
|
|
@@ -602,13 +604,14 @@ const synopsis = (row: CommandRow): string => {
|
|
|
602
604
|
const { token, arity } = row
|
|
603
605
|
if (arity === "none") return token
|
|
604
606
|
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`
|
|
607
|
+
if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`
|
|
605
608
|
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`
|
|
606
609
|
if ("optional" in arity) return `${token} [${arity.name}]`
|
|
607
610
|
if ("many" in arity) return `${token} [${arity.name}...]`
|
|
608
611
|
return `${token} <${arity.name}>`
|
|
609
612
|
}
|
|
610
613
|
|
|
611
|
-
const SPACE_FORM_FLAGS = new Set(["--
|
|
614
|
+
const SPACE_FORM_FLAGS = new Set(["--var", "--threshold"])
|
|
612
615
|
|
|
613
616
|
const flagHeader = (row: FlagRow): string => {
|
|
614
617
|
if (row.valueHint === "") return row.name
|
|
@@ -618,15 +621,11 @@ const flagHeader = (row: FlagRow): string => {
|
|
|
618
621
|
: `${row.name}=${row.valueHint}`
|
|
619
622
|
}
|
|
620
623
|
|
|
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
624
|
const GLOBAL_OPTIONS: readonly (readonly [string, string])[] = [
|
|
627
625
|
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
628
626
|
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
629
627
|
["-V, --version", "Print the version"],
|
|
628
|
+
["--", "End the options: every word after it is an argument"],
|
|
630
629
|
]
|
|
631
630
|
|
|
632
631
|
const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
@@ -638,7 +637,8 @@ const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
|
638
637
|
["version", "Print the version", "Print gtd's version."],
|
|
639
638
|
]
|
|
640
639
|
|
|
641
|
-
const EXAMPLES = ` $ gtd
|
|
640
|
+
const EXAMPLES = ` $ gtd start list what can start
|
|
641
|
+
$ gtd start <name> start a process
|
|
642
642
|
$ gtd next see what happens next
|
|
643
643
|
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
644
644
|
$ gtd help land one command's details`
|
|
@@ -652,13 +652,11 @@ export const renderHelp = (): string => {
|
|
|
652
652
|
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) =>
|
|
653
653
|
definition(synopsis(r), r.summary, TOP_COLUMN),
|
|
654
654
|
)
|
|
655
|
-
const start =
|
|
656
|
-
group === "Start and stop" ? definition(START_ROW.term, START_ROW.summary, TOP_COLUMN) : []
|
|
657
655
|
const extra =
|
|
658
656
|
group === "Driver plumbing"
|
|
659
657
|
? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN))
|
|
660
658
|
: []
|
|
661
|
-
return `${group}:\n${lines([...
|
|
659
|
+
return `${group}:\n${lines([...rows, ...extra])}`
|
|
662
660
|
})
|
|
663
661
|
return (
|
|
664
662
|
"Usage: gtd <command> [options]\n\n" +
|
|
@@ -674,7 +672,7 @@ export const renderHelp = (): string => {
|
|
|
674
672
|
`\nExamples:\n${EXAMPLES}\n\n` +
|
|
675
673
|
lines(
|
|
676
674
|
wrap(
|
|
677
|
-
"Run `gtd help <command>` for a command's details and options, and `gtd
|
|
675
|
+
"Run `gtd help <command>` for a command's details and options, and `gtd guide` to learn how to drive gtd.",
|
|
678
676
|
),
|
|
679
677
|
)
|
|
680
678
|
)
|
|
@@ -683,7 +681,7 @@ export const renderHelp = (): string => {
|
|
|
683
681
|
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
684
682
|
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
685
683
|
|
|
686
|
-
gtd
|
|
684
|
+
gtd start <name> start a process
|
|
687
685
|
gtd next print what should happen next
|
|
688
686
|
gtd land record what the tree now shows
|
|
689
687
|
|
|
@@ -764,11 +762,13 @@ const tokenize = (tail: readonly string[]): TokenizeError | TokenizeOk => {
|
|
|
764
762
|
const present = new Set<string>()
|
|
765
763
|
let jsonSeen = false
|
|
766
764
|
let afterBareJson: string | undefined
|
|
765
|
+
const end = tail.indexOf("--")
|
|
767
766
|
|
|
768
767
|
for (let i = 0; i < tail.length; i++) {
|
|
769
768
|
const original = tail[i]!
|
|
769
|
+
if (i === end) continue
|
|
770
770
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original
|
|
771
|
-
if (!tok.startsWith("-") || tok === "-") {
|
|
771
|
+
if ((end !== -1 && i > end) || !tok.startsWith("-") || tok === "-") {
|
|
772
772
|
if (tail[i - 1] === "--json") afterBareJson ??= original
|
|
773
773
|
positionals.push(original)
|
|
774
774
|
continue
|
|
@@ -894,7 +894,7 @@ const tooMany = (
|
|
|
894
894
|
const arityError = (cmd: string, rest: readonly string[], arity: Arity): string | undefined => {
|
|
895
895
|
if (arity === "none") return tooMany(cmd, rest, "none", 0)
|
|
896
896
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names)
|
|
897
|
-
if ("many" in arity) return undefined
|
|
897
|
+
if ("many" in arity || "bare" in arity) return undefined
|
|
898
898
|
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1)
|
|
899
899
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`
|
|
900
900
|
return "variadic" in arity ? undefined : tooMany(cmd, rest, `one ${arity.name}`, 1)
|
|
@@ -931,6 +931,27 @@ const decodeFlags = (
|
|
|
931
931
|
return Either.right(bag)
|
|
932
932
|
}
|
|
933
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
|
+
|
|
934
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. */
|
|
935
956
|
const buildLandCommand = (bag: {
|
|
936
957
|
readonly "--cost"?: number
|
|
@@ -943,9 +964,19 @@ const buildLandCommand = (bag: {
|
|
|
943
964
|
|
|
944
965
|
const HELP_FLAGS = new Set(["--help", "-h"])
|
|
945
966
|
|
|
946
|
-
/**
|
|
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. */
|
|
947
974
|
const helpPlan = (argv: readonly string[]): CliPlan => {
|
|
948
|
-
const tokenized = tokenize(
|
|
975
|
+
const tokenized = tokenize(
|
|
976
|
+
optionsOf(argv)
|
|
977
|
+
.slice(2)
|
|
978
|
+
.filter((tok) => !HELP_FLAGS.has(tok)),
|
|
979
|
+
)
|
|
949
980
|
const topic = tokenized.kind === "ok" ? tokenized.positionals : []
|
|
950
981
|
return { kind: "output", stdout: renderTopicHelp(topic) ?? renderHelp() }
|
|
951
982
|
}
|
|
@@ -963,8 +994,9 @@ const helpCommandPlan = (topic: readonly string[], json: boolean): CliPlan => {
|
|
|
963
994
|
|
|
964
995
|
// fallow-ignore-next-line complexity
|
|
965
996
|
export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
966
|
-
|
|
967
|
-
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")) {
|
|
968
1000
|
return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
969
1001
|
}
|
|
970
1002
|
|
|
@@ -973,8 +1005,6 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
973
1005
|
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized
|
|
974
1006
|
|
|
975
1007
|
const first = positionals[0]
|
|
976
|
-
const workflowRaw = byFlag.get("--workflow")?.[0]
|
|
977
|
-
const workflowPresent = workflowRaw !== undefined
|
|
978
1008
|
|
|
979
1009
|
if (first === "version") return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
980
1010
|
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen)
|
|
@@ -1012,31 +1042,17 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1012
1042
|
// `Arity` variant is needed for this one verb.
|
|
1013
1043
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer"
|
|
1014
1044
|
|
|
1015
|
-
// The `--workflow` selector: no command at all, with `--workflow` present,
|
|
1016
|
-
// resolves to the generic `start` command instead — landing and starting
|
|
1017
|
-
// are different verbs, so `gtd land --workflow <name>` is NOT a synonym (it
|
|
1018
|
-
// fails the scope check below instead).
|
|
1019
|
-
const selectsStart = workflowPresent && first === undefined
|
|
1020
1045
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run"
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
: judgeAnswer
|
|
1024
|
-
? "judgeAnswer"
|
|
1025
|
-
: judgeRun
|
|
1026
|
-
? "judgeRun"
|
|
1027
|
-
: row?.kind
|
|
1028
|
-
|
|
1029
|
-
if (row === undefined && !selectsStart) {
|
|
1046
|
+
|
|
1047
|
+
if (row === undefined || first === undefined) {
|
|
1030
1048
|
// No dispatchable row resolved (missing/unknown command) — a scoped flag
|
|
1031
|
-
// used here (e.g.
|
|
1032
|
-
//
|
|
1033
|
-
// the old parser, which validated --workflow/--var/--cost/--model ahead of
|
|
1034
|
-
// 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.
|
|
1035
1051
|
const violation = scopeViolation(undefined, present)
|
|
1036
1052
|
if (violation !== undefined) return usagePlan(violation, jsonSeen)
|
|
1037
1053
|
if (first === undefined) {
|
|
1038
1054
|
return usagePlan(
|
|
1039
|
-
"gtd: missing command — run `gtd help` for every command, or `gtd
|
|
1055
|
+
"gtd: missing command — run `gtd help` for every command, or `gtd guide` to learn how to drive gtd",
|
|
1040
1056
|
jsonSeen,
|
|
1041
1057
|
SHORT_USAGE,
|
|
1042
1058
|
)
|
|
@@ -1047,37 +1063,21 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1047
1063
|
)
|
|
1048
1064
|
}
|
|
1049
1065
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
// `kind === "start"` is unreachable here: `selectsStart` only fires when
|
|
1066
|
-
// `first === undefined`, which the `if` branch above already handled.
|
|
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
|
|
1072
|
-
? undefined
|
|
1073
|
-
: arityError(isHistory ? row!.token : first!, restPositionals, row!.arity)
|
|
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
|
-
}
|
|
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)
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
1083
|
const violation = scopeViolation(kind, present)
|
|
@@ -1094,6 +1094,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1094
1094
|
readonly "--cost"?: number
|
|
1095
1095
|
readonly "--model"?: string
|
|
1096
1096
|
readonly "--var"?: Readonly<Record<string, string>>
|
|
1097
|
+
readonly "--threshold"?: Readonly<Record<string, string>>
|
|
1097
1098
|
readonly "--open-questions"?: boolean
|
|
1098
1099
|
readonly "--open-threads"?: boolean
|
|
1099
1100
|
readonly "--host"?: string
|
|
@@ -1125,28 +1126,20 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1125
1126
|
}
|
|
1126
1127
|
|
|
1127
1128
|
if (kind === "start") {
|
|
1128
|
-
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
|
+
}
|
|
1129
1136
|
return {
|
|
1130
1137
|
kind: "command",
|
|
1131
1138
|
command: {
|
|
1132
1139
|
kind: "start",
|
|
1133
|
-
|
|
1134
|
-
|
|
1140
|
+
...(name !== undefined ? { name } : {}),
|
|
1141
|
+
args,
|
|
1135
1142
|
vars: bag["--var"] ?? {},
|
|
1136
|
-
label,
|
|
1137
|
-
},
|
|
1138
|
-
json,
|
|
1139
|
-
verbose,
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
if (kind === "door") {
|
|
1144
|
-
return {
|
|
1145
|
-
kind: "command",
|
|
1146
|
-
command: {
|
|
1147
|
-
kind: "door",
|
|
1148
|
-
...(restPositionals[0] !== undefined ? { name: restPositionals[0] } : {}),
|
|
1149
|
-
args: restPositionals.slice(1),
|
|
1150
1143
|
},
|
|
1151
1144
|
json,
|
|
1152
1145
|
verbose,
|
|
@@ -1237,23 +1230,9 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1237
1230
|
}
|
|
1238
1231
|
|
|
1239
1232
|
if (kind === "historyJudges") {
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
return usagePlan(
|
|
1244
|
-
`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
1245
|
-
jsonSeen,
|
|
1246
|
-
)
|
|
1247
|
-
}
|
|
1248
|
-
const nan = Object.entries(vars).find(
|
|
1249
|
-
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
1250
|
-
)
|
|
1251
|
-
if (nan !== undefined) {
|
|
1252
|
-
return usagePlan(
|
|
1253
|
-
`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
1254
|
-
jsonSeen,
|
|
1255
|
-
)
|
|
1256
|
-
}
|
|
1233
|
+
const thresholds = judgeThresholds(bag["--threshold"] ?? {})
|
|
1234
|
+
if (Either.isLeft(thresholds)) return usagePlan(thresholds.left, jsonSeen)
|
|
1235
|
+
const vars = thresholds.right
|
|
1257
1236
|
return {
|
|
1258
1237
|
kind: "command",
|
|
1259
1238
|
command: {
|
|
@@ -1315,8 +1294,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1315
1294
|
| "restore"
|
|
1316
1295
|
| "next"
|
|
1317
1296
|
| "validate"
|
|
1318
|
-
| "
|
|
1319
|
-
| "doors"
|
|
1297
|
+
| "guide"
|
|
1320
1298
|
| "base"
|
|
1321
1299
|
| "judge"
|
|
1322
1300
|
| "judgeAnswer"
|
package/src/cli/History.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { historyTrail } from "../edge/index.js"
|
|
|
3
3
|
import { combinedScript, fetchHistoryRefs } from "../emit/index.js"
|
|
4
4
|
import { GitService, Narrator, PROCESS_HISTORY_PREFIX } from "../platform/index.js"
|
|
5
5
|
import { parseCommitMessage } from "../replay/index.js"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
writeJson,
|
|
8
|
+
writeScript,
|
|
9
|
+
type ArtifactOut,
|
|
10
|
+
type CommandRequirements,
|
|
11
|
+
type JsonMode,
|
|
12
|
+
} from "./Output.js"
|
|
7
13
|
|
|
8
14
|
export interface HistoryEntry {
|
|
9
15
|
readonly ref: string
|
|
@@ -138,8 +144,7 @@ export const runHistoryShowCommand = (
|
|
|
138
144
|
/** Prints the script and never runs git: gtd's promise that it only decides and prints. */
|
|
139
145
|
export const runHistoryFetchCommand = (
|
|
140
146
|
remote: string,
|
|
147
|
+
json: JsonMode,
|
|
141
148
|
out: ArtifactOut,
|
|
142
149
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
143
|
-
|
|
144
|
-
out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`)
|
|
145
|
-
})
|
|
150
|
+
writeScript(json, out, `${combinedScript(fetchHistoryRefs(remote), "")}\n`)
|
package/src/cli/Judges.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Effect, Option } from "effect"
|
|
|
2
2
|
import { answered, numeric } from "../flows/index.js"
|
|
3
3
|
import { GitService } from "../platform/index.js"
|
|
4
4
|
import { parseCommitMessage, type JudgeVerdict } from "../replay/index.js"
|
|
5
|
-
import { headingSectionBodies } from "../steering/
|
|
5
|
+
import { headingSectionBodies } from "../steering/internal.js"
|
|
6
6
|
import { selectHistoryEntries, type HistoryEntry } from "./History.js"
|
|
7
7
|
import { printable } from "./Metrics.js"
|
|
8
8
|
import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
|