@pmelab/gtd 59.0.1 → 60.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +48 -32
- 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-BhTey1Do.mjs +30517 -0
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +11285 -19864
- package/dist/gtd.core.mjs +593 -0
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +246 -37
- 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 +7 -7
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +185 -184
- package/src/cli/Cli.ts +219 -224
- 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 +34 -3
- package/src/cli/Program.test.ts +78 -84
- package/src/cli/Program.ts +192 -128
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +72 -62
- package/src/edge/Edge.ts +6 -6
- 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 +189 -55
- 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/install/Version.ts +9 -3
- package/src/install/index.ts +1 -1
- package/src/platform/CommandRunner.ts +13 -11
- package/src/platform/Commentary.ts +2 -2
- package/src/platform/Git.test.ts +4 -5
- package/src/platform/Git.ts +42 -38
- package/src/platform/Host.ts +1 -1
- package/src/platform/NodeServices.ts +11 -0
- package/src/platform/Workspace.test.ts +4 -5
- package/src/platform/Workspace.ts +3 -3
- package/src/platform/WorktreeState.test.ts +2 -3
- package/src/platform/WorktreeState.ts +1 -1
- package/src/platform/index.ts +2 -1
- 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 +12 -12
- 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 +5 -3
- package/src/twins/GitTiers.ts +6 -7
- package/src/twins/InMemRepo.ts +1 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/cliIo.ts +9 -4
- 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 +27 -10
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +316 -119
- package/src/workflow/load.ts +478 -305
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- 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/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
package/src/cli/Cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Cause, Effect, Either, Layer } from "effect"
|
|
1
|
+
import { Cause, Effect, Layer, Result } from "effect"
|
|
3
2
|
import {
|
|
4
3
|
GtdError,
|
|
5
4
|
GtdUsageError,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
Host,
|
|
10
9
|
Workspace,
|
|
11
10
|
CommandRunner,
|
|
11
|
+
NodeServices,
|
|
12
12
|
} from "../platform/index.js"
|
|
13
13
|
import {
|
|
14
14
|
flagSettings,
|
|
@@ -20,6 +20,7 @@ import { GTD_VERSION } from "../install/index.js"
|
|
|
20
20
|
import {
|
|
21
21
|
ConfigDiscovery,
|
|
22
22
|
ConfigService,
|
|
23
|
+
EngineCore,
|
|
23
24
|
isSettingName,
|
|
24
25
|
SETTING_NAME_RULE,
|
|
25
26
|
StartFlags,
|
|
@@ -75,7 +76,7 @@ interface FlagRow {
|
|
|
75
76
|
readonly repeatable: boolean
|
|
76
77
|
readonly scope: FlagScope
|
|
77
78
|
/** All raw occurrences (already de-`=`-ed) → the decoded value, or an error message. */
|
|
78
|
-
readonly decode: (raws: readonly string[]) =>
|
|
79
|
+
readonly decode: (raws: readonly string[]) => Result.Result<unknown, string>
|
|
79
80
|
readonly scopeError: string
|
|
80
81
|
readonly valueHint: string
|
|
81
82
|
/** One sentence for a command's help, wrapped when rendered; `helpFor` overrides it for a command the flag means something else on. */
|
|
@@ -85,20 +86,63 @@ interface FlagRow {
|
|
|
85
86
|
readonly conflicts?: readonly string[]
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
/** `<name>=<value>` occurrences of `flag` → one record, each name a setting name given at most once. */
|
|
90
|
+
const decodeAssignments = (
|
|
91
|
+
flag: string,
|
|
92
|
+
raws: readonly string[],
|
|
93
|
+
): Result.Result<Readonly<Record<string, string>>, string> => {
|
|
94
|
+
const vars: Record<string, string> = {}
|
|
95
|
+
for (const raw of raws) {
|
|
96
|
+
const eq = raw.indexOf("=")
|
|
97
|
+
if (eq <= 0) {
|
|
98
|
+
return Result.fail(`gtd: ${flag} must be <name>=<value> with a non-empty name — got "${raw}"`)
|
|
99
|
+
}
|
|
100
|
+
const name = raw.slice(0, eq)
|
|
101
|
+
const value = raw.slice(eq + 1)
|
|
102
|
+
if (!isSettingName(name)) {
|
|
103
|
+
return Result.fail(
|
|
104
|
+
`gtd: ${flag} ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
if (/[\r\n]/.test(value)) return Result.fail(`gtd: ${flag} ${name} must be a single-line value`)
|
|
108
|
+
if (Object.hasOwn(vars, name))
|
|
109
|
+
return Result.fail(`gtd: ${flag} ${name} specified more than once`)
|
|
110
|
+
if (!name.startsWith("flag_")) {
|
|
111
|
+
vars[name] = value
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
const pinned = flagSettingValue(value)
|
|
115
|
+
if (pinned === undefined) return Result.fail(`gtd: ${flag} ${name} must be 1, 0, true or false`)
|
|
116
|
+
vars[name] = pinned
|
|
117
|
+
}
|
|
118
|
+
return Result.succeed(vars)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const nonNegativeNumber = (raw: string, flag: string): Result.Result<number, string> => {
|
|
89
122
|
const n = Number(raw)
|
|
90
123
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) {
|
|
91
|
-
return
|
|
124
|
+
return Result.fail(`gtd: ${flag} must be a non-negative number — got "${raw}"`)
|
|
92
125
|
}
|
|
93
|
-
return
|
|
126
|
+
return Result.succeed(n)
|
|
94
127
|
}
|
|
95
128
|
|
|
129
|
+
/** The commands whose only output is a script: `--json` prints `{ schema, script }`. */
|
|
130
|
+
const SCRIPT_KINDS: ReadonlySet<Command["kind"]> = new Set<Command["kind"]>([
|
|
131
|
+
"start",
|
|
132
|
+
"abandon",
|
|
133
|
+
"restore",
|
|
134
|
+
"validate",
|
|
135
|
+
"historyFetch",
|
|
136
|
+
])
|
|
137
|
+
|
|
138
|
+
const SCRIPT_JSON_HELP = "Print { schema, script }, or with --json=script the script alone"
|
|
139
|
+
|
|
96
140
|
const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["kind"]>([
|
|
141
|
+
...SCRIPT_KINDS,
|
|
97
142
|
"next",
|
|
98
143
|
"land",
|
|
99
144
|
"judge",
|
|
100
145
|
"judgeAnswer",
|
|
101
|
-
"doors",
|
|
102
146
|
"historyList",
|
|
103
147
|
"historyShow",
|
|
104
148
|
"historyMetrics",
|
|
@@ -112,14 +156,16 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
112
156
|
arity: "optional",
|
|
113
157
|
repeatable: false,
|
|
114
158
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
115
|
-
decode: ([raw]) =>
|
|
159
|
+
decode: ([raw]) => Result.succeed(raw ?? ""),
|
|
116
160
|
scopeError:
|
|
117
|
-
"gtd: --json is only valid for `gtd next`/`gtd land
|
|
118
|
-
"every other command prints plain text; see `gtd
|
|
161
|
+
"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`) — " +
|
|
162
|
+
"every other command prints plain text; see `gtd guide` for the driver protocol briefing",
|
|
119
163
|
valueHint: "<path>",
|
|
120
164
|
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
165
|
helpFor: {
|
|
122
166
|
strays: "Print one entry of the array (--json=0), nothing past its end",
|
|
167
|
+
...Object.fromEntries([...SCRIPT_KINDS].map((kind) => [kind, SCRIPT_JSON_HELP])),
|
|
168
|
+
start: `${SCRIPT_JSON_HELP}. Bare, print the list as a JSON array of { kind, name } (a door adds workflow and args)`,
|
|
123
169
|
},
|
|
124
170
|
},
|
|
125
171
|
{
|
|
@@ -130,8 +176,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
130
176
|
decode: ([raw]) => {
|
|
131
177
|
const n = Number(raw)
|
|
132
178
|
return raw !== undefined && Number.isInteger(n) && n >= 0 && n <= 65535
|
|
133
|
-
?
|
|
134
|
-
:
|
|
179
|
+
? Result.succeed(n)
|
|
180
|
+
: Result.fail(`gtd: --port must be an integer 0–65535 (got '${raw ?? ""}')`)
|
|
135
181
|
},
|
|
136
182
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
137
183
|
valueHint: "<n>",
|
|
@@ -144,8 +190,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
144
190
|
scope: (kind) => kind === "ui",
|
|
145
191
|
decode: ([raw]) =>
|
|
146
192
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
147
|
-
?
|
|
148
|
-
:
|
|
193
|
+
? Result.fail("gtd: --host must be a non-empty, single-line value")
|
|
194
|
+
: Result.succeed(raw),
|
|
149
195
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
150
196
|
valueHint: "<addr>",
|
|
151
197
|
help: "Bind this address directly instead of publishing through tailscale serve",
|
|
@@ -155,7 +201,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
155
201
|
arity: 0,
|
|
156
202
|
repeatable: false,
|
|
157
203
|
scope: (kind) => kind === "ui",
|
|
158
|
-
decode: () =>
|
|
204
|
+
decode: () => Result.succeed(true),
|
|
159
205
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
160
206
|
valueHint: "",
|
|
161
207
|
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key",
|
|
@@ -165,7 +211,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
165
211
|
arity: 0,
|
|
166
212
|
repeatable: false,
|
|
167
213
|
scope: (kind) => kind === "ui",
|
|
168
|
-
decode: () =>
|
|
214
|
+
decode: () => Result.succeed(true),
|
|
169
215
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
170
216
|
valueHint: "",
|
|
171
217
|
help: "Serve local development sources instead of the packaged build",
|
|
@@ -177,8 +223,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
177
223
|
scope: (kind) => kind === "judgeRun",
|
|
178
224
|
decode: ([raw]) =>
|
|
179
225
|
raw === "fixed" || raw === "jev" || raw === "llm"
|
|
180
|
-
?
|
|
181
|
-
:
|
|
226
|
+
? Result.succeed(raw)
|
|
227
|
+
: Result.fail(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
182
228
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
183
229
|
valueHint: "<name>",
|
|
184
230
|
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)",
|
|
@@ -190,8 +236,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
190
236
|
scope: (kind) => kind === "judgeRun",
|
|
191
237
|
decode: ([raw]) =>
|
|
192
238
|
raw === undefined || raw === ""
|
|
193
|
-
?
|
|
194
|
-
:
|
|
239
|
+
? Result.fail("gtd: --answers must be a non-empty path")
|
|
240
|
+
: Result.succeed(raw),
|
|
195
241
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
196
242
|
valueHint: "<path>",
|
|
197
243
|
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)",
|
|
@@ -213,96 +259,55 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
213
259
|
scope: (kind) => kind === "land" || kind === "judgeRun",
|
|
214
260
|
decode: ([raw]) =>
|
|
215
261
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
216
|
-
?
|
|
217
|
-
:
|
|
218
|
-
scopeError:
|
|
262
|
+
? Result.fail("gtd: --model must be a non-empty, single-line value")
|
|
263
|
+
: Result.succeed(raw),
|
|
264
|
+
scopeError:
|
|
265
|
+
"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
266
|
valueHint: "<name>",
|
|
220
267
|
help: "",
|
|
221
268
|
helpFor: {
|
|
222
|
-
land: "With --cost: the model that cost was spent on",
|
|
269
|
+
land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
|
|
223
270
|
judgeRun:
|
|
224
271
|
"The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)",
|
|
225
272
|
},
|
|
226
273
|
},
|
|
227
274
|
{
|
|
228
|
-
name: "--
|
|
275
|
+
name: "--threshold",
|
|
229
276
|
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",
|
|
277
|
+
repeatable: true,
|
|
278
|
+
scope: (kind) => kind === "historyJudges",
|
|
279
|
+
decode: (raws) => decodeAssignments("--threshold", raws),
|
|
280
|
+
scopeError: "gtd: --threshold is only valid for `gtd history judges`",
|
|
281
|
+
valueHint: "<name>=<value>",
|
|
282
|
+
help: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)",
|
|
238
283
|
},
|
|
239
284
|
{
|
|
240
285
|
name: "--var",
|
|
241
286
|
arity: 1,
|
|
242
287
|
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`)",
|
|
288
|
+
scope: (kind) => kind === "start",
|
|
289
|
+
decode: (raws) => decodeAssignments("--var", raws),
|
|
290
|
+
scopeError: "gtd: --var is only valid for `gtd start <name>`",
|
|
281
291
|
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
|
-
},
|
|
292
|
+
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
293
|
},
|
|
289
294
|
{
|
|
290
295
|
name: "--jsonl",
|
|
291
296
|
arity: 0,
|
|
292
297
|
repeatable: false,
|
|
293
298
|
scope: (kind) => kind === "historyJudges",
|
|
294
|
-
decode: () =>
|
|
299
|
+
decode: () => Result.succeed(true),
|
|
295
300
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
296
301
|
valueHint: "",
|
|
297
302
|
help: "Print one JSON line per verdict instead of the report",
|
|
298
|
-
conflicts: ["--json", "--
|
|
303
|
+
conflicts: ["--json", "--threshold"],
|
|
299
304
|
},
|
|
300
305
|
{
|
|
301
306
|
name: "--open-questions",
|
|
302
307
|
arity: 0,
|
|
303
308
|
repeatable: false,
|
|
304
309
|
scope: (kind) => kind === "check",
|
|
305
|
-
decode: () =>
|
|
310
|
+
decode: () => Result.succeed(true),
|
|
306
311
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
307
312
|
valueHint: "",
|
|
308
313
|
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain",
|
|
@@ -312,7 +317,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
312
317
|
arity: 0,
|
|
313
318
|
repeatable: false,
|
|
314
319
|
scope: (kind) => kind === "check",
|
|
315
|
-
decode: () =>
|
|
320
|
+
decode: () => Result.succeed(true),
|
|
316
321
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
317
322
|
valueHint: "",
|
|
318
323
|
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",
|
|
@@ -322,7 +327,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
322
327
|
arity: 0,
|
|
323
328
|
repeatable: false,
|
|
324
329
|
scope: () => true,
|
|
325
|
-
decode: () =>
|
|
330
|
+
decode: () => Result.succeed(true),
|
|
326
331
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
327
332
|
valueHint: "",
|
|
328
333
|
help: "Narrate on stderr what gtd resolved and where its config came from",
|
|
@@ -356,6 +361,8 @@ type Arity =
|
|
|
356
361
|
| { readonly names: readonly [string, string] }
|
|
357
362
|
// One required leading positional, then any number the command itself judges.
|
|
358
363
|
| { readonly name: string; readonly variadic: string }
|
|
364
|
+
// The same, the leading one optional too.
|
|
365
|
+
| { readonly name: string; readonly variadic: string; readonly bare: true }
|
|
359
366
|
// At most one trailing positional.
|
|
360
367
|
| { readonly name: string; readonly optional: true }
|
|
361
368
|
// Any number of positionals, none included.
|
|
@@ -407,25 +414,21 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
407
414
|
arity: "none",
|
|
408
415
|
group: "Core loop",
|
|
409
416
|
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.`,
|
|
417
|
+
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
418
|
},
|
|
412
419
|
{
|
|
413
|
-
token: "
|
|
414
|
-
kind: "
|
|
415
|
-
arity: { name: "name", variadic: "args" },
|
|
420
|
+
token: "start",
|
|
421
|
+
kind: "start",
|
|
422
|
+
arity: { name: "name", variadic: "args", bare: true },
|
|
416
423
|
group: "Start and stop",
|
|
417
|
-
summary: "Start a
|
|
418
|
-
details: `Start a process
|
|
424
|
+
summary: "Start a workflow or door; bare, list them",
|
|
425
|
+
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
419
426
|
|
|
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.`,
|
|
427
|
+
<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.
|
|
428
|
+
|
|
429
|
+
s=$(gtd start <name> --json=script) && sh -c "$s"
|
|
430
|
+
|
|
431
|
+
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
432
|
},
|
|
430
433
|
{
|
|
431
434
|
token: "abandon",
|
|
@@ -435,7 +438,7 @@ With no <name>, an unknown one, or the wrong number of args, it lists the doors
|
|
|
435
438
|
summary: "Stop the process, keeping its changes",
|
|
436
439
|
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
440
|
|
|
438
|
-
s=$(gtd abandon) && sh -c "$s"`,
|
|
441
|
+
s=$(gtd abandon --json=script) && sh -c "$s"`,
|
|
439
442
|
},
|
|
440
443
|
{
|
|
441
444
|
token: "restore",
|
|
@@ -513,7 +516,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
513
516
|
summary: "Print a script that fetches finished processes",
|
|
514
517
|
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
518
|
|
|
516
|
-
s=$(gtd history fetch) && sh -c "$s"`,
|
|
519
|
+
s=$(gtd history fetch --json=script) && sh -c "$s"`,
|
|
517
520
|
},
|
|
518
521
|
{
|
|
519
522
|
token: "ui",
|
|
@@ -524,13 +527,21 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
524
527
|
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
528
|
},
|
|
526
529
|
{
|
|
527
|
-
token: "
|
|
528
|
-
kind: "
|
|
530
|
+
token: "guide",
|
|
531
|
+
kind: "guide",
|
|
529
532
|
arity: "none",
|
|
530
533
|
group: "Driver plumbing",
|
|
531
534
|
summary: "Print a briefing for writing a driver",
|
|
532
535
|
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
536
|
},
|
|
537
|
+
{
|
|
538
|
+
token: "bundle",
|
|
539
|
+
kind: "bundle",
|
|
540
|
+
arity: "none",
|
|
541
|
+
group: "Driver plumbing",
|
|
542
|
+
summary: "Print the pin of the current workflow",
|
|
543
|
+
details: `Print a pin to stdout: gtd's engine and the current workflow — gtd.config.ts, every module it imports and every npm package they use — as one gzipped ES module, after a plain-text header line \`gtdpin <format> gtd <version>\`. Git diffs it as binary. Refuses a workflow gtd would refuse, and a package that loads a native addon (.node), naming it. Writes nothing.`,
|
|
544
|
+
},
|
|
534
545
|
{
|
|
535
546
|
token: "judge",
|
|
536
547
|
kind: "judge",
|
|
@@ -555,7 +566,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
555
566
|
summary: "Answer a judgment with a model",
|
|
556
567
|
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
568
|
|
|
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.`,
|
|
569
|
+
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
570
|
},
|
|
560
571
|
{
|
|
561
572
|
token: "check",
|
|
@@ -563,7 +574,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
|
|
|
563
574
|
arity: { names: ["mode", "file"] },
|
|
564
575
|
group: "Driver plumbing",
|
|
565
576
|
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.`,
|
|
577
|
+
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
578
|
},
|
|
568
579
|
{
|
|
569
580
|
token: "uncheck",
|
|
@@ -602,13 +613,14 @@ const synopsis = (row: CommandRow): string => {
|
|
|
602
613
|
const { token, arity } = row
|
|
603
614
|
if (arity === "none") return token
|
|
604
615
|
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`
|
|
616
|
+
if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`
|
|
605
617
|
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`
|
|
606
618
|
if ("optional" in arity) return `${token} [${arity.name}]`
|
|
607
619
|
if ("many" in arity) return `${token} [${arity.name}...]`
|
|
608
620
|
return `${token} <${arity.name}>`
|
|
609
621
|
}
|
|
610
622
|
|
|
611
|
-
const SPACE_FORM_FLAGS = new Set(["--
|
|
623
|
+
const SPACE_FORM_FLAGS = new Set(["--var", "--threshold"])
|
|
612
624
|
|
|
613
625
|
const flagHeader = (row: FlagRow): string => {
|
|
614
626
|
if (row.valueHint === "") return row.name
|
|
@@ -618,15 +630,11 @@ const flagHeader = (row: FlagRow): string => {
|
|
|
618
630
|
: `${row.name}=${row.valueHint}`
|
|
619
631
|
}
|
|
620
632
|
|
|
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
633
|
const GLOBAL_OPTIONS: readonly (readonly [string, string])[] = [
|
|
627
634
|
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
628
635
|
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
629
636
|
["-V, --version", "Print the version"],
|
|
637
|
+
["--", "End the options: every word after it is an argument"],
|
|
630
638
|
]
|
|
631
639
|
|
|
632
640
|
const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
@@ -638,7 +646,8 @@ const EXTRA_TOPICS: readonly (readonly [string, string, string])[] = [
|
|
|
638
646
|
["version", "Print the version", "Print gtd's version."],
|
|
639
647
|
]
|
|
640
648
|
|
|
641
|
-
const EXAMPLES = ` $ gtd
|
|
649
|
+
const EXAMPLES = ` $ gtd start list what can start
|
|
650
|
+
$ gtd start <name> start a process
|
|
642
651
|
$ gtd next see what happens next
|
|
643
652
|
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
644
653
|
$ gtd help land one command's details`
|
|
@@ -652,13 +661,11 @@ export const renderHelp = (): string => {
|
|
|
652
661
|
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) =>
|
|
653
662
|
definition(synopsis(r), r.summary, TOP_COLUMN),
|
|
654
663
|
)
|
|
655
|
-
const start =
|
|
656
|
-
group === "Start and stop" ? definition(START_ROW.term, START_ROW.summary, TOP_COLUMN) : []
|
|
657
664
|
const extra =
|
|
658
665
|
group === "Driver plumbing"
|
|
659
666
|
? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN))
|
|
660
667
|
: []
|
|
661
|
-
return `${group}:\n${lines([...
|
|
668
|
+
return `${group}:\n${lines([...rows, ...extra])}`
|
|
662
669
|
})
|
|
663
670
|
return (
|
|
664
671
|
"Usage: gtd <command> [options]\n\n" +
|
|
@@ -674,7 +681,7 @@ export const renderHelp = (): string => {
|
|
|
674
681
|
`\nExamples:\n${EXAMPLES}\n\n` +
|
|
675
682
|
lines(
|
|
676
683
|
wrap(
|
|
677
|
-
"Run `gtd help <command>` for a command's details and options, and `gtd
|
|
684
|
+
"Run `gtd help <command>` for a command's details and options, and `gtd guide` to learn how to drive gtd.",
|
|
678
685
|
),
|
|
679
686
|
)
|
|
680
687
|
)
|
|
@@ -683,7 +690,7 @@ export const renderHelp = (): string => {
|
|
|
683
690
|
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
684
691
|
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
685
692
|
|
|
686
|
-
gtd
|
|
693
|
+
gtd start <name> start a process
|
|
687
694
|
gtd next print what should happen next
|
|
688
695
|
gtd land record what the tree now shows
|
|
689
696
|
|
|
@@ -764,11 +771,13 @@ const tokenize = (tail: readonly string[]): TokenizeError | TokenizeOk => {
|
|
|
764
771
|
const present = new Set<string>()
|
|
765
772
|
let jsonSeen = false
|
|
766
773
|
let afterBareJson: string | undefined
|
|
774
|
+
const end = tail.indexOf("--")
|
|
767
775
|
|
|
768
776
|
for (let i = 0; i < tail.length; i++) {
|
|
769
777
|
const original = tail[i]!
|
|
778
|
+
if (i === end) continue
|
|
770
779
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original
|
|
771
|
-
if (!tok.startsWith("-") || tok === "-") {
|
|
780
|
+
if ((end !== -1 && i > end) || !tok.startsWith("-") || tok === "-") {
|
|
772
781
|
if (tail[i - 1] === "--json") afterBareJson ??= original
|
|
773
782
|
positionals.push(original)
|
|
774
783
|
continue
|
|
@@ -894,7 +903,7 @@ const tooMany = (
|
|
|
894
903
|
const arityError = (cmd: string, rest: readonly string[], arity: Arity): string | undefined => {
|
|
895
904
|
if (arity === "none") return tooMany(cmd, rest, "none", 0)
|
|
896
905
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names)
|
|
897
|
-
if ("many" in arity) return undefined
|
|
906
|
+
if ("many" in arity || "bare" in arity) return undefined
|
|
898
907
|
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1)
|
|
899
908
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`
|
|
900
909
|
return "variadic" in arity ? undefined : tooMany(cmd, rest, `one ${arity.name}`, 1)
|
|
@@ -916,7 +925,7 @@ const pairArityError = (
|
|
|
916
925
|
/** Decode every present flag into a bag of values, or the first decode error encountered. */
|
|
917
926
|
const decodeFlags = (
|
|
918
927
|
byFlag: ReadonlyMap<string, readonly string[]>,
|
|
919
|
-
):
|
|
928
|
+
): Result.Result<Readonly<Record<string, unknown>>, string> => {
|
|
920
929
|
const bag: Record<string, unknown> = {}
|
|
921
930
|
for (const row of FLAGS) {
|
|
922
931
|
const raws = byFlag.get(row.name)
|
|
@@ -925,10 +934,31 @@ const decodeFlags = (
|
|
|
925
934
|
// to decode — presence alone is tracked via `present`, not the bag.
|
|
926
935
|
if (row.arity === "optional" && raws.length === 0) continue
|
|
927
936
|
const decoded = row.decode(raws)
|
|
928
|
-
if (
|
|
929
|
-
bag[row.name] = decoded.
|
|
937
|
+
if (Result.isFailure(decoded)) return Result.fail(decoded.failure)
|
|
938
|
+
bag[row.name] = decoded.success
|
|
930
939
|
}
|
|
931
|
-
return
|
|
940
|
+
return Result.succeed(bag)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** `gtd history judges`' thresholds: each a known judge threshold with a numeric value. */
|
|
944
|
+
const judgeThresholds = (
|
|
945
|
+
thresholds: Readonly<Record<string, string>>,
|
|
946
|
+
): Result.Result<Readonly<Record<string, string>>, string> => {
|
|
947
|
+
const unknown = Object.keys(thresholds).find((name) => !Object.hasOwn(THRESHOLDS, name))
|
|
948
|
+
if (unknown !== undefined) {
|
|
949
|
+
return Result.fail(
|
|
950
|
+
`gtd history judges: --threshold ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
const nan = Object.entries(thresholds).find(
|
|
954
|
+
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
955
|
+
)
|
|
956
|
+
if (nan !== undefined) {
|
|
957
|
+
return Result.fail(
|
|
958
|
+
`gtd history judges: --threshold ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
return Result.succeed(thresholds)
|
|
932
962
|
}
|
|
933
963
|
|
|
934
964
|
/** 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. */
|
|
@@ -943,9 +973,19 @@ const buildLandCommand = (bag: {
|
|
|
943
973
|
|
|
944
974
|
const HELP_FLAGS = new Set(["--help", "-h"])
|
|
945
975
|
|
|
946
|
-
/**
|
|
976
|
+
/** The options part of argv: everything before a `--`, which ends the options. */
|
|
977
|
+
const optionsOf = (argv: readonly string[]): readonly string[] => {
|
|
978
|
+
const end = argv.indexOf("--", 2)
|
|
979
|
+
return end === -1 ? argv : argv.slice(0, end)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** `--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
983
|
const helpPlan = (argv: readonly string[]): CliPlan => {
|
|
948
|
-
const tokenized = tokenize(
|
|
984
|
+
const tokenized = tokenize(
|
|
985
|
+
optionsOf(argv)
|
|
986
|
+
.slice(2)
|
|
987
|
+
.filter((tok) => !HELP_FLAGS.has(tok)),
|
|
988
|
+
)
|
|
949
989
|
const topic = tokenized.kind === "ok" ? tokenized.positionals : []
|
|
950
990
|
return { kind: "output", stdout: renderTopicHelp(topic) ?? renderHelp() }
|
|
951
991
|
}
|
|
@@ -963,8 +1003,9 @@ const helpCommandPlan = (topic: readonly string[], json: boolean): CliPlan => {
|
|
|
963
1003
|
|
|
964
1004
|
// fallow-ignore-next-line complexity
|
|
965
1005
|
export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
966
|
-
|
|
967
|
-
if (
|
|
1006
|
+
const options = optionsOf(argv)
|
|
1007
|
+
if (options.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv)
|
|
1008
|
+
if (options.includes("--version") || options.includes("-V")) {
|
|
968
1009
|
return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
969
1010
|
}
|
|
970
1011
|
|
|
@@ -973,8 +1014,6 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
973
1014
|
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized
|
|
974
1015
|
|
|
975
1016
|
const first = positionals[0]
|
|
976
|
-
const workflowRaw = byFlag.get("--workflow")?.[0]
|
|
977
|
-
const workflowPresent = workflowRaw !== undefined
|
|
978
1017
|
|
|
979
1018
|
if (first === "version") return { kind: "output", stdout: `${GTD_VERSION}\n` }
|
|
980
1019
|
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen)
|
|
@@ -1012,31 +1051,17 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1012
1051
|
// `Arity` variant is needed for this one verb.
|
|
1013
1052
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer"
|
|
1014
1053
|
|
|
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
1054
|
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) {
|
|
1055
|
+
|
|
1056
|
+
if (row === undefined || first === undefined) {
|
|
1030
1057
|
// 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).
|
|
1058
|
+
// used here (e.g. a bare `gtd --var x=1`) is a more specific error than
|
|
1059
|
+
// "missing command"/"unknown command", so it takes priority.
|
|
1035
1060
|
const violation = scopeViolation(undefined, present)
|
|
1036
1061
|
if (violation !== undefined) return usagePlan(violation, jsonSeen)
|
|
1037
1062
|
if (first === undefined) {
|
|
1038
1063
|
return usagePlan(
|
|
1039
|
-
"gtd: missing command — run `gtd help` for every command, or `gtd
|
|
1064
|
+
"gtd: missing command — run `gtd help` for every command, or `gtd guide` to learn how to drive gtd",
|
|
1040
1065
|
jsonSeen,
|
|
1041
1066
|
SHORT_USAGE,
|
|
1042
1067
|
)
|
|
@@ -1047,37 +1072,21 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1047
1072
|
)
|
|
1048
1073
|
}
|
|
1049
1074
|
|
|
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
|
-
}
|
|
1075
|
+
const kind: Command["kind"] = judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row.kind
|
|
1076
|
+
|
|
1077
|
+
// `judgeAnswer` consumes TWO leading positionals ("judge" and "answer"), everything else one.
|
|
1078
|
+
const restPositionals = positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1)
|
|
1079
|
+
|
|
1080
|
+
const bare = kind === "check" && restPositionals.length === 0 && present.has("--open-threads")
|
|
1081
|
+
const arityMsg = bare
|
|
1082
|
+
? undefined
|
|
1083
|
+
: arityError(isHistory ? row.token : first, restPositionals, row.arity)
|
|
1084
|
+
if (arityMsg !== undefined) {
|
|
1085
|
+
const hint =
|
|
1086
|
+
afterBareJson !== undefined && restPositionals.includes(afterBareJson)
|
|
1087
|
+
? ` — to print one field, write --json=${afterBareJson}`
|
|
1088
|
+
: ""
|
|
1089
|
+
return usagePlan(arityMsg + hint, jsonSeen)
|
|
1081
1090
|
}
|
|
1082
1091
|
|
|
1083
1092
|
const violation = scopeViolation(kind, present)
|
|
@@ -1087,13 +1096,14 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1087
1096
|
if (conflict !== undefined) return usagePlan(conflict, jsonSeen)
|
|
1088
1097
|
|
|
1089
1098
|
const decoded = decodeFlags(byFlag)
|
|
1090
|
-
if (
|
|
1091
|
-
const bag = decoded.
|
|
1099
|
+
if (Result.isFailure(decoded)) return usagePlan(decoded.failure, jsonSeen)
|
|
1100
|
+
const bag = decoded.success as {
|
|
1092
1101
|
readonly "--json"?: string
|
|
1093
1102
|
readonly "--port"?: number
|
|
1094
1103
|
readonly "--cost"?: number
|
|
1095
1104
|
readonly "--model"?: string
|
|
1096
1105
|
readonly "--var"?: Readonly<Record<string, string>>
|
|
1106
|
+
readonly "--threshold"?: Readonly<Record<string, string>>
|
|
1097
1107
|
readonly "--open-questions"?: boolean
|
|
1098
1108
|
readonly "--open-threads"?: boolean
|
|
1099
1109
|
readonly "--host"?: string
|
|
@@ -1125,28 +1135,20 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1125
1135
|
}
|
|
1126
1136
|
|
|
1127
1137
|
if (kind === "start") {
|
|
1128
|
-
const
|
|
1138
|
+
const [name, ...args] = restPositionals
|
|
1139
|
+
if (name === undefined && bag["--var"] !== undefined) {
|
|
1140
|
+
return usagePlan(
|
|
1141
|
+
"gtd start: --var needs a <name> — run `gtd start` to list the workflows and doors",
|
|
1142
|
+
jsonSeen,
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1129
1145
|
return {
|
|
1130
1146
|
kind: "command",
|
|
1131
1147
|
command: {
|
|
1132
1148
|
kind: "start",
|
|
1133
|
-
|
|
1134
|
-
|
|
1149
|
+
...(name !== undefined ? { name } : {}),
|
|
1150
|
+
args,
|
|
1135
1151
|
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
1152
|
},
|
|
1151
1153
|
json,
|
|
1152
1154
|
verbose,
|
|
@@ -1237,23 +1239,9 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1237
1239
|
}
|
|
1238
1240
|
|
|
1239
1241
|
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
|
-
}
|
|
1242
|
+
const thresholds = judgeThresholds(bag["--threshold"] ?? {})
|
|
1243
|
+
if (Result.isFailure(thresholds)) return usagePlan(thresholds.failure, jsonSeen)
|
|
1244
|
+
const vars = thresholds.success
|
|
1257
1245
|
return {
|
|
1258
1246
|
kind: "command",
|
|
1259
1247
|
command: {
|
|
@@ -1315,8 +1303,8 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1315
1303
|
| "restore"
|
|
1316
1304
|
| "next"
|
|
1317
1305
|
| "validate"
|
|
1318
|
-
| "
|
|
1319
|
-
| "
|
|
1306
|
+
| "guide"
|
|
1307
|
+
| "bundle"
|
|
1320
1308
|
| "base"
|
|
1321
1309
|
| "judge"
|
|
1322
1310
|
| "judgeAnswer"
|
|
@@ -1330,7 +1318,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1330
1318
|
// ---------------------------------------------------------------------------
|
|
1331
1319
|
|
|
1332
1320
|
export interface CliIo {
|
|
1333
|
-
readonly stdout: (chunk: string) => void
|
|
1321
|
+
readonly stdout: (chunk: string | Uint8Array) => void
|
|
1334
1322
|
readonly stderr: (chunk: string) => void
|
|
1335
1323
|
readonly exit: (code: number) => void
|
|
1336
1324
|
/**
|
|
@@ -1355,7 +1343,7 @@ const writeStderr = (chunk: string): void => {
|
|
|
1355
1343
|
export type LiveToolCommands = Layer.Layer<
|
|
1356
1344
|
ToolCommands,
|
|
1357
1345
|
never,
|
|
1358
|
-
Exclude<CommandRequirements, ToolCommands> |
|
|
1346
|
+
Exclude<CommandRequirements, ToolCommands> | NodeServices.NodeServices
|
|
1359
1347
|
>
|
|
1360
1348
|
|
|
1361
1349
|
export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
@@ -1380,20 +1368,21 @@ export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
|
1380
1368
|
// "command"`, so --version/--help/a usage error never provoke it.
|
|
1381
1369
|
layers: (verbose) =>
|
|
1382
1370
|
// Dependency order matters: Workspace.Live needs GitService, both need
|
|
1383
|
-
// Host, and GitService needs
|
|
1371
|
+
// Host, and GitService needs NodeServices' ChildProcessSpawner. Each
|
|
1384
1372
|
// `provideMerge` satisfies the layers above it AND stays in the output.
|
|
1385
1373
|
tools.pipe(
|
|
1386
1374
|
Layer.provideMerge(
|
|
1387
1375
|
Layer.mergeAll(
|
|
1388
1376
|
ConfigService.Live,
|
|
1389
1377
|
ConfigDiscovery.Live,
|
|
1378
|
+
EngineCore.Live,
|
|
1390
1379
|
Workspace.Live,
|
|
1391
1380
|
Narrator.layer(writeStderr, verbose),
|
|
1392
1381
|
CommandRunner.Live,
|
|
1393
1382
|
).pipe(
|
|
1394
1383
|
Layer.provideMerge(GitService.Live),
|
|
1395
1384
|
Layer.provideMerge(Host.Live),
|
|
1396
|
-
Layer.provideMerge(
|
|
1385
|
+
Layer.provideMerge(NodeServices.layer),
|
|
1397
1386
|
),
|
|
1398
1387
|
),
|
|
1399
1388
|
),
|
|
@@ -1408,14 +1397,20 @@ export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
|
1408
1397
|
export const normalizeTrailingNewline = (artifact: string): string =>
|
|
1409
1398
|
artifact.length === 0 ? artifact : `${artifact.replace(/\n+$/, "")}\n`
|
|
1410
1399
|
|
|
1400
|
+
// Bytes are a binary artifact, written as they are.
|
|
1411
1401
|
const bufferedArtifactOut = (io: CliIo): ArtifactOut => {
|
|
1412
|
-
let buffer = ""
|
|
1402
|
+
let buffer: string | Uint8Array = ""
|
|
1413
1403
|
return {
|
|
1414
1404
|
write: (chunk) => {
|
|
1415
|
-
buffer
|
|
1405
|
+
buffer =
|
|
1406
|
+
typeof buffer === "string" && typeof chunk === "string"
|
|
1407
|
+
? buffer + chunk
|
|
1408
|
+
: Buffer.concat([Buffer.from(buffer), Buffer.from(chunk)])
|
|
1416
1409
|
},
|
|
1417
1410
|
flush: () => {
|
|
1418
|
-
if (buffer.length > 0)
|
|
1411
|
+
if (buffer.length > 0) {
|
|
1412
|
+
io.stdout(typeof buffer === "string" ? normalizeTrailingNewline(buffer) : buffer)
|
|
1413
|
+
}
|
|
1419
1414
|
buffer = ""
|
|
1420
1415
|
},
|
|
1421
1416
|
}
|
|
@@ -1501,8 +1496,8 @@ export const runCli = (argv: readonly string[], io: CliIo): Effect.Effect<void,
|
|
|
1501
1496
|
// (Ctrl-C on `ui`/`lsp`'s blocking `Effect.never`) is passed
|
|
1502
1497
|
// through untouched via `failCause` so `NodeRuntime.runMain` still sees a
|
|
1503
1498
|
// real interruption; everything else gets the envelope.
|
|
1504
|
-
Effect.
|
|
1505
|
-
Cause.
|
|
1499
|
+
Effect.catch((cause) =>
|
|
1500
|
+
Cause.hasInterruptsOnly(cause)
|
|
1506
1501
|
? Effect.failCause(cause)
|
|
1507
1502
|
: report(io, plan.json.kind !== "off")(cause),
|
|
1508
1503
|
),
|