@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "../flows.mjs";
|
|
2
|
-
import { E as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches } from "./wire-
|
|
2
|
+
import { E as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING } from "./wire-BTUhRGXj.mjs";
|
|
3
3
|
import { F as formatFinding, N as headingSectionBodies, P as headingSections, a as unansweredQuestions, j as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./steering-CCNvngSH.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
@@ -25743,7 +25743,7 @@ const dedupeDiagnostics = (diagnostics) => {
|
|
|
25743
25743
|
};
|
|
25744
25744
|
//#endregion
|
|
25745
25745
|
//#region src/workflow/load.ts
|
|
25746
|
-
const configError = (diagnostics) => new GtdError(`gtd config:\n${diagnostics.filter((d) => d.severity === "error").map((d) => ` - ${formatDiagnostic(d)}`).join("\n")}`);
|
|
25746
|
+
const configError = (diagnostics) => new GtdError(`gtd: config:\n${diagnostics.filter((d) => d.severity === "error").map((d) => ` - ${formatDiagnostic(d)}`).join("\n")}`);
|
|
25747
25747
|
const failOnErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error") ? fail$3(configError(diagnostics)) : _void;
|
|
25748
25748
|
const MIGRATION_DOC = "https://github.com/pmelab/gtd/blob/main/docs/configuration.md#migrating-from-gtdrc";
|
|
25749
25749
|
/**
|
|
@@ -25759,7 +25759,7 @@ const refuseLegacyRc = gen(function* () {
|
|
|
25759
25759
|
const found = files.filter((f) => f.owned).map((f) => f.path);
|
|
25760
25760
|
if (found.length === 0) return;
|
|
25761
25761
|
return yield* fail$3(new GtdError([
|
|
25762
|
-
"gtd config:",
|
|
25762
|
+
"gtd: config:",
|
|
25763
25763
|
...found.map((file) => ` - ${file}: no longer read — delete it`),
|
|
25764
25764
|
" gtd 23 removed the .gtdrc files; move each key they set:",
|
|
25765
25765
|
" vars → an option of the workflow factory in gtd.config.ts, or its `defaults` export",
|
|
@@ -25795,7 +25795,7 @@ const load = gen(function* () {
|
|
|
25795
25795
|
});
|
|
25796
25796
|
const loaded = yield* try_({
|
|
25797
25797
|
try: () => loadWorkflows(module, shipped),
|
|
25798
|
-
catch: (e) => new GtdError(`gtd config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
25798
|
+
catch: (e) => new GtdError(`gtd: config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
25799
25799
|
});
|
|
25800
25800
|
const compiled = loaded.config;
|
|
25801
25801
|
const diagnostics = dedupeDiagnostics(sortDiagnostics([...compiled.diagnostics, ...loaded.diagnostics]));
|
|
@@ -26300,7 +26300,7 @@ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$2(promise(() => r
|
|
|
26300
26300
|
})), (outcome) => {
|
|
26301
26301
|
if (outcome.kind === "rest") return succeed$3(outcome.rest.name);
|
|
26302
26302
|
const problem = outcome.kind === "failed" || outcome.kind === "refused" ? outcome.message.replace(/^gtd: /, "") : "the default entry must begin at a step — that step is where a finished process waits";
|
|
26303
|
-
return fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: ${problem}`));
|
|
26303
|
+
return fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: ${problem}`));
|
|
26304
26304
|
});
|
|
26305
26305
|
/**
|
|
26306
26306
|
* The default entry's first step — where a finished process waits — found the
|
|
@@ -26316,20 +26316,20 @@ const firstStep = (loaded, head) => gen(function* () {
|
|
|
26316
26316
|
const env = watchedEmptySettings();
|
|
26317
26317
|
const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
|
|
26318
26318
|
const reads = [...settingReads("process", vars), ...settingReads("environment", env)];
|
|
26319
|
-
if (reads.length > 0) return yield* fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
|
|
26319
|
+
if (reads.length > 0) return yield* fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
|
|
26320
26320
|
if (outcome._tag === "Left") return yield* fail$3(outcome.left);
|
|
26321
26321
|
const name = outcome.right;
|
|
26322
26322
|
if (!empty.read()) return name;
|
|
26323
26323
|
const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
|
|
26324
26324
|
if (atHead === name) return name;
|
|
26325
|
-
return yield* fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
|
|
26325
|
+
return yield* fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
|
|
26326
26326
|
});
|
|
26327
26327
|
var ConfigService = class ConfigService extends Tag("ConfigService")() {
|
|
26328
26328
|
static Live = succeed$2(ConfigService, { load });
|
|
26329
26329
|
};
|
|
26330
26330
|
//#endregion
|
|
26331
26331
|
//#region src/workflow/skills.ts
|
|
26332
|
-
const fail = (origin, message) => new GtdError(`gtd config:\n - ${origin}: ${message}`);
|
|
26332
|
+
const fail = (origin, message) => new GtdError(`gtd: config:\n - ${origin}: ${message}`);
|
|
26333
26333
|
const checkShape = (value) => {
|
|
26334
26334
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("skills export must return an object of scope -> skill names");
|
|
26335
26335
|
for (const [scope, names] of Object.entries(value)) if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) throw new Error(`skills export: "${scope}" must be an array of skill names`);
|
|
@@ -27113,7 +27113,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
27113
27113
|
const step = outcome.rest;
|
|
27114
27114
|
yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
|
|
27115
27115
|
const stepDef = stepDefOf(step);
|
|
27116
|
-
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
27116
|
+
if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
|
|
27117
27117
|
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
27118
27118
|
status: normalizeStatus(e.status),
|
|
27119
27119
|
path: e.path
|
|
@@ -27665,7 +27665,7 @@ const emitScripts = (required = [], optional = []) => ({
|
|
|
27665
27665
|
optional: assembleScript(optional)
|
|
27666
27666
|
});
|
|
27667
27667
|
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
27668
|
-
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it —
|
|
27668
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
27669
27669
|
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
27670
27670
|
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
27671
27671
|
/**
|
|
@@ -28835,7 +28835,10 @@ const runLandCommand = (opts, json, out) => gen(function* () {
|
|
|
28835
28835
|
const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => gen(function* () {
|
|
28836
28836
|
const config = yield* (yield* ConfigService).load;
|
|
28837
28837
|
const resolved = config.workflowNamed(workflow);
|
|
28838
|
-
if (resolved === void 0)
|
|
28838
|
+
if (resolved === void 0) {
|
|
28839
|
+
const startable = config.workflowNames.length > 0 ? `startable: ${config.workflowNames.join(", ")}` : "this config exports no workflow besides its default, which an ordinary start runs";
|
|
28840
|
+
return yield* fail$3(new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — ${startable}`));
|
|
28841
|
+
}
|
|
28839
28842
|
const rest = yield* restAt(void 0, varOverrides);
|
|
28840
28843
|
const started = yield* startRefusal(rest, resolved, varOverrides);
|
|
28841
28844
|
const plan = yield* planStart({
|
|
@@ -28849,9 +28852,24 @@ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => ge
|
|
|
28849
28852
|
config: rest.config
|
|
28850
28853
|
});
|
|
28851
28854
|
if (plan.kind === "refusal") return yield* fail$3(new Error(plan.message));
|
|
28852
|
-
out.write(landingScript(ScriptSurface.render(plan.steps)));
|
|
28855
|
+
out.write(listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
|
|
28853
28856
|
});
|
|
28857
|
+
/**
|
|
28858
|
+
* A start commits the whole tree (`git add -A`), so its script says up front
|
|
28859
|
+
* which paths that is. Comment lines only: control characters are masked so a
|
|
28860
|
+
* path can never end the comment and become a command.
|
|
28861
|
+
*/
|
|
28862
|
+
const listingCommitted = (script, changes) => {
|
|
28863
|
+
if (changes.length === 0) return script;
|
|
28864
|
+
const listing = ["# commits every change in the working tree (git add -A):", ...changes.map((change) => `# ${change.status} ${change.path.replace(/\p{Cc}/gu, "?")}`)].join("\n");
|
|
28865
|
+
const at = script.indexOf("\n\n");
|
|
28866
|
+
return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`;
|
|
28867
|
+
};
|
|
28854
28868
|
const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
|
|
28869
|
+
const noSuchDoor = (name, doors) => {
|
|
28870
|
+
const listing = doors.size > 0 ? `doors: ${[...doors].map(([n, d]) => doorSynopsis(n, d)).join(", ")}` : "this config has no doors";
|
|
28871
|
+
return new GtdUsageError(name === void 0 ? `gtd door: missing door name — ${listing}` : `gtd door: unknown door "${name}" — ${listing}`);
|
|
28872
|
+
};
|
|
28855
28873
|
/**
|
|
28856
28874
|
* `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
|
|
28857
28875
|
* process settings its `vars` derive from the positional args. A wrong name or
|
|
@@ -28859,11 +28877,8 @@ const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => ar
|
|
|
28859
28877
|
*/
|
|
28860
28878
|
const runDoorCommand = (name, args, out) => gen(function* () {
|
|
28861
28879
|
const config = yield* (yield* ConfigService).load;
|
|
28862
|
-
const door = config.doors.get(name);
|
|
28863
|
-
if (door === void 0)
|
|
28864
|
-
const known = [...config.doors];
|
|
28865
|
-
return yield* fail$3(new GtdUsageError(`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`));
|
|
28866
|
-
}
|
|
28880
|
+
const door = name === void 0 ? void 0 : config.doors.get(name);
|
|
28881
|
+
if (name === void 0 || door === void 0) return yield* fail$3(noSuchDoor(name, config.doors));
|
|
28867
28882
|
const declared = door.args ?? [];
|
|
28868
28883
|
const required = declared.filter((arg) => arg.optional !== true).length;
|
|
28869
28884
|
if (args.length < required || args.length > declared.length) return yield* fail$3(new GtdUsageError(`gtd door ${name}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd door ${doorSynopsis(name, door)}`));
|
|
@@ -28881,6 +28896,7 @@ const runDoorCommand = (name, args, out) => gen(function* () {
|
|
|
28881
28896
|
const runDoorsCommand = (json, out) => gen(function* () {
|
|
28882
28897
|
const doors = [...(yield* (yield* ConfigService).load).doors];
|
|
28883
28898
|
if (json.kind === "off") {
|
|
28899
|
+
if (doors.length === 0) yield* (yield* Narrator).warn("gtd doors: this config has no doors — a workflow factory's `doors` option or the config's `doors` export adds one");
|
|
28884
28900
|
out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""));
|
|
28885
28901
|
return;
|
|
28886
28902
|
}
|
|
@@ -29028,6 +29044,13 @@ const runNextCommand = (json, out) => gen(function* () {
|
|
|
29028
29044
|
else if (json.kind === "select") yield* writeSelection(out, fields, json.path);
|
|
29029
29045
|
else {
|
|
29030
29046
|
const validateCommand = emitsValidatablePrompt(rendered) ? selfValidateCommand(rest.def, rest.state, rendered.mode, rendered.file) : void 0;
|
|
29047
|
+
if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
|
|
29048
|
+
const shown = renderBeatPlain({
|
|
29049
|
+
...fields,
|
|
29050
|
+
kind: "message"
|
|
29051
|
+
});
|
|
29052
|
+
yield* narrator.warn(shown.slice(0, shown.indexOf("\n\n")));
|
|
29053
|
+
}
|
|
29031
29054
|
out.write(renderBeatPlain(fields, validateCommand));
|
|
29032
29055
|
}
|
|
29033
29056
|
});
|
|
@@ -29183,12 +29206,13 @@ const runUiCliCommand = (command, out) => gen(function* () {
|
|
|
29183
29206
|
* cwds (e.g. macOS /tmp → /private/tmp) match.
|
|
29184
29207
|
*/
|
|
29185
29208
|
const assertRunningFromRepoRoot = (git, host) => gen(function* () {
|
|
29186
|
-
const topLevel = yield* git.topLevel();
|
|
29187
|
-
if ((yield* host.realPath(topLevel)) !== (yield* host.realPath(host.root))) return yield* fail$3(/* @__PURE__ */ new Error(`gtd
|
|
29209
|
+
const topLevel = yield* git.topLevel().pipe(mapError$1((e) => /not a git repository/.test(e.message) ? /* @__PURE__ */ new Error("gtd: not a git repository — run gtd inside one (`git init` makes one)") : e));
|
|
29210
|
+
if ((yield* host.realPath(topLevel)) !== (yield* host.realPath(host.root))) return yield* fail$3(/* @__PURE__ */ new Error(`gtd: run gtd from the repository root — cd ${shellWord(topLevel)} (the current directory is ${host.root})`));
|
|
29188
29211
|
});
|
|
29212
|
+
const shellWord = (path) => /^[\w@%+=:,./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`;
|
|
29189
29213
|
/** A command that derives workflow state needs at least one commit too — a commitless repository has no HEAD to resolve a `Rest` at. */
|
|
29190
29214
|
const assertRepositoryHasCommits = (git) => gen(function* () {
|
|
29191
|
-
if (!(yield* git.hasCommits())) return yield* fail$3(/* @__PURE__ */ new Error("gtd
|
|
29215
|
+
if (!(yield* git.hasCommits())) return yield* fail$3(/* @__PURE__ */ new Error("gtd: the repository has no commits yet — make an initial commit, then run gtd again"));
|
|
29192
29216
|
});
|
|
29193
29217
|
/**
|
|
29194
29218
|
* What a command kind needs before it may run. Defined here, not in
|
|
@@ -29275,6 +29299,57 @@ const runCommand$1 = (command, json, out) => {
|
|
|
29275
29299
|
});
|
|
29276
29300
|
};
|
|
29277
29301
|
//#endregion
|
|
29302
|
+
//#region src/cli/Help.ts
|
|
29303
|
+
const HELP_WIDTH = 80;
|
|
29304
|
+
/**
|
|
29305
|
+
* Word-wraps prose to `width` columns, every line prefixed by `indent`.
|
|
29306
|
+
* Paragraphs are separated by a blank line; a paragraph whose lines start
|
|
29307
|
+
* with two spaces is an example and is kept verbatim (indented, not wrapped).
|
|
29308
|
+
*/
|
|
29309
|
+
const wrapParagraph = (paragraph, width, indent) => {
|
|
29310
|
+
const lines = [];
|
|
29311
|
+
let line = "";
|
|
29312
|
+
for (const word of paragraph.match(/\S*`[^`]*`\S*|\S+/g) ?? []) if (line !== "" && indent.length + line.length + 1 + word.length > width) {
|
|
29313
|
+
lines.push(indent + line);
|
|
29314
|
+
line = word;
|
|
29315
|
+
} else line = line === "" ? word : `${line} ${word}`;
|
|
29316
|
+
if (line !== "") lines.push(indent + line);
|
|
29317
|
+
return lines;
|
|
29318
|
+
};
|
|
29319
|
+
const wrap = (text, width = HELP_WIDTH, indent = "") => text.split("\n\n").flatMap((paragraph, i) => [...i > 0 ? [""] : [], ...paragraph.startsWith(" ") ? paragraph.split("\n").map((l) => indent + l) : wrapParagraph(paragraph, width, indent)]);
|
|
29320
|
+
/** `term` and `description` side by side, the description wrapped into the column after `column`; a term too wide for it gets a line of its own. */
|
|
29321
|
+
const definition = (term, description, column) => {
|
|
29322
|
+
const cell = ` ${term}`;
|
|
29323
|
+
const body = wrap(description, HELP_WIDTH, " ".repeat(column));
|
|
29324
|
+
if (cell.length + 1 >= column) return [cell, ...body];
|
|
29325
|
+
const [first = "", ...rest] = body;
|
|
29326
|
+
return [cell.padEnd(column) + first.trimStart(), ...rest];
|
|
29327
|
+
};
|
|
29328
|
+
/** Optimal-string-alignment distance: Levenshtein plus adjacent transpositions, so `nxet` is one edit from `next`. */
|
|
29329
|
+
const distance = (a, b) => {
|
|
29330
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
29331
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
|
|
29332
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
29333
|
+
let best = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
29334
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, d[i - 2][j - 2] + 1);
|
|
29335
|
+
d[i][j] = best;
|
|
29336
|
+
}
|
|
29337
|
+
return d[a.length][b.length];
|
|
29338
|
+
};
|
|
29339
|
+
/** The candidate closest to `word`, when it is a plausible typo of it: at most one edit per three characters, and at least one. */
|
|
29340
|
+
const closest = (word, candidates) => {
|
|
29341
|
+
const limit = Math.max(1, Math.floor(word.length / 3));
|
|
29342
|
+
let best;
|
|
29343
|
+
for (const name of candidates) {
|
|
29344
|
+
const d = distance(word, name);
|
|
29345
|
+
if (d <= limit && (best === void 0 || d < best.d)) best = {
|
|
29346
|
+
name,
|
|
29347
|
+
d
|
|
29348
|
+
};
|
|
29349
|
+
}
|
|
29350
|
+
return best?.name;
|
|
29351
|
+
};
|
|
29352
|
+
//#endregion
|
|
29278
29353
|
//#region src/cli/Cli.ts
|
|
29279
29354
|
const nonNegativeNumber = (raw, flag) => {
|
|
29280
29355
|
const n = Number(raw);
|
|
@@ -29302,20 +29377,8 @@ const FLAGS = [
|
|
|
29302
29377
|
decode: ([raw]) => right(raw ?? ""),
|
|
29303
29378
|
scopeError: "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` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
29304
29379
|
valueHint: "<path>",
|
|
29305
|
-
help:
|
|
29306
|
-
|
|
29307
|
-
"list/gtd history show/gtd history metrics/gtd history judges/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
29308
|
-
"--json=<path> (a dotted key path into that document, e.g.",
|
|
29309
|
-
"kind, content, session.id) prints just that value: a",
|
|
29310
|
-
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
29311
|
-
"one JSON entry per line — a numeric segment (e.g.",
|
|
29312
|
-
"skills.0) indexes into one entry of a list field instead.",
|
|
29313
|
-
"An absent optional field prints nothing and exits 0 —",
|
|
29314
|
-
"including when an earlier segment of <path> is itself",
|
|
29315
|
-
"absent/null (e.g. session.id at a non-prompt rest), which",
|
|
29316
|
-
"never counts as unknown; an unknown path is a usage error",
|
|
29317
|
-
"(exit 2)."
|
|
29318
|
-
]
|
|
29380
|
+
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",
|
|
29381
|
+
helpFor: { strays: "Print one entry of the array (--json=0), nothing past its end" }
|
|
29319
29382
|
},
|
|
29320
29383
|
{
|
|
29321
29384
|
name: "--port",
|
|
@@ -29328,11 +29391,7 @@ const FLAGS = [
|
|
|
29328
29391
|
},
|
|
29329
29392
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
29330
29393
|
valueHint: "<n>",
|
|
29331
|
-
help:
|
|
29332
|
-
"(gtd ui only) the tailscale serve port (default: the first",
|
|
29333
|
-
"free of 8443, 10000, 443), or the bind port when --host",
|
|
29334
|
-
"opts out of serve (default: a free port)"
|
|
29335
|
-
]
|
|
29394
|
+
help: "The tailscale serve port (default: the first free of 8443, 10000, 443), or with --host the port to bind (default: any free one)"
|
|
29336
29395
|
},
|
|
29337
29396
|
{
|
|
29338
29397
|
name: "--host",
|
|
@@ -29342,11 +29401,7 @@ const FLAGS = [
|
|
|
29342
29401
|
decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --host must be a non-empty, single-line value") : right(raw),
|
|
29343
29402
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
29344
29403
|
valueHint: "<addr>",
|
|
29345
|
-
help:
|
|
29346
|
-
"(gtd ui only) opt out of the default tailscale serve front",
|
|
29347
|
-
"door and bind this address directly instead, showing it in",
|
|
29348
|
-
"the printed URL"
|
|
29349
|
-
]
|
|
29404
|
+
help: "Bind this address directly instead of publishing through tailscale serve"
|
|
29350
29405
|
},
|
|
29351
29406
|
{
|
|
29352
29407
|
name: "--self-signed",
|
|
@@ -29356,7 +29411,7 @@ const FLAGS = [
|
|
|
29356
29411
|
decode: () => right(true),
|
|
29357
29412
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
29358
29413
|
valueHint: "",
|
|
29359
|
-
help:
|
|
29414
|
+
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key"
|
|
29360
29415
|
},
|
|
29361
29416
|
{
|
|
29362
29417
|
name: "--dev",
|
|
@@ -29366,7 +29421,7 @@ const FLAGS = [
|
|
|
29366
29421
|
decode: () => right(true),
|
|
29367
29422
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
29368
29423
|
valueHint: "",
|
|
29369
|
-
help:
|
|
29424
|
+
help: "Serve local development sources instead of the packaged build"
|
|
29370
29425
|
},
|
|
29371
29426
|
{
|
|
29372
29427
|
name: "--provider",
|
|
@@ -29376,11 +29431,7 @@ const FLAGS = [
|
|
|
29376
29431
|
decode: ([raw]) => raw === "fixed" || raw === "jev" || raw === "llm" ? right(raw) : left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
29377
29432
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
29378
29433
|
valueHint: "<name>",
|
|
29379
|
-
help:
|
|
29380
|
-
"(gtd judge run only) the answerer: fixed | jev | llm;",
|
|
29381
|
-
"omitted: GTD_JUDGE_PROVIDER, then judge.json, then jev",
|
|
29382
|
-
"when TYPESAFE_API_KEY is set, else llm"
|
|
29383
|
-
]
|
|
29434
|
+
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)"
|
|
29384
29435
|
},
|
|
29385
29436
|
{
|
|
29386
29437
|
name: "--answers",
|
|
@@ -29390,11 +29441,7 @@ const FLAGS = [
|
|
|
29390
29441
|
decode: ([raw]) => raw === void 0 || raw === "" ? left("gtd: --answers must be a non-empty path") : right(raw),
|
|
29391
29442
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
29392
29443
|
valueHint: "<path>",
|
|
29393
|
-
help: [
|
|
29394
|
-
"(gtd judge run --provider fixed only) a JSON verdict array",
|
|
29395
|
-
"[{ id, answer, p }]; else the GTD_JUDGE_ANSWERS env var",
|
|
29396
|
-
"(inline JSON) is read"
|
|
29397
|
-
]
|
|
29444
|
+
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)"
|
|
29398
29445
|
},
|
|
29399
29446
|
{
|
|
29400
29447
|
name: "--cost",
|
|
@@ -29404,7 +29451,7 @@ const FLAGS = [
|
|
|
29404
29451
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
29405
29452
|
scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
|
|
29406
29453
|
valueHint: "<n>",
|
|
29407
|
-
help:
|
|
29454
|
+
help: "Record the token cost of the invocation that just finished on the commit"
|
|
29408
29455
|
},
|
|
29409
29456
|
{
|
|
29410
29457
|
name: "--model",
|
|
@@ -29414,11 +29461,11 @@ const FLAGS = [
|
|
|
29414
29461
|
decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --model must be a non-empty, single-line value") : right(raw),
|
|
29415
29462
|
scopeError: "gtd: --model is only valid for `gtd land` (with --cost) or `gtd judge run`",
|
|
29416
29463
|
valueHint: "<name>",
|
|
29417
|
-
help:
|
|
29418
|
-
|
|
29419
|
-
"
|
|
29420
|
-
"GTD_JUDGE_MODEL, then judge.json, then sonnet"
|
|
29421
|
-
|
|
29464
|
+
help: "",
|
|
29465
|
+
helpFor: {
|
|
29466
|
+
land: "With --cost: the model that cost was spent on",
|
|
29467
|
+
judgeRun: "The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)"
|
|
29468
|
+
}
|
|
29422
29469
|
},
|
|
29423
29470
|
{
|
|
29424
29471
|
name: "--workflow",
|
|
@@ -29428,7 +29475,7 @@ const FLAGS = [
|
|
|
29428
29475
|
decode: ([raw]) => right(raw ?? ""),
|
|
29429
29476
|
scopeError: "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` form; landing and starting are different verbs",
|
|
29430
29477
|
valueHint: "<name>",
|
|
29431
|
-
help:
|
|
29478
|
+
help: "Start a new process on the named workflow"
|
|
29432
29479
|
},
|
|
29433
29480
|
{
|
|
29434
29481
|
name: "--var",
|
|
@@ -29459,14 +29506,11 @@ const FLAGS = [
|
|
|
29459
29506
|
},
|
|
29460
29507
|
scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
|
|
29461
29508
|
valueHint: "<name>=<value>",
|
|
29462
|
-
help:
|
|
29463
|
-
|
|
29464
|
-
"
|
|
29465
|
-
"
|
|
29466
|
-
|
|
29467
|
-
"under this threshold: judgeIdenticalMinP or",
|
|
29468
|
-
"qualityTriageMinP"
|
|
29469
|
-
]
|
|
29509
|
+
help: "",
|
|
29510
|
+
helpFor: {
|
|
29511
|
+
start: "Pin one of the workflow's declared settings for the new process (repeatable)",
|
|
29512
|
+
historyJudges: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)"
|
|
29513
|
+
}
|
|
29470
29514
|
},
|
|
29471
29515
|
{
|
|
29472
29516
|
name: "--jsonl",
|
|
@@ -29476,7 +29520,7 @@ const FLAGS = [
|
|
|
29476
29520
|
decode: () => right(true),
|
|
29477
29521
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
29478
29522
|
valueHint: "",
|
|
29479
|
-
help:
|
|
29523
|
+
help: "Print one JSON line per verdict instead of the report",
|
|
29480
29524
|
conflicts: ["--json", "--var"]
|
|
29481
29525
|
},
|
|
29482
29526
|
{
|
|
@@ -29487,12 +29531,7 @@ const FLAGS = [
|
|
|
29487
29531
|
decode: () => right(true),
|
|
29488
29532
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
29489
29533
|
valueHint: "",
|
|
29490
|
-
help:
|
|
29491
|
-
"(gtd check only) ignore <mode>'s structural findings and",
|
|
29492
|
-
"instead run the qa open-questions predicate over <file>,",
|
|
29493
|
-
"printing each unanswered question one per line and exiting",
|
|
29494
|
-
"non-zero when any remain"
|
|
29495
|
-
]
|
|
29534
|
+
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain"
|
|
29496
29535
|
},
|
|
29497
29536
|
{
|
|
29498
29537
|
name: "--open-threads",
|
|
@@ -29502,15 +29541,7 @@ const FLAGS = [
|
|
|
29502
29541
|
decode: () => right(true),
|
|
29503
29542
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
29504
29543
|
valueHint: "",
|
|
29505
|
-
help:
|
|
29506
|
-
"(gtd check only) ignore <mode>'s structural findings and",
|
|
29507
|
-
"instead list every open thread (a footnote whose last",
|
|
29508
|
-
"entry is the agent's) over <file>, one per line with its",
|
|
29509
|
-
"first H entry, exiting non-zero when any are open. With no",
|
|
29510
|
-
"<mode>/<file>, lists code-comment threads in the files the",
|
|
29511
|
-
"current process changed (read from its diff base, unlike",
|
|
29512
|
-
"the rest of `gtd check`)"
|
|
29513
|
-
]
|
|
29544
|
+
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"
|
|
29514
29545
|
},
|
|
29515
29546
|
{
|
|
29516
29547
|
name: "--verbose",
|
|
@@ -29520,11 +29551,7 @@ const FLAGS = [
|
|
|
29520
29551
|
decode: () => right(true),
|
|
29521
29552
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
29522
29553
|
valueHint: "",
|
|
29523
|
-
help:
|
|
29524
|
-
"enable stderr narration for this invocation: which rest",
|
|
29525
|
-
"resolved and how config resolved across layers. Aliased",
|
|
29526
|
-
"to -v"
|
|
29527
|
-
]
|
|
29554
|
+
help: "Narrate on stderr what gtd resolved and where its config came from"
|
|
29528
29555
|
}
|
|
29529
29556
|
];
|
|
29530
29557
|
/**
|
|
@@ -29543,203 +29570,39 @@ const flagByToken = (token) => {
|
|
|
29543
29570
|
return FLAGS.find((f) => f.name === bareName);
|
|
29544
29571
|
};
|
|
29545
29572
|
const COMMAND_ROWS = [
|
|
29546
|
-
{
|
|
29547
|
-
token: "land",
|
|
29548
|
-
kind: "land",
|
|
29549
|
-
arity: "none",
|
|
29550
|
-
details: [
|
|
29551
|
-
"Land whatever the tree now shows at the currently resolved",
|
|
29552
|
-
"rest — a human capture, an agent/check turn, or an empty",
|
|
29553
|
-
"attempt (a fruitless prompt turn). Pass",
|
|
29554
|
-
"--cost=<n> (optionally --model=<name>) to record the",
|
|
29555
|
-
"just-finished invocation's token cost and model on the",
|
|
29556
|
-
"turn commit (summed into it.processCost/",
|
|
29557
|
-
"processCostByModel). Plain (the default) prints one",
|
|
29558
|
-
"human-readable sentence naming the commit subject (or a",
|
|
29559
|
-
"no-op note) plus a pointer to `gtd land --json=script | sh`",
|
|
29560
|
-
"to get the landing script — never the script itself.",
|
|
29561
|
-
"--json emits script (the actual POSIX sh",
|
|
29562
|
-
"a driver runs) alongside settled, idle, state (the",
|
|
29563
|
-
"post-land target), subject, cost and model. Exits 0 on",
|
|
29564
|
-
"success, 1 on any refusal — see the Exit codes section",
|
|
29565
|
-
"below"
|
|
29566
|
-
]
|
|
29567
|
-
},
|
|
29568
|
-
{
|
|
29569
|
-
token: "abandon",
|
|
29570
|
-
kind: "abandon",
|
|
29571
|
-
arity: "none",
|
|
29572
|
-
details: [
|
|
29573
|
-
"End the process currently underway without completing it:",
|
|
29574
|
-
"rewind HEAD to the commit the process started from,",
|
|
29575
|
-
"keeping everything it produced as uncommitted changes. A",
|
|
29576
|
-
"no-op when no process is underway"
|
|
29577
|
-
]
|
|
29578
|
-
},
|
|
29579
|
-
{
|
|
29580
|
-
token: "restore",
|
|
29581
|
-
kind: "restore",
|
|
29582
|
-
arity: "none",
|
|
29583
|
-
details: [
|
|
29584
|
-
"Hard-reset HEAD back to the tip retained by the last",
|
|
29585
|
-
"abandon (refs/worktree/gtd/history), bringing back an",
|
|
29586
|
-
"abandoned process's turns. Refuses on a dirty working",
|
|
29587
|
-
"tree, when there is no retained history, or when HEAD has",
|
|
29588
|
-
"advanced past the retained tip with commits that would be",
|
|
29589
|
-
"lost"
|
|
29590
|
-
]
|
|
29591
|
-
},
|
|
29592
29573
|
{
|
|
29593
29574
|
token: "next",
|
|
29594
29575
|
kind: "next",
|
|
29595
29576
|
arity: "none",
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
"it's the bare step (plus the self-validation instruction",
|
|
29604
|
-
"when applicable) with no header at all, since those bytes",
|
|
29605
|
-
"are the agent's own input. --json emits the one structured",
|
|
29606
|
-
"surface gtd has: kind (capture|message|script|prompt|",
|
|
29607
|
-
"stalled) selects what a driver does, content is what it",
|
|
29608
|
-
"runs or shows, idle marks the workflow's initial state with",
|
|
29609
|
-
"a clean tree, plus the prompt session, model, validate",
|
|
29610
|
-
"script, log path, changes, next and the resting state's own",
|
|
29611
|
-
"fields. --json=<path> (a dotted key path into that same",
|
|
29612
|
-
"document, e.g. kind, content, session.id) prints just that",
|
|
29613
|
-
"value instead of the whole document — see --json's own help",
|
|
29614
|
-
"above. Exits 0 — see the Exit codes section below"
|
|
29615
|
-
]
|
|
29616
|
-
},
|
|
29617
|
-
{
|
|
29618
|
-
token: "validate",
|
|
29619
|
-
kind: "validate",
|
|
29620
|
-
arity: "none",
|
|
29621
|
-
details: [
|
|
29622
|
-
"Print the script that formats (when declared) then",
|
|
29623
|
-
"validates the resolved rest's steering file, using its",
|
|
29624
|
-
"mode's commands (its file:/mode:), instead of running it —",
|
|
29625
|
-
"a driver runs the script and reads the findings from its",
|
|
29626
|
-
"own exit code/output. Always exits 0; prints \"nothing to",
|
|
29627
|
-
"validate\" when the resolved rest declares nothing to run.",
|
|
29628
|
-
"On a non-zero validate exit the emitted script prints a",
|
|
29629
|
-
"ready-to-send fix prompt (instruction + findings) and",
|
|
29630
|
-
"exits with the validator's own code"
|
|
29631
|
-
]
|
|
29632
|
-
},
|
|
29633
|
-
{
|
|
29634
|
-
token: "lsp",
|
|
29635
|
-
kind: "lsp",
|
|
29636
|
-
arity: "none",
|
|
29637
|
-
details: ["Start the LSP server for .gtd/ steering files (stdio)"]
|
|
29638
|
-
},
|
|
29639
|
-
{
|
|
29640
|
-
token: "ui",
|
|
29641
|
-
kind: "ui",
|
|
29642
|
-
arity: "none",
|
|
29643
|
-
details: [
|
|
29644
|
-
"Expose gtd's web/phone client for THIS worktree — the",
|
|
29645
|
-
"invoking directory, never a configured list of roots — and",
|
|
29646
|
-
"refuses outside a repository like every other state command.",
|
|
29647
|
-
"By default, publishes through `tailscale serve` (reachable",
|
|
29648
|
-
"from anywhere on the tailnet, including over a DERP relay)",
|
|
29649
|
-
"with tailscaled terminating TLS, walking 8443, then 10000,",
|
|
29650
|
-
"then 443 until one publishes; falls back to binding a local",
|
|
29651
|
-
"HTTPS server directly, never refusing, when none of the",
|
|
29652
|
-
"three works. --host <addr> opts out of serve and binds that",
|
|
29653
|
-
"address directly instead; --port <n> overrides the serve",
|
|
29654
|
-
"port (default: the first free of 8443, 10000, 443), or the",
|
|
29655
|
-
"bind port when --host is given (default: a free port);",
|
|
29656
|
-
"--self-signed also opts out of serve, generating a",
|
|
29657
|
-
"throwaway TLS certificate instead of the configured",
|
|
29658
|
-
"ui.cert/ui.key; --dev runs against local development sources",
|
|
29659
|
-
"instead of the packaged build"
|
|
29660
|
-
]
|
|
29661
|
-
},
|
|
29662
|
-
{
|
|
29663
|
-
token: "check",
|
|
29664
|
-
kind: "check",
|
|
29665
|
-
arity: { names: ["mode", "file"] },
|
|
29666
|
-
details: [
|
|
29667
|
-
"Read <file> and run the built-in steering format named",
|
|
29668
|
-
"<mode> (see `gtd validate`'s modes: qa, review, findings)",
|
|
29669
|
-
"over its contents, printing each finding one per line and",
|
|
29670
|
-
"exiting non-zero when there are any. Resolves no workflow",
|
|
29671
|
-
"state and reads no config — standalone, runnable from any",
|
|
29672
|
-
"directory with <mode>/<file> given explicitly. This is what a",
|
|
29673
|
-
"workflow's emitted validation script invokes as a leaf step.",
|
|
29674
|
-
"--open-questions runs the qa unanswered-questions predicate",
|
|
29675
|
-
"instead (see --help); --open-threads lists open threads.",
|
|
29676
|
-
"`gtd check --open-threads` alone lists open code-comment",
|
|
29677
|
-
"threads (`// H: ...`) in the files the process changed;",
|
|
29678
|
-
"it prints nothing when no process is underway."
|
|
29679
|
-
]
|
|
29680
|
-
},
|
|
29681
|
-
{
|
|
29682
|
-
token: "uncheck",
|
|
29683
|
-
kind: "uncheck",
|
|
29684
|
-
arity: { name: "file" },
|
|
29685
|
-
details: [
|
|
29686
|
-
"Read <file> and reset every review-mode `- [x]`/`- [X]`",
|
|
29687
|
-
"file-pointer box back to `- [ ]`, writing the result back",
|
|
29688
|
-
"only when the bytes actually changed. Resolves no workflow",
|
|
29689
|
-
"state and reads no config — standalone, runnable from any",
|
|
29690
|
-
"directory with <file> given explicitly. This is what the",
|
|
29691
|
-
"landing script runs ahead of the commit at the human review",
|
|
29692
|
-
"gate, so no tick ever reaches a commit. Takes no <mode> —",
|
|
29693
|
-
"it means review-mode file pointers and nothing else, never",
|
|
29694
|
-
"qa-mode's answered-question boxes. A missing file writes",
|
|
29695
|
-
"nothing and exits 0"
|
|
29696
|
-
]
|
|
29697
|
-
},
|
|
29698
|
-
{
|
|
29699
|
-
token: "install",
|
|
29700
|
-
kind: "install",
|
|
29701
|
-
arity: "none",
|
|
29702
|
-
details: [
|
|
29703
|
-
"Print a complete, self-contained briefing that teaches an",
|
|
29704
|
-
"agent (or a human) to build a gtd driver in any shell or",
|
|
29705
|
-
"runtime — the self-serve version of",
|
|
29706
|
-
"https://github.com/pmelab/gtd/blob/main/docs/driver.md's",
|
|
29707
|
-
"'Writing your own driver', and how to get a workflow: gtd",
|
|
29708
|
-
"ships none. It writes nothing."
|
|
29709
|
-
]
|
|
29577
|
+
group: "Core loop",
|
|
29578
|
+
summary: "Print what should happen next",
|
|
29579
|
+
details: `Print what should happen next in the process, changing nothing — safe to poll.
|
|
29580
|
+
|
|
29581
|
+
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.
|
|
29582
|
+
|
|
29583
|
+
--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).`
|
|
29710
29584
|
},
|
|
29711
29585
|
{
|
|
29712
|
-
token: "
|
|
29713
|
-
kind: "
|
|
29586
|
+
token: "land",
|
|
29587
|
+
kind: "land",
|
|
29714
29588
|
arity: "none",
|
|
29715
|
-
|
|
29716
|
-
|
|
29717
|
-
|
|
29718
|
-
|
|
29719
|
-
|
|
29720
|
-
|
|
29721
|
-
|
|
29722
|
-
|
|
29723
|
-
|
|
29724
|
-
"Refuses (exit 1) when the workflow declares no summary:",
|
|
29725
|
-
"template, or when the resolved run has no commits to name —",
|
|
29726
|
-
"it is runnable any time before the next thing lands on the",
|
|
29727
|
-
"branch"
|
|
29728
|
-
]
|
|
29589
|
+
group: "Core loop",
|
|
29590
|
+
summary: "Record what the tree now shows",
|
|
29591
|
+
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.
|
|
29592
|
+
|
|
29593
|
+
gtd writes nothing itself. Plain output names the commit it would make; --json=script prints the script that makes it:
|
|
29594
|
+
|
|
29595
|
+
s=$(gtd land --json=script) && sh -c "$s"
|
|
29596
|
+
|
|
29597
|
+
--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.`
|
|
29729
29598
|
},
|
|
29730
29599
|
{
|
|
29731
|
-
token: "
|
|
29732
|
-
kind: "
|
|
29600
|
+
token: "validate",
|
|
29601
|
+
kind: "validate",
|
|
29733
29602
|
arity: "none",
|
|
29734
|
-
|
|
29735
|
-
|
|
29736
|
-
|
|
29737
|
-
"bare, newline-terminated, and nothing else. Writes nothing:",
|
|
29738
|
-
"no git, no state transition, no session identity. Before",
|
|
29739
|
-
"the first review round it's the process's diff base;",
|
|
29740
|
-
"afterward it's the most-recent review round's boundary.",
|
|
29741
|
-
"Refuses (exit 1) when no process is underway."
|
|
29742
|
-
]
|
|
29603
|
+
group: "Core loop",
|
|
29604
|
+
summary: "Print the script that checks the steering file",
|
|
29605
|
+
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.`
|
|
29743
29606
|
},
|
|
29744
29607
|
{
|
|
29745
29608
|
token: "door",
|
|
@@ -29748,73 +29611,55 @@ const COMMAND_ROWS = [
|
|
|
29748
29611
|
name: "name",
|
|
29749
29612
|
variadic: "args"
|
|
29750
29613
|
},
|
|
29751
|
-
|
|
29752
|
-
|
|
29753
|
-
|
|
29754
|
-
|
|
29755
|
-
|
|
29756
|
-
"`fix`, `review [base]`, `rollout <flag>` and",
|
|
29757
|
-
"`cleanup <flag>`), plus the config's `doors` export. An",
|
|
29758
|
-
"unknown door, or too few or too many args, is a usage error",
|
|
29759
|
-
"(exit 2) naming the doors or the door's synopsis"
|
|
29760
|
-
]
|
|
29614
|
+
group: "Start and stop",
|
|
29615
|
+
summary: "Start a process through a named shortcut",
|
|
29616
|
+
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.
|
|
29617
|
+
|
|
29618
|
+
With no <name>, an unknown one, or the wrong number of args, it lists the doors and exits 2.`
|
|
29761
29619
|
},
|
|
29762
29620
|
{
|
|
29763
29621
|
token: "doors",
|
|
29764
29622
|
kind: "doors",
|
|
29765
29623
|
arity: "none",
|
|
29766
|
-
|
|
29767
|
-
|
|
29768
|
-
|
|
29769
|
-
"args: [{ name, optional }] } instead; --json=<path> reads one",
|
|
29770
|
-
"value off it. Writes nothing"
|
|
29771
|
-
]
|
|
29624
|
+
group: "Start and stop",
|
|
29625
|
+
summary: "List the doors",
|
|
29626
|
+
details: `List every door, sorted: one \`name [args] → workflow\` line each. --json prints a JSON array of { name, workflow, args: [{ name, optional }] }. Writes nothing.`
|
|
29772
29627
|
},
|
|
29773
29628
|
{
|
|
29774
|
-
token: "
|
|
29775
|
-
kind: "
|
|
29629
|
+
token: "abandon",
|
|
29630
|
+
kind: "abandon",
|
|
29776
29631
|
arity: "none",
|
|
29777
|
-
|
|
29778
|
-
|
|
29779
|
-
|
|
29780
|
-
|
|
29781
|
-
|
|
29782
|
-
"the resolved rest declares no judge:"
|
|
29783
|
-
]
|
|
29632
|
+
group: "Start and stop",
|
|
29633
|
+
summary: "Stop the process, keeping its changes",
|
|
29634
|
+
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.
|
|
29635
|
+
|
|
29636
|
+
s=$(gtd abandon) && sh -c "$s"`
|
|
29784
29637
|
},
|
|
29785
29638
|
{
|
|
29786
|
-
token: "
|
|
29787
|
-
kind: "
|
|
29639
|
+
token: "restore",
|
|
29640
|
+
kind: "restore",
|
|
29788
29641
|
arity: "none",
|
|
29789
|
-
|
|
29790
|
-
|
|
29791
|
-
|
|
29792
|
-
"against an Effect Schema built from those same question",
|
|
29793
|
-
"ids. Refuses (exit 1) when the resolved rest declares no",
|
|
29794
|
-
"judge:. Exits 2 (a usage error, like an unknown --json",
|
|
29795
|
-
"selector) when stdin isn't valid JSON or the verdict",
|
|
29796
|
-
"doesn't decode against the pending question ids."
|
|
29797
|
-
]
|
|
29642
|
+
group: "Start and stop",
|
|
29643
|
+
summary: "Bring back the last abandoned process",
|
|
29644
|
+
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.`
|
|
29798
29645
|
},
|
|
29799
29646
|
{
|
|
29800
|
-
token: "
|
|
29801
|
-
kind: "
|
|
29647
|
+
token: "summary",
|
|
29648
|
+
kind: "summary",
|
|
29802
29649
|
arity: "none",
|
|
29803
|
-
|
|
29804
|
-
|
|
29805
|
-
|
|
29806
|
-
|
|
29807
|
-
|
|
29808
|
-
|
|
29809
|
-
|
|
29810
|
-
|
|
29811
|
-
|
|
29812
|
-
|
|
29813
|
-
|
|
29814
|
-
|
|
29815
|
-
|
|
29816
|
-
"every question, with nothing on stdout"
|
|
29817
|
-
]
|
|
29650
|
+
group: "Inspect",
|
|
29651
|
+
summary: "Print the prompt for the closing message",
|
|
29652
|
+
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.
|
|
29653
|
+
|
|
29654
|
+
Refuses when the workflow declares no summary template, or when the process has no commits.`
|
|
29655
|
+
},
|
|
29656
|
+
{
|
|
29657
|
+
token: "base",
|
|
29658
|
+
kind: "base",
|
|
29659
|
+
arity: "none",
|
|
29660
|
+
group: "Inspect",
|
|
29661
|
+
summary: "Print the commit a review diff starts from",
|
|
29662
|
+
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.`
|
|
29818
29663
|
},
|
|
29819
29664
|
{
|
|
29820
29665
|
token: "strays",
|
|
@@ -29823,45 +29668,27 @@ const COMMAND_ROWS = [
|
|
|
29823
29668
|
name: "range",
|
|
29824
29669
|
variadic: "glob"
|
|
29825
29670
|
},
|
|
29826
|
-
|
|
29827
|
-
|
|
29828
|
-
|
|
29829
|
-
|
|
29830
|
-
|
|
29831
|
-
"globs in the shell. stdout: a sorted JSON array, [] when",
|
|
29832
|
-
"clean; stderr: a summary, plus one stray: line per path with",
|
|
29833
|
-
"--verbose. --json=<n> picks one entry, nothing past the end;",
|
|
29834
|
-
"a symlink is a path like any other, never followed; a",
|
|
29835
|
-
"submodule counts as its path, whatever diff.ignoreSubmodules",
|
|
29836
|
-
"or .gitmodules ignore say. Runs in any git repository, no",
|
|
29837
|
-
"process needed. Exits 10 when",
|
|
29838
|
-
"strays were found, 1 on an unknown revision, 2 on a bad range"
|
|
29839
|
-
]
|
|
29671
|
+
group: "Inspect",
|
|
29672
|
+
summary: "List changed files outside the globs",
|
|
29673
|
+
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.
|
|
29674
|
+
|
|
29675
|
+
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.`
|
|
29840
29676
|
},
|
|
29841
29677
|
{
|
|
29842
29678
|
token: "history list",
|
|
29843
29679
|
kind: "historyList",
|
|
29844
29680
|
arity: "none",
|
|
29845
|
-
|
|
29846
|
-
|
|
29847
|
-
|
|
29848
|
-
"--json prints one array of { ref, entry, branch, tip, date,",
|
|
29849
|
-
"subject }. Writes nothing and never replays; with no refs it",
|
|
29850
|
-
"hints at `gtd history fetch | sh` and still exits 0"
|
|
29851
|
-
]
|
|
29681
|
+
group: "Inspect",
|
|
29682
|
+
summary: "List finished processes",
|
|
29683
|
+
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\`.`
|
|
29852
29684
|
},
|
|
29853
29685
|
{
|
|
29854
29686
|
token: "history show",
|
|
29855
29687
|
kind: "historyShow",
|
|
29856
29688
|
arity: { name: "ref" },
|
|
29857
|
-
|
|
29858
|
-
|
|
29859
|
-
|
|
29860
|
-
"entry hash (or unique prefix) or a branch name. Refuses",
|
|
29861
|
-
"(exit 1) when a config file differs from its content at the",
|
|
29862
|
-
"ref's tip. A version mismatch only warns on stderr. --json",
|
|
29863
|
-
"prints the trail as one document; writes nothing"
|
|
29864
|
-
]
|
|
29689
|
+
group: "Inspect",
|
|
29690
|
+
summary: "Replay one finished process",
|
|
29691
|
+
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.`
|
|
29865
29692
|
},
|
|
29866
29693
|
{
|
|
29867
29694
|
token: "history metrics",
|
|
@@ -29870,18 +29697,9 @@ const COMMAND_ROWS = [
|
|
|
29870
29697
|
name: "ref",
|
|
29871
29698
|
many: true
|
|
29872
29699
|
},
|
|
29873
|
-
|
|
29874
|
-
|
|
29875
|
-
|
|
29876
|
-
"model and phase with the double work of a rebuild, quality-lap",
|
|
29877
|
-
"findings per lens, and red checks that passed on their",
|
|
29878
|
-
"re-check — per process and in total. Each <ref>",
|
|
29879
|
-
"is read as by history show, but a branch name means every",
|
|
29880
|
-
"process kept from it; none means every ref.",
|
|
29881
|
-
"Reads commit subjects and trailers, never replays, so a",
|
|
29882
|
-
"changed config is no refusal. --json prints { processes, total",
|
|
29883
|
-
"} as one document; writes nothing"
|
|
29884
|
-
]
|
|
29700
|
+
group: "Inspect",
|
|
29701
|
+
summary: "Report cost and rework of finished processes",
|
|
29702
|
+
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.`
|
|
29885
29703
|
},
|
|
29886
29704
|
{
|
|
29887
29705
|
token: "history judges",
|
|
@@ -29890,18 +29708,9 @@ const COMMAND_ROWS = [
|
|
|
29890
29708
|
name: "ref",
|
|
29891
29709
|
many: true
|
|
29892
29710
|
},
|
|
29893
|
-
|
|
29894
|
-
|
|
29895
|
-
|
|
29896
|
-
"and p bucket, how many verdicts later events overturned.",
|
|
29897
|
-
"--var <name>=<value> re-decides every past verdict under",
|
|
29898
|
-
"another threshold and prints what would have changed;",
|
|
29899
|
-
"--jsonl prints one { process, judge, commit, step, question,",
|
|
29900
|
-
"state, answer, p, outcome } line per verdict instead. <ref>",
|
|
29901
|
-
"is read as by history metrics. Never replays and asks no",
|
|
29902
|
-
"model. --json prints { processes, total, whatIf }; writes",
|
|
29903
|
-
"nothing"
|
|
29904
|
-
]
|
|
29711
|
+
group: "Inspect",
|
|
29712
|
+
summary: "Calibrate the judges of finished processes",
|
|
29713
|
+
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.`
|
|
29905
29714
|
},
|
|
29906
29715
|
{
|
|
29907
29716
|
token: "history fetch",
|
|
@@ -29910,69 +29719,205 @@ const COMMAND_ROWS = [
|
|
|
29910
29719
|
name: "remote",
|
|
29911
29720
|
optional: true
|
|
29912
29721
|
},
|
|
29913
|
-
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
|
|
29917
|
-
|
|
29722
|
+
group: "Inspect",
|
|
29723
|
+
summary: "Print a script that fetches finished processes",
|
|
29724
|
+
details: `Print a script that fetches every finished process kept on <remote> (default: origin) and reports how many arrived. gtd runs no git itself:
|
|
29725
|
+
|
|
29726
|
+
s=$(gtd history fetch) && sh -c "$s"`
|
|
29727
|
+
},
|
|
29728
|
+
{
|
|
29729
|
+
token: "ui",
|
|
29730
|
+
kind: "ui",
|
|
29731
|
+
arity: "none",
|
|
29732
|
+
group: "Inspect",
|
|
29733
|
+
summary: "Serve the web and phone client",
|
|
29734
|
+
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.`
|
|
29735
|
+
},
|
|
29736
|
+
{
|
|
29737
|
+
token: "install",
|
|
29738
|
+
kind: "install",
|
|
29739
|
+
arity: "none",
|
|
29740
|
+
group: "Driver plumbing",
|
|
29741
|
+
summary: "Print a briefing for writing a driver",
|
|
29742
|
+
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.`
|
|
29743
|
+
},
|
|
29744
|
+
{
|
|
29745
|
+
token: "judge",
|
|
29746
|
+
kind: "judge",
|
|
29747
|
+
arity: "none",
|
|
29748
|
+
group: "Driver plumbing",
|
|
29749
|
+
summary: "Print the pending judgment",
|
|
29750
|
+
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.`
|
|
29751
|
+
},
|
|
29752
|
+
{
|
|
29753
|
+
token: "judge answer",
|
|
29754
|
+
kind: "judgeAnswer",
|
|
29755
|
+
arity: "none",
|
|
29756
|
+
group: "Driver plumbing",
|
|
29757
|
+
summary: "Land a verdict read from stdin",
|
|
29758
|
+
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).`
|
|
29759
|
+
},
|
|
29760
|
+
{
|
|
29761
|
+
token: "judge run",
|
|
29762
|
+
kind: "judgeRun",
|
|
29763
|
+
arity: "none",
|
|
29764
|
+
group: "Driver plumbing",
|
|
29765
|
+
summary: "Answer a judgment with a model",
|
|
29766
|
+
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.
|
|
29767
|
+
|
|
29768
|
+
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.`
|
|
29769
|
+
},
|
|
29770
|
+
{
|
|
29771
|
+
token: "check",
|
|
29772
|
+
kind: "check",
|
|
29773
|
+
arity: { names: ["mode", "file"] },
|
|
29774
|
+
group: "Driver plumbing",
|
|
29775
|
+
summary: "Check a steering file's format",
|
|
29776
|
+
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.`
|
|
29777
|
+
},
|
|
29778
|
+
{
|
|
29779
|
+
token: "uncheck",
|
|
29780
|
+
kind: "uncheck",
|
|
29781
|
+
arity: { name: "file" },
|
|
29782
|
+
group: "Driver plumbing",
|
|
29783
|
+
summary: "Untick a review file's boxes",
|
|
29784
|
+
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.`
|
|
29785
|
+
},
|
|
29786
|
+
{
|
|
29787
|
+
token: "lsp",
|
|
29788
|
+
kind: "lsp",
|
|
29789
|
+
arity: "none",
|
|
29790
|
+
group: "Driver plumbing",
|
|
29791
|
+
summary: "Start the steering-file language server",
|
|
29792
|
+
details: `Start the language server for .gtd/ steering files on stdio.`
|
|
29918
29793
|
}
|
|
29919
29794
|
];
|
|
29920
29795
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
29921
|
-
|
|
29922
|
-
const
|
|
29923
|
-
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
const
|
|
29928
|
-
const
|
|
29929
|
-
const
|
|
29796
|
+
/** Every word that may follow `gtd`, for "did you mean". */
|
|
29797
|
+
const COMMAND_WORDS = [.../* @__PURE__ */ new Set([
|
|
29798
|
+
...COMMAND_ROWS.map((r) => r.token.split(" ")[0]),
|
|
29799
|
+
"help",
|
|
29800
|
+
"version"
|
|
29801
|
+
])];
|
|
29802
|
+
const TOP_COLUMN = 28;
|
|
29803
|
+
const OPTION_COLUMN = 22;
|
|
29804
|
+
const synopsis = (row) => {
|
|
29805
|
+
const { token, arity } = row;
|
|
29806
|
+
if (arity === "none") return token;
|
|
29807
|
+
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`;
|
|
29808
|
+
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`;
|
|
29809
|
+
if ("optional" in arity) return `${token} [${arity.name}]`;
|
|
29810
|
+
if ("many" in arity) return `${token} [${arity.name}...]`;
|
|
29811
|
+
return `${token} <${arity.name}>`;
|
|
29812
|
+
};
|
|
29930
29813
|
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--workflow", "--var"]);
|
|
29931
29814
|
const flagHeader = (row) => {
|
|
29932
29815
|
if (row.valueHint === "") return row.name;
|
|
29816
|
+
if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`;
|
|
29933
29817
|
return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
|
|
29934
29818
|
};
|
|
29819
|
+
const START_ROW = {
|
|
29820
|
+
term: "--workflow <name>",
|
|
29821
|
+
summary: "Start a process on a workflow; --var <name>=<value> pins one of its settings"
|
|
29822
|
+
};
|
|
29823
|
+
const GLOBAL_OPTIONS = [
|
|
29824
|
+
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
29825
|
+
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
29826
|
+
["-V, --version", "Print the version"]
|
|
29827
|
+
];
|
|
29828
|
+
const EXTRA_TOPICS = [[
|
|
29829
|
+
"help [command]",
|
|
29830
|
+
"Print help for gtd or one command",
|
|
29831
|
+
"Print gtd's overview, or with <command> that command's own help."
|
|
29832
|
+
], [
|
|
29833
|
+
"version",
|
|
29834
|
+
"Print the version",
|
|
29835
|
+
"Print gtd's version."
|
|
29836
|
+
]];
|
|
29837
|
+
const EXAMPLES = ` $ gtd --workflow <name> start a process
|
|
29838
|
+
$ gtd next see what happens next
|
|
29839
|
+
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
29840
|
+
$ gtd help land one command's details`;
|
|
29841
|
+
const lines = (block) => block.map((l) => `${l}\n`).join("");
|
|
29842
|
+
const GROUPS = [
|
|
29843
|
+
"Core loop",
|
|
29844
|
+
"Start and stop",
|
|
29845
|
+
"Inspect",
|
|
29846
|
+
"Driver plumbing"
|
|
29847
|
+
];
|
|
29935
29848
|
const renderHelp = () => {
|
|
29936
|
-
const
|
|
29937
|
-
const
|
|
29938
|
-
|
|
29849
|
+
const groups = GROUPS.map((group) => {
|
|
29850
|
+
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) => definition(synopsis(r), r.summary, TOP_COLUMN));
|
|
29851
|
+
const start = group === "Start and stop" ? definition(START_ROW.term, START_ROW.summary, TOP_COLUMN) : [];
|
|
29852
|
+
const extra = group === "Driver plumbing" ? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN)) : [];
|
|
29853
|
+
return `${group}:\n${lines([
|
|
29854
|
+
...start,
|
|
29855
|
+
...rows,
|
|
29856
|
+
...extra
|
|
29857
|
+
])}`;
|
|
29939
29858
|
});
|
|
29940
|
-
return
|
|
29941
|
-
|
|
29942
|
-
|
|
29943
|
-
|
|
29944
|
-
|
|
29945
|
-
|
|
29946
|
-
|
|
29859
|
+
return "Usage: gtd <command> [options]\n\n" + lines(wrap("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.")) + "\n" + groups.join("\n") + "\nOptions:\n" + lines(GLOBAL_OPTIONS.flatMap(([term, text]) => definition(term, text, TOP_COLUMN))) + `\nExamples:\n${EXAMPLES}\n\n` + lines(wrap("Run `gtd help <command>` for a command's details and options, and `gtd install` to learn how to drive gtd."));
|
|
29860
|
+
};
|
|
29861
|
+
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
29862
|
+
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
29863
|
+
|
|
29864
|
+
gtd --workflow <name> start a process
|
|
29865
|
+
gtd next print what should happen next
|
|
29866
|
+
gtd land record what the tree now shows
|
|
29867
|
+
|
|
29868
|
+
`;
|
|
29869
|
+
const renderCommandHelp = (row) => {
|
|
29870
|
+
const options = FLAGS.filter((f) => f.name !== "--verbose" && f.scope(row.kind)).flatMap((f) => definition(flagHeader(f), f.helpFor?.[row.kind] ?? f.help, OPTION_COLUMN));
|
|
29871
|
+
const global = definition("-v, --verbose", "Narrate on stderr what gtd resolved", OPTION_COLUMN);
|
|
29872
|
+
return `Usage: gtd ${synopsis(row)}${options.length > 0 ? " [options]" : ""}\n\n` + lines(wrap(row.details)) + `\nOptions:\n${lines([...options, ...global])}`;
|
|
29873
|
+
};
|
|
29874
|
+
/** `gtd help <topic…>`'s text: one command's help, every `history` subcommand's for `history`, or undefined for an unknown topic. */
|
|
29875
|
+
const renderTopicHelp = (topic) => {
|
|
29876
|
+
const two = commandByToken(topic.slice(0, 2).join(" "));
|
|
29877
|
+
if (two !== void 0 && topic.length >= 2) return renderCommandHelp(two);
|
|
29878
|
+
const [first = ""] = topic;
|
|
29879
|
+
const one = commandByToken(first);
|
|
29880
|
+
if (one !== void 0) return renderCommandHelp(one);
|
|
29881
|
+
const extra = EXTRA_TOPICS.find(([term]) => term.split(" ")[0] === first);
|
|
29882
|
+
if (extra !== void 0) return `Usage: gtd ${extra[0]}\n\n${lines(wrap(extra[2]))}`;
|
|
29883
|
+
const family = COMMAND_ROWS.filter((r) => r.token.startsWith(`${first} `));
|
|
29884
|
+
if (first !== "" && family.length > 0) return family.map(renderCommandHelp).join("\n");
|
|
29885
|
+
};
|
|
29886
|
+
/** A usage error's tail: the nearest candidate when `word` looks like a typo of one, and where the full list is. */
|
|
29887
|
+
const didYouMean = (word, candidates, prefix = "", pointer = "`gtd help` for every command") => {
|
|
29888
|
+
const guess = closest(word, candidates);
|
|
29889
|
+
return guess === void 0 ? ` — run ${pointer}` : ` — did you mean \`${prefix}${guess}\`? Run ${pointer}`;
|
|
29890
|
+
};
|
|
29891
|
+
const unknownOption = (tok, jsonSeen) => {
|
|
29892
|
+
const candidates = tok.startsWith("--") ? FLAGS.map((f) => f.name) : [];
|
|
29893
|
+
return {
|
|
29894
|
+
kind: "error",
|
|
29895
|
+
message: `gtd: unknown option '${tok}'${didYouMean(tok.split("=")[0], candidates, "", "`gtd help <command>` for its options")}`,
|
|
29896
|
+
jsonSeen
|
|
29897
|
+
};
|
|
29947
29898
|
};
|
|
29948
29899
|
const tokenize = (tail) => {
|
|
29949
29900
|
const positionals = [];
|
|
29950
29901
|
const byFlag = /* @__PURE__ */ new Map();
|
|
29951
29902
|
const present = /* @__PURE__ */ new Set();
|
|
29952
29903
|
let jsonSeen = false;
|
|
29904
|
+
let afterBareJson;
|
|
29953
29905
|
for (let i = 0; i < tail.length; i++) {
|
|
29954
29906
|
const original = tail[i];
|
|
29955
29907
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original;
|
|
29956
|
-
if (!tok.startsWith("
|
|
29908
|
+
if (!tok.startsWith("-") || tok === "-") {
|
|
29909
|
+
if (tail[i - 1] === "--json") afterBareJson ??= original;
|
|
29957
29910
|
positionals.push(original);
|
|
29958
29911
|
continue;
|
|
29959
29912
|
}
|
|
29960
|
-
const row = flagByToken(tok);
|
|
29961
|
-
if (row === void 0) return
|
|
29962
|
-
kind: "error",
|
|
29963
|
-
message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
|
|
29964
|
-
jsonSeen
|
|
29965
|
-
};
|
|
29913
|
+
const row = tok.startsWith("--") ? flagByToken(tok) : void 0;
|
|
29914
|
+
if (row === void 0) return unknownOption(tok, jsonSeen);
|
|
29966
29915
|
const eq = tok.indexOf("=");
|
|
29967
29916
|
const alreadyPresent = present.has(row.name);
|
|
29968
29917
|
present.add(row.name);
|
|
29969
29918
|
if (row.name === "--json") jsonSeen = true;
|
|
29970
29919
|
if (row.arity === 0) {
|
|
29971
|
-
if (eq !== -1) return
|
|
29972
|
-
kind: "error",
|
|
29973
|
-
message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
|
|
29974
|
-
jsonSeen
|
|
29975
|
-
};
|
|
29920
|
+
if (eq !== -1) return unknownOption(tok, jsonSeen);
|
|
29976
29921
|
if (!byFlag.has(row.name)) byFlag.set(row.name, []);
|
|
29977
29922
|
continue;
|
|
29978
29923
|
}
|
|
@@ -30023,7 +29968,8 @@ const tokenize = (tail) => {
|
|
|
30023
29968
|
positionals,
|
|
30024
29969
|
byFlag,
|
|
30025
29970
|
present,
|
|
30026
|
-
jsonSeen
|
|
29971
|
+
jsonSeen,
|
|
29972
|
+
...afterBareJson !== void 0 ? { afterBareJson } : {}
|
|
30027
29973
|
};
|
|
30028
29974
|
};
|
|
30029
29975
|
const usagePlan = (message, json, stdout = "") => ({
|
|
@@ -30075,18 +30021,38 @@ const buildLandCommand = (bag) => ({
|
|
|
30075
30021
|
...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
|
|
30076
30022
|
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
30077
30023
|
});
|
|
30078
|
-
const
|
|
30079
|
-
|
|
30024
|
+
const HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
|
|
30025
|
+
/** `--help`/`-h` anywhere: the named command's own help, else the overview — never a usage error, so a typo'd command still gets help. */
|
|
30026
|
+
const helpPlan = (argv) => {
|
|
30027
|
+
const tokenized = tokenize(argv.slice(2).filter((tok) => !HELP_FLAGS.has(tok)));
|
|
30028
|
+
const topic = tokenized.kind === "ok" ? tokenized.positionals : [];
|
|
30029
|
+
return {
|
|
30030
|
+
kind: "output",
|
|
30031
|
+
stdout: renderTopicHelp(topic) ?? renderHelp()
|
|
30032
|
+
};
|
|
30033
|
+
};
|
|
30034
|
+
/** `gtd help [topic…]`: an unknown topic is a usage error naming the nearest command. */
|
|
30035
|
+
const helpCommandPlan = (topic, json) => {
|
|
30036
|
+
if (topic.length === 0) return {
|
|
30080
30037
|
kind: "output",
|
|
30081
30038
|
stdout: renderHelp()
|
|
30082
30039
|
};
|
|
30040
|
+
const text = renderTopicHelp(topic);
|
|
30041
|
+
if (text !== void 0) return {
|
|
30042
|
+
kind: "output",
|
|
30043
|
+
stdout: text
|
|
30044
|
+
};
|
|
30045
|
+
return usagePlan(`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0], COMMAND_WORDS, "gtd help ")}`, json);
|
|
30046
|
+
};
|
|
30047
|
+
const parseArgv = (argv) => {
|
|
30048
|
+
if (argv.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv);
|
|
30083
30049
|
if (argv.includes("--version") || argv.includes("-V")) return {
|
|
30084
30050
|
kind: "output",
|
|
30085
30051
|
stdout: `${GTD_VERSION}\n`
|
|
30086
30052
|
};
|
|
30087
30053
|
const tokenized = tokenize(argv.slice(2));
|
|
30088
30054
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
|
|
30089
|
-
const { positionals, byFlag, present, jsonSeen } = tokenized;
|
|
30055
|
+
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized;
|
|
30090
30056
|
const first = positionals[0];
|
|
30091
30057
|
const workflowRaw = byFlag.get("--workflow")?.[0];
|
|
30092
30058
|
const workflowPresent = workflowRaw !== void 0;
|
|
@@ -30094,13 +30060,14 @@ const parseArgv = (argv) => {
|
|
|
30094
30060
|
kind: "output",
|
|
30095
30061
|
stdout: `${GTD_VERSION}\n`
|
|
30096
30062
|
};
|
|
30097
|
-
if (first === "help") return
|
|
30098
|
-
kind: "output",
|
|
30099
|
-
stdout: renderHelp()
|
|
30100
|
-
};
|
|
30063
|
+
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen);
|
|
30101
30064
|
const isHistory = first === "history";
|
|
30102
30065
|
const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
|
|
30103
|
-
if (isHistory && row === void 0)
|
|
30066
|
+
if (isHistory && row === void 0) {
|
|
30067
|
+
const subcommands = COMMAND_ROWS.filter((r) => r.token.startsWith("history ")).map((r) => r.token.split(" ")[1]);
|
|
30068
|
+
const typo = closest(positionals[1] ?? "", subcommands);
|
|
30069
|
+
return usagePlan(typo !== void 0 ? `gtd history: unknown subcommand '${positionals[1]}' — did you mean \`gtd history ${typo}\`?` : "gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]", jsonSeen);
|
|
30070
|
+
}
|
|
30104
30071
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
|
|
30105
30072
|
const selectsStart = workflowPresent && first === void 0;
|
|
30106
30073
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run";
|
|
@@ -30108,15 +30075,18 @@ const parseArgv = (argv) => {
|
|
|
30108
30075
|
if (row === void 0 && !selectsStart) {
|
|
30109
30076
|
const violation = scopeViolation(void 0, present);
|
|
30110
30077
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
30111
|
-
if (first === void 0) return usagePlan("gtd: missing command —
|
|
30112
|
-
return usagePlan(`unknown command '${first}'`, jsonSeen);
|
|
30078
|
+
if (first === void 0) return usagePlan("gtd: missing command — run `gtd help` for every command, or `gtd install` to learn how to drive gtd", jsonSeen, SHORT_USAGE);
|
|
30079
|
+
return usagePlan(`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`, jsonSeen);
|
|
30113
30080
|
}
|
|
30114
30081
|
const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
|
|
30115
30082
|
if (kind === "start" && first === void 0) {
|
|
30116
30083
|
if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
|
|
30117
30084
|
} else {
|
|
30118
|
-
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
|
|
30119
|
-
if (arityMsg !== void 0)
|
|
30085
|
+
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") || kind === "door" && restPositionals.length === 0 ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
|
|
30086
|
+
if (arityMsg !== void 0) {
|
|
30087
|
+
const hint = afterBareJson !== void 0 && restPositionals.includes(afterBareJson) ? ` — to print one field, write --json=${afterBareJson}` : "";
|
|
30088
|
+
return usagePlan(arityMsg + hint, jsonSeen);
|
|
30089
|
+
}
|
|
30120
30090
|
}
|
|
30121
30091
|
const violation = scopeViolation(kind, present);
|
|
30122
30092
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
@@ -30158,7 +30128,7 @@ const parseArgv = (argv) => {
|
|
|
30158
30128
|
kind: "command",
|
|
30159
30129
|
command: {
|
|
30160
30130
|
kind: "door",
|
|
30161
|
-
name: restPositionals[0],
|
|
30131
|
+
...restPositionals[0] !== void 0 ? { name: restPositionals[0] } : {},
|
|
30162
30132
|
args: restPositionals.slice(1)
|
|
30163
30133
|
},
|
|
30164
30134
|
json,
|
|
@@ -31788,12 +31758,10 @@ const recognizeGtdUncheck = (repo, block) => {
|
|
|
31788
31758
|
return { kind: "noop" };
|
|
31789
31759
|
};
|
|
31790
31760
|
/**
|
|
31791
|
-
*
|
|
31792
|
-
*
|
|
31793
|
-
* script, so recognizing it as a no-op (like the retry-helper function
|
|
31794
|
-
* definition) keeps the `@inmem` tier green.
|
|
31761
|
+
* A block of nothing but comments is a no-op in `sh` too — `combinedScript`'s
|
|
31762
|
+
* leading `DID_NOT_RUN_COMMENT`, and the list of paths a start commits.
|
|
31795
31763
|
*/
|
|
31796
|
-
const
|
|
31764
|
+
const recognizeCommentBlock = (block) => block.split("\n").every((line) => line.startsWith("#")) ? { kind: "noop" } : void 0;
|
|
31797
31765
|
/** `combinedScript`'s optional half. Reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`. Exception: under `git: false` any unrecognized inner block fails the outer script (git or not), so the in-memory twin knowingly diverges from `sh` rather than run git unseen. */
|
|
31798
31766
|
const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0].replace(/\\n$/, "")}\n`;
|
|
31799
31767
|
const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`;
|
|
@@ -31907,7 +31875,7 @@ const recognizeWriteLines = (repo, block) => {
|
|
|
31907
31875
|
/** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
|
|
31908
31876
|
const recognizersFor = (repo, commands, git) => [
|
|
31909
31877
|
(block) => recognizeSetFlags(block),
|
|
31910
|
-
(block) =>
|
|
31878
|
+
(block) => recognizeCommentBlock(block),
|
|
31911
31879
|
(block) => recognizePrecondition(repo, block),
|
|
31912
31880
|
(block) => recognizeFileExistsGuard(repo, block),
|
|
31913
31881
|
(block) => recognizeBinaryGuard(commands, block),
|
|
@@ -32047,6 +32015,19 @@ const runLines = (repo, commands, script) => {
|
|
|
32047
32015
|
if (!applied.ok) throw cannotRun(applied.error ?? "");
|
|
32048
32016
|
};
|
|
32049
32017
|
const failed = (what, result) => /* @__PURE__ */ new Error(`${what} exited ${result.exitCode}\n${result.stderr}${result.stdout}`);
|
|
32018
|
+
const ANSWER = {
|
|
32019
|
+
noul: "true",
|
|
32020
|
+
choice: "\"<option>\"",
|
|
32021
|
+
score: "0.5"
|
|
32022
|
+
};
|
|
32023
|
+
/** The verdict a judge beat's questions take, as a template to copy. */
|
|
32024
|
+
const expectedVerdict = (doc) => {
|
|
32025
|
+
let questions = [];
|
|
32026
|
+
try {
|
|
32027
|
+
questions = JSON.parse(doc.judge ?? "").questions;
|
|
32028
|
+
} catch {}
|
|
32029
|
+
return ["expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:", `[${questions.map(({ id, primitive }) => `{ "id": ${JSON.stringify(id)}, "answer": ${ANSWER[primitive] ?? "\"<answer>\""}, "p": 0.9 }`).join(", ")}]`].join("\n");
|
|
32030
|
+
};
|
|
32050
32031
|
const testRepo = (options) => {
|
|
32051
32032
|
const repo = new InMemRepo();
|
|
32052
32033
|
const env = { ...options.env };
|
|
@@ -32101,10 +32082,10 @@ const testRepo = (options) => {
|
|
|
32101
32082
|
if (!applied.ok) throw new Error(`${what}: its script failed — ${applied.error ?? ""}`);
|
|
32102
32083
|
};
|
|
32103
32084
|
/** Runs a write command, then the script its `--json=script` twin prints. */
|
|
32104
|
-
const runWrite = async (args, stdin) => {
|
|
32085
|
+
const runWrite = async (args, stdin, refused = (error) => error) => {
|
|
32105
32086
|
const call = (...extra) => stdin === void 0 ? gtd(...args, ...extra) : withStdin(stdin, () => gtd(...args, ...extra));
|
|
32106
32087
|
const reported = await call();
|
|
32107
|
-
if (reported.exitCode !== 0) throw failed(`gtd ${args.join(" ")}`, reported);
|
|
32088
|
+
if (reported.exitCode !== 0) throw refused(failed(`gtd ${args.join(" ")}`, reported));
|
|
32108
32089
|
const script = await call("--json=script");
|
|
32109
32090
|
apply(`gtd ${args.join(" ")}`, script.stdout);
|
|
32110
32091
|
return reported;
|
|
@@ -32127,8 +32108,10 @@ const testRepo = (options) => {
|
|
|
32127
32108
|
await runWrite(["land"]);
|
|
32128
32109
|
return;
|
|
32129
32110
|
}
|
|
32130
|
-
|
|
32131
|
-
|
|
32111
|
+
const actor = `the actor for judge step "${doc.state}"`;
|
|
32112
|
+
if (verdict === void 0) throw new Error(`${actor} returned no verdict\n${expectedVerdict(doc)}`);
|
|
32113
|
+
if (typeof verdict !== "string") throw new Error(`${actor} returned ${Array.isArray(verdict) ? "an array" : typeof verdict === "object" ? "an object" : `a ${typeof verdict}`}, not a JSON string — JSON.stringify it\n${expectedVerdict(doc)}`);
|
|
32114
|
+
await runWrite(["judge", "answer"], verdict, (error) => /* @__PURE__ */ new Error(`${actor} sent a verdict gtd refused:\n${verdict}\n${expectedVerdict(doc)}\n${error.message}`));
|
|
32132
32115
|
};
|
|
32133
32116
|
const self = {
|
|
32134
32117
|
command: (command) => {
|
|
@@ -32167,8 +32150,10 @@ const testRepo = (options) => {
|
|
|
32167
32150
|
land: () => runWrite(["land"]),
|
|
32168
32151
|
next,
|
|
32169
32152
|
drive: async (actors = {}, { maxBeats = 50 } = {}) => {
|
|
32153
|
+
const passed = [];
|
|
32170
32154
|
for (let beat = 0; beat < maxBeats; beat++) {
|
|
32171
32155
|
const doc = await next();
|
|
32156
|
+
passed.push(doc.state);
|
|
32172
32157
|
if (doc.kind === "stalled") return doc;
|
|
32173
32158
|
if (doc.kind === "capture") {
|
|
32174
32159
|
await runWrite(["land"]);
|
|
@@ -32178,7 +32163,7 @@ const testRepo = (options) => {
|
|
|
32178
32163
|
if (actor === void 0) return doc;
|
|
32179
32164
|
await act(doc, await actor(doc, self));
|
|
32180
32165
|
}
|
|
32181
|
-
throw new Error(`the process did not rest within ${maxBeats} beats`);
|
|
32166
|
+
throw new Error(`the process did not rest within ${maxBeats} beats; it passed through:\n${passed.join(" → ")}`);
|
|
32182
32167
|
},
|
|
32183
32168
|
subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
|
|
32184
32169
|
messages: () => repo.commitHistory().map(({ message }) => message),
|