@pmelab/gtd 59.0.0 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { C as moveScript, D as revertScript, E as restoreScript, H as installContext, K as markedOptions, O as showScript, S as checkScript, T as removeScript, Y as protocolMismatch, i as configure, k as writeScript$1, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, x as applyScript } from "./flows-DENt0uWV.mjs";
|
|
2
|
+
import { O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, j as renderFormat, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-DZ8R1RU9.mjs";
|
|
3
|
+
import { M as headingSectionBodies, N as formatFinding, a as sections, c as openThreads, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-DF_BMadH.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { PassThrough } from "node:stream";
|
|
@@ -24210,7 +24210,7 @@ if [ -z "\${1:-}" ]; then
|
|
|
24210
24210
|
exit 2
|
|
24211
24211
|
fi
|
|
24212
24212
|
cd "$(git rev-parse --show-toplevel)"
|
|
24213
|
-
script="$(gtd
|
|
24213
|
+
script="$(gtd start "$@")"
|
|
24214
24214
|
sh -c "$script"
|
|
24215
24215
|
exec "$GTD_BUILD"`;
|
|
24216
24216
|
const HEADER = () => `gtd ${GTD_VERSION} — driver protocol\n\ngtd decides and prints; the driver executes. gtd never runs git, never runs\nyour agent, never runs a check script, and never runs a steering mode's own\nformat:/validate: command either — that pair is text in the script gtd next\n--json's validate field (or gtd validate) prints, for the driver to run\nitself ahead of gtd land.\n`;
|
|
@@ -24389,7 +24389,7 @@ const DRIVER_OBLIGATIONS = `
|
|
|
24389
24389
|
\`--json=judge\`, pipes that rendered \`{ schema, state, questions }\` document to a
|
|
24390
24390
|
judge model, and pipes ITS verdict (one \`{ id, answer, p }\` per pending
|
|
24391
24391
|
question, as JSON on stdin) to \`gtd judge answer\` — the FIFTH command
|
|
24392
|
-
that changes anything, alongside \`gtd land\`/\`gtd
|
|
24392
|
+
that changes anything, alongside \`gtd land\`/\`gtd start\`/\`gtd
|
|
24393
24393
|
abandon\`/\`gtd restore\`. It follows the exact same required-half /
|
|
24394
24394
|
optional-half script contract as \`gtd land\` in obligation 8 above:
|
|
24395
24395
|
read \`--json=script\` (and, if you want them, \`--json=settled\`/
|
|
@@ -24586,9 +24586,9 @@ ${EDIT_COMMAND}
|
|
|
24586
24586
|
|
|
24587
24587
|
### \`gtd-door <name> [args...]\` — start a process through a door and drive it
|
|
24588
24588
|
|
|
24589
|
-
Runs \`gtd
|
|
24589
|
+
Runs \`gtd start <name> [args...]\`, captured by command substitution — never a
|
|
24590
24590
|
pipe: a pipeline reports only its LAST command's exit status, so
|
|
24591
|
-
\`gtd
|
|
24591
|
+
\`gtd start ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
|
|
24592
24592
|
loop over a process that was never started. It runs the captured script, then
|
|
24593
24593
|
\`exec\`s the suite's RESOLVED \`gtd-build\` path — never the literal string
|
|
24594
24594
|
\`gtd-build\`. This is one invocation that carries the process all the way to
|
|
@@ -24596,9 +24596,9 @@ its next human gate, not just a starter: \`gtd-door review\` on a RED baseline
|
|
|
24596
24596
|
hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
|
|
24597
24597
|
rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
24598
24598
|
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
24599
|
-
\`gtd
|
|
24600
|
-
|
|
24601
|
-
\`review [base]\`
|
|
24599
|
+
A bare \`gtd start\` lists what a repository can start: its workflows, and
|
|
24600
|
+
the doors those workflows bring (gtd-workflows' \`default\` brings \`fix\`
|
|
24601
|
+
and \`review [base]\`) plus any the config declares. Refuses with
|
|
24602
24602
|
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
24603
24603
|
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
24604
24604
|
|
|
@@ -24700,7 +24700,7 @@ const PREREQUISITES = `
|
|
|
24700
24700
|
## Prerequisites and portability
|
|
24701
24701
|
|
|
24702
24702
|
- A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
|
|
24703
|
-
scripts (\`gtd land --json=script\`, \`gtd
|
|
24703
|
+
scripts (\`gtd land --json=script\`, \`gtd start <name>\`, \`gtd abandon\`,
|
|
24704
24704
|
\`gtd restore\`, \`gtd judge answer --json=script\`) are POSIX sh; captured,
|
|
24705
24705
|
then piped into it (see obligation 8 above). Reading
|
|
24706
24706
|
\`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own output needs
|
|
@@ -24825,7 +24825,7 @@ const parseStepId = (raw) => {
|
|
|
24825
24825
|
occurrence: Number(match[2])
|
|
24826
24826
|
};
|
|
24827
24827
|
};
|
|
24828
|
-
const isVerdict = (value) => {
|
|
24828
|
+
const isVerdict$1 = (value) => {
|
|
24829
24829
|
if (typeof value !== "object" || value === null) return false;
|
|
24830
24830
|
const v = value;
|
|
24831
24831
|
return typeof v.id === "string" && [
|
|
@@ -24889,7 +24889,7 @@ const TRAILER_READERS = {
|
|
|
24889
24889
|
},
|
|
24890
24890
|
"Gtd-Judge": (value, into) => {
|
|
24891
24891
|
const parsed = parseJson(value);
|
|
24892
|
-
if (isVerdict(parsed)) into.judge.push(parsed);
|
|
24892
|
+
if (isVerdict$1(parsed)) into.judge.push(parsed);
|
|
24893
24893
|
},
|
|
24894
24894
|
"Gtd-Mark": (value, into) => {
|
|
24895
24895
|
const kv = MARK_RE.exec(value);
|
|
@@ -25107,6 +25107,11 @@ const approveConflict = (step) => {
|
|
|
25107
25107
|
if (keys.length === 0) return void 0;
|
|
25108
25108
|
return `gtd: step "${step.name}": approve cannot be combined with ${keys.join(", ")} in human() options — an approval records exactly the judged tree, and a steering file is rewritten on landing`;
|
|
25109
25109
|
};
|
|
25110
|
+
const modeWithoutFile = (step) => {
|
|
25111
|
+
const options = step.request.options;
|
|
25112
|
+
if (options.mode === void 0 || options.file !== void 0) return void 0;
|
|
25113
|
+
return `gtd: step "${step.name}": mode requires file in ${step.kind}() options — name the steering file the mode checks`;
|
|
25114
|
+
};
|
|
25110
25115
|
const replay = async (input) => {
|
|
25111
25116
|
const commits = input.episode.commits.map((c) => ({
|
|
25112
25117
|
...c,
|
|
@@ -25268,7 +25273,7 @@ const replay = async (input) => {
|
|
|
25268
25273
|
kind: "divergence",
|
|
25269
25274
|
message: `gtd: commit ${short(expected.hash)} names "${expected.name}" as the next step, but replay reached "${step.name}" — ${DIVERGED}`
|
|
25270
25275
|
};
|
|
25271
|
-
const error = unknownOptions(step) ?? approveConflict(step) ?? checkIdentity(step);
|
|
25276
|
+
const error = unknownOptions(step) ?? approveConflict(step) ?? modeWithoutFile(step) ?? checkIdentity(step);
|
|
25272
25277
|
return error === void 0 ? void 0 : {
|
|
25273
25278
|
kind: "failed",
|
|
25274
25279
|
message: error
|
|
@@ -25403,24 +25408,10 @@ const replay = async (input) => {
|
|
|
25403
25408
|
changes: () => changesBetween(previousPosition.tree, position.tree),
|
|
25404
25409
|
changesSince,
|
|
25405
25410
|
matches: globMatches,
|
|
25406
|
-
sections
|
|
25407
|
-
sectionBodies
|
|
25408
|
-
openQuestions
|
|
25409
|
-
|
|
25410
|
-
return qa === void 0 ? [] : unansweredQuestions(qa, text).map((q) => ({
|
|
25411
|
-
question: q.question,
|
|
25412
|
-
line: q.headingLine + 1
|
|
25413
|
-
}));
|
|
25414
|
-
},
|
|
25415
|
-
threads: (text) => {
|
|
25416
|
-
const { threads, findings } = parseThreadsWithFindings(text);
|
|
25417
|
-
return threads.map((t) => ({
|
|
25418
|
-
name: t.name,
|
|
25419
|
-
line: t.line + 1,
|
|
25420
|
-
waitingOn: t.waitingOn,
|
|
25421
|
-
faults: findings.filter((f) => f.message.startsWith(`Footnote thread "[^${t.name}]": `)).map((f) => f.message)
|
|
25422
|
-
}));
|
|
25423
|
-
},
|
|
25411
|
+
sections,
|
|
25412
|
+
sectionBodies,
|
|
25413
|
+
openQuestions,
|
|
25414
|
+
threads,
|
|
25424
25415
|
codeThreads: () => changesBetween(treeAt(input.start) ?? input.startTree ?? input.episode.base.tree, position.tree).filter((c) => c.status !== "deleted").flatMap((c) => {
|
|
25425
25416
|
const { threads, findings } = parseCodeThreads(c.path, position.tree.read(c.path) ?? "");
|
|
25426
25417
|
return threads.map((t) => ({
|
|
@@ -25743,7 +25734,7 @@ const dedupeDiagnostics = (diagnostics) => {
|
|
|
25743
25734
|
};
|
|
25744
25735
|
//#endregion
|
|
25745
25736
|
//#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")}`);
|
|
25737
|
+
const configError = (diagnostics) => new GtdError(`gtd: config:\n${diagnostics.filter((d) => d.severity === "error").map((d) => ` - ${formatDiagnostic(d)}`).join("\n")}`);
|
|
25747
25738
|
const failOnErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error") ? fail$3(configError(diagnostics)) : _void;
|
|
25748
25739
|
const MIGRATION_DOC = "https://github.com/pmelab/gtd/blob/main/docs/configuration.md#migrating-from-gtdrc";
|
|
25749
25740
|
/**
|
|
@@ -25759,11 +25750,11 @@ const refuseLegacyRc = gen(function* () {
|
|
|
25759
25750
|
const found = files.filter((f) => f.owned).map((f) => f.path);
|
|
25760
25751
|
if (found.length === 0) return;
|
|
25761
25752
|
return yield* fail$3(new GtdError([
|
|
25762
|
-
"gtd config:",
|
|
25753
|
+
"gtd: config:",
|
|
25763
25754
|
...found.map((file) => ` - ${file}: no longer read — delete it`),
|
|
25764
25755
|
" gtd 23 removed the .gtdrc files; move each key they set:",
|
|
25765
|
-
" vars → an option of the workflow factory in gtd.config.ts, or
|
|
25766
|
-
" env → an option of the workflow factory in gtd.config.ts, or
|
|
25756
|
+
" vars → an option of the workflow factory in gtd.config.ts, or `workflow(flow, { defaults })`",
|
|
25757
|
+
" env → an option of the workflow factory in gtd.config.ts, or `workflow(flow, { envDefaults })`",
|
|
25767
25758
|
" judge → ~/.config/gtd/judge.json, or GTD_JUDGE_PROVIDER / GTD_JUDGE_MODEL",
|
|
25768
25759
|
" modes, ui, skills, access → the `config` export of gtd.config.ts (`defineConfig`)",
|
|
25769
25760
|
" $schema → nothing; drop it",
|
|
@@ -25783,7 +25774,7 @@ const load = gen(function* () {
|
|
|
25783
25774
|
const missing = {
|
|
25784
25775
|
severity: "error",
|
|
25785
25776
|
path: [],
|
|
25786
|
-
message: `no workflows configured: no ${WORKFLOW_MODULE} here or above — gtd ships no workflow; \`gtd
|
|
25777
|
+
message: `no workflows configured: no ${WORKFLOW_MODULE} here or above — gtd ships no workflow; \`gtd guide\` says where to get one`,
|
|
25787
25778
|
origin: host.root
|
|
25788
25779
|
};
|
|
25789
25780
|
return yield* fail$3(configError([missing]));
|
|
@@ -25795,23 +25786,22 @@ const load = gen(function* () {
|
|
|
25795
25786
|
});
|
|
25796
25787
|
const loaded = yield* try_({
|
|
25797
25788
|
try: () => loadWorkflows(module, shipped),
|
|
25798
|
-
catch: (e) => new GtdError(`gtd config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
25789
|
+
catch: (e) => new GtdError(`gtd: config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
|
|
25799
25790
|
});
|
|
25800
25791
|
const compiled = loaded.config;
|
|
25801
25792
|
const diagnostics = dedupeDiagnostics(sortDiagnostics([...compiled.diagnostics, ...loaded.diagnostics]));
|
|
25802
25793
|
yield* failOnErrors(diagnostics);
|
|
25803
25794
|
if (loaded.default === void 0) return yield* die("unreachable: a default fails to build only with an error reported");
|
|
25804
25795
|
const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
|
|
25805
|
-
const steering = loaded.file.steering;
|
|
25806
25796
|
const define = (entry) => ({
|
|
25807
25797
|
flow: entry.flow,
|
|
25808
|
-
summary: entry.
|
|
25809
|
-
base: (w, v) => entry.
|
|
25810
|
-
steering,
|
|
25798
|
+
summary: entry.summary,
|
|
25799
|
+
base: (w, v) => entry.base?.(w, v),
|
|
25800
|
+
steering: entry.steering,
|
|
25811
25801
|
modes: compiled.modes,
|
|
25812
|
-
skills: entry.
|
|
25802
|
+
skills: entry.skills,
|
|
25813
25803
|
configuredSkills: compiled.skills,
|
|
25814
|
-
access: entry.
|
|
25804
|
+
access: entry.access,
|
|
25815
25805
|
configuredAccess: compiled.access,
|
|
25816
25806
|
skillsOrigin: entry.file.origin,
|
|
25817
25807
|
initial
|
|
@@ -25933,48 +25923,69 @@ const requiredOnUse = (modules, files) => {
|
|
|
25933
25923
|
get: (loaded, id) => typeof id !== "string" || id in loaded ? loaded[id] : loaded[id] = require(files.get(id))
|
|
25934
25924
|
});
|
|
25935
25925
|
};
|
|
25936
|
-
|
|
25937
|
-
|
|
25938
|
-
|
|
25939
|
-
|
|
25926
|
+
/** `what` names the value in an error: `workflow "x"'s "steering"`. */
|
|
25927
|
+
const stringRecord = (value, what) => {
|
|
25928
|
+
const record = value ?? {};
|
|
25929
|
+
if (typeof record !== "object" || record === null || Object.values(record).some((entry) => typeof entry !== "string")) throw new Error(`${what} is not a record of strings`);
|
|
25930
|
+
return record;
|
|
25940
25931
|
};
|
|
25941
25932
|
const isStringArray = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
|
|
25942
|
-
const checkSkillsRecord = (value) => {
|
|
25943
|
-
if (typeof value !== "object" || value === null || Object.values(value).some((entry) => !isStringArray(entry))) throw new Error(
|
|
25933
|
+
const checkSkillsRecord = (value, what) => {
|
|
25934
|
+
if (typeof value !== "object" || value === null || Object.values(value).some((entry) => !isStringArray(entry))) throw new Error(`${what} is not a record of skill-name arrays`);
|
|
25944
25935
|
return value;
|
|
25945
25936
|
};
|
|
25946
|
-
/**
|
|
25947
|
-
const
|
|
25948
|
-
|
|
25949
|
-
|
|
25950
|
-
const record = checkSkillsRecord(value);
|
|
25937
|
+
/** A workflow's `skills` option — a scope full name -> skill list record, or a function of the vars returning one. */
|
|
25938
|
+
const skillsFrom = (value, what) => {
|
|
25939
|
+
if (typeof value === "function") return (vars) => checkSkillsRecord(value(vars), what);
|
|
25940
|
+
const record = checkSkillsRecord(value ?? {}, what);
|
|
25951
25941
|
return () => record;
|
|
25952
25942
|
};
|
|
25953
|
-
|
|
25954
|
-
|
|
25955
|
-
const
|
|
25943
|
+
const checkAccessRecord = (value, what) => {
|
|
25944
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${what} is not a record of scope access (scope -> { read?, write? }) or a function returning one`);
|
|
25945
|
+
for (const [scope, access] of Object.entries(value)) {
|
|
25946
|
+
const fault = accessShapeFault(access);
|
|
25947
|
+
if (fault !== void 0) throw new Error(`${what}: scope "${scope}": ${fault}`);
|
|
25948
|
+
}
|
|
25949
|
+
return value;
|
|
25950
|
+
};
|
|
25951
|
+
/**
|
|
25952
|
+
* A workflow's `access` option — a scope full name -> `{ read?, write? }`
|
|
25953
|
+
* record, checked now, or a function of the vars returning one, checked when
|
|
25954
|
+
* the process's settings are known.
|
|
25955
|
+
*/
|
|
25956
|
+
const accessFrom = (value, what) => {
|
|
25956
25957
|
if (typeof value === "function") return (vars) => value(vars);
|
|
25957
|
-
|
|
25958
|
+
const record = checkAccessRecord(value ?? {}, what);
|
|
25959
|
+
return () => record;
|
|
25958
25960
|
};
|
|
25959
|
-
const optionalFunction = (
|
|
25960
|
-
const value = exports[name];
|
|
25961
|
+
const optionalFunction = (value, what) => {
|
|
25961
25962
|
if (value === void 0) return void 0;
|
|
25962
|
-
if (typeof value !== "function") throw new Error(
|
|
25963
|
+
if (typeof value !== "function") throw new Error(`${what} is not a function`);
|
|
25963
25964
|
return value;
|
|
25964
25965
|
};
|
|
25965
|
-
const
|
|
25966
|
-
"default",
|
|
25967
|
-
"summary",
|
|
25968
|
-
"base",
|
|
25966
|
+
const RETIRED_EXPORTS = /* @__PURE__ */ new Set([
|
|
25969
25967
|
"defaults",
|
|
25970
25968
|
"envDefaults",
|
|
25971
25969
|
"steering",
|
|
25972
25970
|
"skills",
|
|
25973
25971
|
"access",
|
|
25974
25972
|
"doors",
|
|
25975
|
-
"
|
|
25973
|
+
"summary",
|
|
25974
|
+
"base"
|
|
25975
|
+
]);
|
|
25976
|
+
/** Names that are never a workflow. */
|
|
25977
|
+
const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
|
|
25978
|
+
"default",
|
|
25979
|
+
"config",
|
|
25980
|
+
...RETIRED_EXPORTS
|
|
25976
25981
|
]);
|
|
25977
|
-
|
|
25982
|
+
const retiredExports = (exports, origin) => Object.keys(exports).filter((name) => RETIRED_EXPORTS.has(name)).sort().map((name) => ({
|
|
25983
|
+
severity: "error",
|
|
25984
|
+
path: [name],
|
|
25985
|
+
origin,
|
|
25986
|
+
message: `the "${name}" export is no longer read — pass it to the workflow it configures: \`workflow(flow, { ${name} })\`, or the build a workflow factory returns`
|
|
25987
|
+
}));
|
|
25988
|
+
/** The rules every declared setting meets. */
|
|
25978
25989
|
const checkSettings = (defaults, envDefaults, declares) => {
|
|
25979
25990
|
for (const [kind, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) for (const [name, setting] of Object.entries(record)) {
|
|
25980
25991
|
if (!isSettingName(name)) throw new Error(`${declares(kind)} ${JSON.stringify(name)}, not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
@@ -25983,23 +25994,6 @@ const checkSettings = (defaults, envDefaults, declares) => {
|
|
|
25983
25994
|
const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
|
|
25984
25995
|
if (both.length > 0) throw new Error(`${both.map((name) => `"${name}"`).join(", ")} declared in both "defaults" and "envDefaults" — a setting is a process setting or an environment setting, not both`);
|
|
25985
25996
|
};
|
|
25986
|
-
/** A module's per-file exports: `defaults`, `envDefaults`, `steering`, `skills`, `access`, `summary`, `base`. */
|
|
25987
|
-
const fileFrom = (exports, origin, dependencyFiles) => {
|
|
25988
|
-
const defaults = stringRecord(exports, "defaults");
|
|
25989
|
-
const envDefaults = stringRecord(exports, "envDefaults");
|
|
25990
|
-
checkSettings(defaults, envDefaults, (kind) => `the "${kind}" export declares`);
|
|
25991
|
-
return {
|
|
25992
|
-
defaults,
|
|
25993
|
-
envDefaults,
|
|
25994
|
-
steering: stringRecord(exports, "steering"),
|
|
25995
|
-
skills: skillsExport(exports),
|
|
25996
|
-
access: accessExport(exports),
|
|
25997
|
-
summary: optionalFunction(exports, "summary"),
|
|
25998
|
-
base: optionalFunction(exports, "base"),
|
|
25999
|
-
origin,
|
|
26000
|
-
dependencyFiles
|
|
26001
|
-
};
|
|
26002
|
-
};
|
|
26003
25997
|
const DOOR_NAME_RULE = /^[a-z][a-z0-9-]*$/;
|
|
26004
25998
|
/** Why one door's `args` are malformed, or `undefined` when they are well-formed. */
|
|
26005
25999
|
const argsProblem = (args) => {
|
|
@@ -26023,22 +26017,11 @@ const doorProblem = (name, door) => {
|
|
|
26023
26017
|
const varsOk = vars === void 0 || typeof vars === "function";
|
|
26024
26018
|
return argsProblem(args) ?? (varsOk ? void 0 : `has "vars" that is not a function`);
|
|
26025
26019
|
};
|
|
26026
|
-
/** A module's `doors` export, shape-checked; the workflow names are checked later, against the catalogue. */
|
|
26027
|
-
const doorsFrom = (exports) => {
|
|
26028
|
-
const value = exports["doors"];
|
|
26029
|
-
if (value === void 0) return /* @__PURE__ */ new Map();
|
|
26030
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`the "doors" export is not a record of doors`);
|
|
26031
|
-
for (const [name, door] of Object.entries(value)) {
|
|
26032
|
-
const problem = doorProblem(name, door);
|
|
26033
|
-
if (problem !== void 0) throw new Error(`door "${name}" ${problem}`);
|
|
26034
|
-
}
|
|
26035
|
-
return new Map(Object.entries(value));
|
|
26036
|
-
};
|
|
26037
26020
|
/**
|
|
26038
|
-
* Each exported workflow's own doors, onto the name it is exported under.
|
|
26039
|
-
*
|
|
26021
|
+
* Each exported workflow's own doors, onto the name it is exported under. A
|
|
26022
|
+
* door several workflows build and none is exported under is a clash: no door.
|
|
26040
26023
|
*/
|
|
26041
|
-
const derivedDoors = (catalogue
|
|
26024
|
+
const derivedDoors = (catalogue) => {
|
|
26042
26025
|
const builders = /* @__PURE__ */ new Map();
|
|
26043
26026
|
for (const [workflow, loaded] of catalogue) for (const [name, spec] of Object.entries(loaded.doors ?? {})) {
|
|
26044
26027
|
const door = {
|
|
@@ -26054,34 +26037,31 @@ const derivedDoors = (catalogue, declared) => {
|
|
|
26054
26037
|
for (const [name, built] of builders) {
|
|
26055
26038
|
const owner = ownerOf(name, built);
|
|
26056
26039
|
if (owner !== void 0) doors.set(name, owner);
|
|
26057
|
-
else
|
|
26040
|
+
else clashes.set(name, built.map((door) => door.workflow));
|
|
26058
26041
|
}
|
|
26059
26042
|
return {
|
|
26060
|
-
doors
|
|
26043
|
+
doors,
|
|
26061
26044
|
clashes
|
|
26062
26045
|
};
|
|
26063
26046
|
};
|
|
26064
26047
|
/** The export named after the door owns it, else the sole builder; a clash of several builds no door. */
|
|
26065
26048
|
const ownerOf = (name, built) => built.find((door) => door.workflow === name) ?? (built.length === 1 ? built[0] : void 0);
|
|
26066
|
-
/** Sort the doors and check each names a startable workflow. */
|
|
26067
|
-
const checkDoors = (declared, catalogue) => {
|
|
26068
|
-
const doors = new Map([...declared].sort(([a], [b]) => a.localeCompare(b)));
|
|
26069
|
-
for (const [name, door] of doors) if (!catalogue.has(door.workflow)) throw new Error(`door "${name}" names workflow "${door.workflow}", which is not startable — known: ${[...catalogue.keys()].sort().join(", ")}`);
|
|
26070
|
-
return doors;
|
|
26071
|
-
};
|
|
26072
26049
|
const namespaceOf = (exported) => typeof exported === "object" && exported !== null ? exported : {};
|
|
26073
26050
|
/**
|
|
26074
|
-
* Evaluate `gtd.config.ts`. Every
|
|
26075
|
-
*
|
|
26076
|
-
* per-call module cache (unrelated to the
|
|
26077
|
-
* passing an empty object in has the side
|
|
26078
|
-
* jiti resolved evaluating this module,
|
|
26079
|
-
* pulled in, transitively.
|
|
26051
|
+
* Evaluate `gtd.config.ts`. Every export but the reserved names that went
|
|
26052
|
+
* through `workflow()` or is a factory workflow is a workflow named by its
|
|
26053
|
+
* export. `cache` is jiti's OWN per-call module cache (unrelated to the
|
|
26054
|
+
* instance's `moduleCache: false`) — passing an empty object in has the side
|
|
26055
|
+
* effect of collecting every real file jiti resolved evaluating this module,
|
|
26056
|
+
* module AND every relative import it pulled in, transitively.
|
|
26080
26057
|
*/
|
|
26081
26058
|
const loadWorkflows = (module, shipped) => {
|
|
26082
26059
|
const { exports: evaluated, files } = module.read === void 0 ? evaluateFromDisk(module, shipped) : evaluateRead(module, module.read, shipped);
|
|
26083
26060
|
const exports = namespaceOf(evaluated);
|
|
26084
|
-
const file =
|
|
26061
|
+
const file = {
|
|
26062
|
+
origin: module.filepath,
|
|
26063
|
+
dependencyFiles: files
|
|
26064
|
+
};
|
|
26085
26065
|
const exported = Object.entries(exports).filter(([name, value]) => !RESERVED_EXPORTS.has(name) && isWorkflow(value));
|
|
26086
26066
|
const value = defaultExport(exports);
|
|
26087
26067
|
const defaultName = exported.find(([, other]) => other === value)?.[0] ?? "default";
|
|
@@ -26096,27 +26076,40 @@ const loadWorkflows = (module, shipped) => {
|
|
|
26096
26076
|
const derived = rejections.length > 0 ? {
|
|
26097
26077
|
doors: /* @__PURE__ */ new Map(),
|
|
26098
26078
|
clashes: /* @__PURE__ */ new Map()
|
|
26099
|
-
} : derivedDoors(catalogue
|
|
26079
|
+
} : derivedDoors(catalogue);
|
|
26080
|
+
const doors = new Map([...derived.doors].sort(([a], [b]) => a.localeCompare(b)));
|
|
26100
26081
|
return {
|
|
26101
26082
|
file,
|
|
26102
26083
|
catalogue,
|
|
26103
|
-
doors
|
|
26084
|
+
doors,
|
|
26104
26085
|
default: defaultWorkflow && {
|
|
26105
26086
|
...defaultWorkflow,
|
|
26106
26087
|
name: defaultName
|
|
26107
26088
|
},
|
|
26108
26089
|
config: compileConfig(exports["config"], module.filepath),
|
|
26109
|
-
diagnostics: [
|
|
26110
|
-
|
|
26111
|
-
|
|
26112
|
-
|
|
26113
|
-
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26090
|
+
diagnostics: [
|
|
26091
|
+
...retiredExports(exports, file.origin),
|
|
26092
|
+
...nameRejections(rejections),
|
|
26093
|
+
...shadowingDoors(doors, catalogue, file.origin),
|
|
26094
|
+
...[...derived.clashes].map(([name, builders]) => {
|
|
26095
|
+
const names = [...builders].sort().map((b) => JSON.stringify(b));
|
|
26096
|
+
return {
|
|
26097
|
+
severity: "warning",
|
|
26098
|
+
message: `door "${name}" is built by workflows ${names.slice(0, -1).join(", ")} and ${names.at(-1)} and none is exported under that name — rename the export that should own it to "${name}"`,
|
|
26099
|
+
path: ["doors", name],
|
|
26100
|
+
origin: file.origin
|
|
26101
|
+
};
|
|
26102
|
+
})
|
|
26103
|
+
]
|
|
26118
26104
|
};
|
|
26119
26105
|
};
|
|
26106
|
+
/** `gtd start <name>` takes the door, so a door named like a workflow it does not start hides that workflow. */
|
|
26107
|
+
const shadowingDoors = (doors, catalogue, origin) => [...doors].filter(([name, door]) => catalogue.has(name) && door.workflow !== name).map(([name, door]) => ({
|
|
26108
|
+
severity: "warning",
|
|
26109
|
+
message: `door "${name}" starts workflow ${JSON.stringify(door.workflow)} and hides the workflow of the same name from \`gtd start ${name}\` — rename one of them`,
|
|
26110
|
+
path: ["doors", name],
|
|
26111
|
+
origin
|
|
26112
|
+
}));
|
|
26120
26113
|
const evaluateFromDisk = (module, shipped) => {
|
|
26121
26114
|
const cache = {};
|
|
26122
26115
|
return {
|
|
@@ -26160,49 +26153,58 @@ const evaluateRead = (module, read, shipped) => {
|
|
|
26160
26153
|
files
|
|
26161
26154
|
};
|
|
26162
26155
|
};
|
|
26163
|
-
const isWorkflow = (value) =>
|
|
26156
|
+
const isWorkflow = (value) => markedOptions(value) !== void 0 || isFactoryWorkflow(value);
|
|
26164
26157
|
const defaultExport = (exports) => {
|
|
26165
26158
|
const value = exports.default;
|
|
26166
|
-
if (value === void 0) throw new Error("no default export — export the workflow an ordinary start runs
|
|
26167
|
-
if (!
|
|
26159
|
+
if (value === void 0) throw new Error("no default export — export the workflow an ordinary start runs; `gtd guide` says where to get one, or write your own");
|
|
26160
|
+
if (typeof value !== "function" && !isFactoryWorkflow(value)) throw new Error("the default export is not a function");
|
|
26168
26161
|
return value;
|
|
26169
26162
|
};
|
|
26170
|
-
/** One exported workflow: a
|
|
26163
|
+
/** One exported workflow: a factory validated and built, a `workflow()` call's options checked alike. */
|
|
26171
26164
|
const buildWorkflow = (name, value, file, rejections) => {
|
|
26172
26165
|
const result = configure(value);
|
|
26173
|
-
if (result === void 0) return {
|
|
26174
|
-
|
|
26175
|
-
|
|
26176
|
-
|
|
26177
|
-
envDefaults: file.envDefaults
|
|
26178
|
-
};
|
|
26166
|
+
if (result === void 0) return checkedBuild(name, file, {
|
|
26167
|
+
...markedOptions(value),
|
|
26168
|
+
flow: value
|
|
26169
|
+
});
|
|
26179
26170
|
if (!result.ok) {
|
|
26180
26171
|
for (const issue of result.issues) rejections.push(optionDiagnostic(name, issue, file));
|
|
26181
26172
|
return;
|
|
26182
26173
|
}
|
|
26183
|
-
return
|
|
26184
|
-
flow: result.build.flow,
|
|
26185
|
-
file,
|
|
26186
|
-
...builtSettings(name, file, result.build),
|
|
26187
|
-
doors: result.build.doors,
|
|
26188
|
-
base: result.build.base
|
|
26189
|
-
};
|
|
26174
|
+
return checkedBuild(name, file, result.build);
|
|
26190
26175
|
};
|
|
26191
|
-
|
|
26192
|
-
|
|
26193
|
-
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26200
|
-
|
|
26201
|
-
|
|
26202
|
-
|
|
26176
|
+
const BUILD_KEYS = /* @__PURE__ */ new Set([
|
|
26177
|
+
"flow",
|
|
26178
|
+
"defaults",
|
|
26179
|
+
"envDefaults",
|
|
26180
|
+
"doors",
|
|
26181
|
+
"base",
|
|
26182
|
+
"skills",
|
|
26183
|
+
"access",
|
|
26184
|
+
"steering",
|
|
26185
|
+
"summary"
|
|
26186
|
+
]);
|
|
26187
|
+
/** A build — a factory's, or a `workflow()` call's options with its flow — held to one set of rules. */
|
|
26188
|
+
const checkedBuild = (name, file, build) => {
|
|
26189
|
+
const what = (key) => `workflow "${name}"'s "${key}"`;
|
|
26190
|
+
const unknown = Object.keys(build).filter((key) => !BUILD_KEYS.has(key));
|
|
26191
|
+
if (unknown.length > 0) throw new Error(`workflow "${name}" has unknown option(s) ${unknown.map((key) => JSON.stringify(key)).join(", ")} — known: ${[...BUILD_KEYS].filter((key) => key !== "flow").join(", ")}`);
|
|
26192
|
+
if (typeof build.flow !== "function") throw new Error(`${what("flow")} is not a function`);
|
|
26193
|
+
const defaults = stringRecord(build.defaults, what("defaults"));
|
|
26194
|
+
const envDefaults = stringRecord(build.envDefaults, what("envDefaults"));
|
|
26195
|
+
checkSettings(defaults, envDefaults, (kind) => `workflow "${name}" declares ${kind}`);
|
|
26196
|
+
if (build.doors !== void 0 && (typeof build.doors !== "object" || build.doors === null || Array.isArray(build.doors))) throw new Error(`${what("doors")} is not a record of doors`);
|
|
26203
26197
|
return {
|
|
26198
|
+
flow: build.flow,
|
|
26199
|
+
file,
|
|
26204
26200
|
defaults,
|
|
26205
|
-
envDefaults
|
|
26201
|
+
envDefaults,
|
|
26202
|
+
summary: optionalFunction(build.summary, what("summary")),
|
|
26203
|
+
steering: stringRecord(build.steering, what("steering")),
|
|
26204
|
+
skills: skillsFrom(build.skills, what("skills")),
|
|
26205
|
+
access: accessFrom(build.access, what("access")),
|
|
26206
|
+
doors: build.doors,
|
|
26207
|
+
base: optionalFunction(build.base, what("base"))
|
|
26206
26208
|
};
|
|
26207
26209
|
};
|
|
26208
26210
|
/** A rejected factory option, reported at `gtd.config.ts`. */
|
|
@@ -26300,7 +26302,7 @@ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$2(promise(() => r
|
|
|
26300
26302
|
})), (outcome) => {
|
|
26301
26303
|
if (outcome.kind === "rest") return succeed$3(outcome.rest.name);
|
|
26302
26304
|
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}`));
|
|
26305
|
+
return fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: ${problem}`));
|
|
26304
26306
|
});
|
|
26305
26307
|
/**
|
|
26306
26308
|
* The default entry's first step — where a finished process waits — found the
|
|
@@ -26316,20 +26318,20 @@ const firstStep = (loaded, head) => gen(function* () {
|
|
|
26316
26318
|
const env = watchedEmptySettings();
|
|
26317
26319
|
const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
|
|
26318
26320
|
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`));
|
|
26321
|
+
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
26322
|
if (outcome._tag === "Left") return yield* fail$3(outcome.left);
|
|
26321
26323
|
const name = outcome.right;
|
|
26322
26324
|
if (!empty.read()) return name;
|
|
26323
26325
|
const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
|
|
26324
26326
|
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`));
|
|
26327
|
+
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
26328
|
});
|
|
26327
26329
|
var ConfigService = class ConfigService extends Tag("ConfigService")() {
|
|
26328
26330
|
static Live = succeed$2(ConfigService, { load });
|
|
26329
26331
|
};
|
|
26330
26332
|
//#endregion
|
|
26331
26333
|
//#region src/workflow/skills.ts
|
|
26332
|
-
const fail = (origin, message) => new GtdError(`gtd config:\n - ${origin}: ${message}`);
|
|
26334
|
+
const fail = (origin, message) => new GtdError(`gtd: config:\n - ${origin}: ${message}`);
|
|
26333
26335
|
const checkShape = (value) => {
|
|
26334
26336
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("skills export must return an object of scope -> skill names");
|
|
26335
26337
|
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`);
|
|
@@ -26518,10 +26520,10 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
|
|
|
26518
26520
|
const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
|
|
26519
26521
|
if (environmentOnly.length > 0) return yield* fail$3(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it as a workflow option in gtd.config.ts or with a GTD_<NAME> environment variable`));
|
|
26520
26522
|
const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
|
|
26521
|
-
if (undeclared.length > 0) return
|
|
26523
|
+
if (undeclared.length > 0) return yield* fail$3(new GtdUsageError(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`));
|
|
26522
26524
|
});
|
|
26523
26525
|
/**
|
|
26524
|
-
* `gtd
|
|
26526
|
+
* `gtd start <name>`: start a brand NEW process on a workflow, writing an
|
|
26525
26527
|
* opening commit that names the workflow's first step and carries a
|
|
26526
26528
|
* `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
|
|
26527
26529
|
* a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
|
|
@@ -26534,8 +26536,7 @@ const planStart = (current, actor, start) => gen(function* () {
|
|
|
26534
26536
|
if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before starting another`);
|
|
26535
26537
|
if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`);
|
|
26536
26538
|
const { firstStep } = first;
|
|
26537
|
-
|
|
26538
|
-
if (overrideRefusal !== void 0) return overrideRefusal;
|
|
26539
|
+
yield* checkOverrides(commandLabel, varOverrides, Object.keys(workflow.vars), Object.keys(workflow.env));
|
|
26539
26540
|
const baseOf = workflow.def.base;
|
|
26540
26541
|
const flags = yield* StartFlags;
|
|
26541
26542
|
const hostEnv = (yield* Host).env;
|
|
@@ -26609,7 +26610,7 @@ const ACTORS = /* @__PURE__ */ new Set([
|
|
|
26609
26610
|
/**
|
|
26610
26611
|
* Where the episode HEAD belongs to begins, read off subjects alone. Walking
|
|
26611
26612
|
* back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
|
|
26612
|
-
* `gtd
|
|
26613
|
+
* `gtd start` writes — no landing ever produces one — and opens an
|
|
26613
26614
|
* episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
|
|
26614
26615
|
* a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
|
|
26615
26616
|
* entering the flow's first step (a finished episode), bounds the episode
|
|
@@ -27113,7 +27114,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
27113
27114
|
const step = outcome.rest;
|
|
27114
27115
|
yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
|
|
27115
27116
|
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(", ")})`));
|
|
27117
|
+
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
27118
|
const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
|
|
27118
27119
|
status: normalizeStatus(e.status),
|
|
27119
27120
|
path: e.path
|
|
@@ -27143,7 +27144,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
27143
27144
|
});
|
|
27144
27145
|
const currentRest = restAt(void 0);
|
|
27145
27146
|
/**
|
|
27146
|
-
* Whether `gtd
|
|
27147
|
+
* Whether `gtd start <name>` can start a process here: the workflow must
|
|
27147
27148
|
* reach a step from the tree the opening commit would capture. That step is
|
|
27148
27149
|
* what the opening commit's subject names.
|
|
27149
27150
|
*/
|
|
@@ -27665,7 +27666,7 @@ const emitScripts = (required = [], optional = []) => ({
|
|
|
27665
27666
|
optional: assembleScript(optional)
|
|
27666
27667
|
});
|
|
27667
27668
|
/** 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 —
|
|
27669
|
+
const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
|
|
27669
27670
|
const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
|
|
27670
27671
|
const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
|
|
27671
27672
|
/**
|
|
@@ -27960,6 +27961,21 @@ const writeJson = (json, out, document) => gen(function* () {
|
|
|
27960
27961
|
else yield* writeSelection(out, document, json.path);
|
|
27961
27962
|
return true;
|
|
27962
27963
|
});
|
|
27964
|
+
/** Exactly one trailing newline: a script embedded in a `--json` document is never re-normalized at its tail. */
|
|
27965
|
+
const normalizeScriptNewline = (script) => script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`;
|
|
27966
|
+
/**
|
|
27967
|
+
* A script-printing command's output: the script itself (or `plain`, when
|
|
27968
|
+
* there is none to run), `{ schema, script }` under `--json`, or one field.
|
|
27969
|
+
*/
|
|
27970
|
+
const writeScript = (json, out, script, plain = script) => gen(function* () {
|
|
27971
|
+
if (json.kind === "off") {
|
|
27972
|
+
out.write(plain);
|
|
27973
|
+
return;
|
|
27974
|
+
}
|
|
27975
|
+
const fields = scriptFields(normalizeScriptNewline(script));
|
|
27976
|
+
if (json.kind === "document") out.write(renderScriptJson(fields));
|
|
27977
|
+
else yield* writeSelection(out, fields, json.path);
|
|
27978
|
+
});
|
|
27963
27979
|
//#endregion
|
|
27964
27980
|
//#region src/cli/Strays.ts
|
|
27965
27981
|
const resolveSide = (git, side) => git.resolveRef(`${side}^{commit}`).pipe(mapError$1(() => new GtdError(`gtd strays: unknown revision '${side}'`)));
|
|
@@ -28054,9 +28070,7 @@ const runHistoryShowCommand = (ref, json, out) => gen(function* () {
|
|
|
28054
28070
|
out.write(`${lines.join("\n")}\n`);
|
|
28055
28071
|
});
|
|
28056
28072
|
/** Prints the script and never runs git: gtd's promise that it only decides and prints. */
|
|
28057
|
-
const runHistoryFetchCommand = (remote, out) =>
|
|
28058
|
-
out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`);
|
|
28059
|
-
});
|
|
28073
|
+
const runHistoryFetchCommand = (remote, json, out) => writeScript(json, out, `${combinedScript(fetchHistoryRefs(remote), "")}\n`);
|
|
28060
28074
|
//#endregion
|
|
28061
28075
|
//#region src/cli/Metrics.ts
|
|
28062
28076
|
const QUALITY$1 = ".gtd/QUALITY.md";
|
|
@@ -28639,7 +28653,7 @@ const runLspCommand = () => flatMap$2(ToolCommands, (tools) => tools.lsp(selfGtd
|
|
|
28639
28653
|
* gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
|
|
28640
28654
|
* get one; it writes nothing.
|
|
28641
28655
|
*/
|
|
28642
|
-
const
|
|
28656
|
+
const runGuideCommand = (out) => gen(function* () {
|
|
28643
28657
|
const briefing = renderBriefing();
|
|
28644
28658
|
out.write(briefing.endsWith("\n") ? briefing : briefing + "\n");
|
|
28645
28659
|
const host = yield* Host;
|
|
@@ -28647,20 +28661,12 @@ const runInstallCommand = (out) => gen(function* () {
|
|
|
28647
28661
|
if (topLevel._tag === "Left") return;
|
|
28648
28662
|
if ((yield* host.realPath(topLevel.right)) !== (yield* host.realPath(host.root))) return;
|
|
28649
28663
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== void 0) return;
|
|
28650
|
-
yield* (yield* Narrator).warn(`gtd
|
|
28664
|
+
yield* (yield* Narrator).warn(`gtd guide: no ${WORKFLOW_MODULE} governs here, and gtd ships no workflow — get one from ${WORKFLOWS_REPO} or write your own (step 2 of the briefing above)`);
|
|
28651
28665
|
});
|
|
28652
28666
|
/**
|
|
28653
|
-
* Exactly one trailing newline — duplicated from `Cli.ts`'s own
|
|
28654
|
-
* `normalizeTrailingNewline` rather than imported, to avoid a circular value
|
|
28655
|
-
* dependency (`Cli.ts` already depends on this module). `LandResult.script`
|
|
28656
|
-
* must carry its own trailing newline because it's embedded verbatim inside
|
|
28657
|
-
* the `--json` document, never re-normalized at its tail.
|
|
28658
|
-
*/
|
|
28659
|
-
const normalizeScriptNewline = (script) => script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`;
|
|
28660
|
-
/**
|
|
28661
28667
|
* `combinedScript`, narrowed to accept only an already-guarded
|
|
28662
28668
|
* `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
|
|
28663
|
-
* exists to protect: `gtd land`/`gtd
|
|
28669
|
+
* exists to protect: `gtd land`/`gtd start`'s emitted script. Every OTHER
|
|
28664
28670
|
* `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
|
|
28665
28671
|
* from `emitScripts` directly and keeps the unbranded signature — those
|
|
28666
28672
|
* paths land no guard-gated git write for a brand to protect.
|
|
@@ -28754,12 +28760,24 @@ const readStdin = () => tryPromise({
|
|
|
28754
28760
|
},
|
|
28755
28761
|
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
28756
28762
|
});
|
|
28763
|
+
/** A stdin that is no judgment is the caller's mistake, a usage error like `gtd judge answer`'s. */
|
|
28764
|
+
const judgmentShape = (input) => {
|
|
28765
|
+
let doc;
|
|
28766
|
+
try {
|
|
28767
|
+
doc = JSON.parse(input);
|
|
28768
|
+
} catch {
|
|
28769
|
+
return fail$3(new GtdUsageError("gtd judge run: stdin is not valid JSON"));
|
|
28770
|
+
}
|
|
28771
|
+
const questions = doc?.questions;
|
|
28772
|
+
return Array.isArray(questions) ? _void : fail$3(new GtdUsageError("gtd judge run: stdin has no \"questions\" array"));
|
|
28773
|
+
};
|
|
28757
28774
|
/** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
|
|
28758
28775
|
const runJudgeRunCommand = (command, out) => gen(function* () {
|
|
28759
28776
|
const host = yield* Host;
|
|
28760
28777
|
const tools = yield* ToolCommands;
|
|
28761
28778
|
yield* refuseLegacyRc;
|
|
28762
28779
|
const input = yield* readStdin();
|
|
28780
|
+
yield* judgmentShape(input);
|
|
28763
28781
|
const verdict = yield* tryPromise({
|
|
28764
28782
|
try: () => tools.judge({
|
|
28765
28783
|
provider: command.provider,
|
|
@@ -28832,16 +28850,19 @@ const runLandCommand = (opts, json, out) => gen(function* () {
|
|
|
28832
28850
|
* `base` renders to something that is not an ancestor of (and differs from)
|
|
28833
28851
|
* HEAD.
|
|
28834
28852
|
*/
|
|
28835
|
-
const
|
|
28853
|
+
const startWorkflow = (workflow, varOverrides, json, out, commandLabel) => gen(function* () {
|
|
28836
28854
|
const config = yield* (yield* ConfigService).load;
|
|
28837
28855
|
const resolved = config.workflowNamed(workflow);
|
|
28838
|
-
if (resolved === void 0)
|
|
28856
|
+
if (resolved === void 0) {
|
|
28857
|
+
const startable = config.workflowNames.length > 0 ? `startable: ${config.workflowNames.join(", ")}` : "this config exports no workflow besides its default, which an ordinary start runs";
|
|
28858
|
+
return yield* fail$3(new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`));
|
|
28859
|
+
}
|
|
28839
28860
|
const rest = yield* restAt(void 0, varOverrides);
|
|
28840
28861
|
const started = yield* startRefusal(rest, resolved, varOverrides);
|
|
28841
28862
|
const plan = yield* planStart({
|
|
28842
28863
|
state: rest.state,
|
|
28843
28864
|
idle: noProcessUnderway(rest)
|
|
28844
|
-
},
|
|
28865
|
+
}, "human", {
|
|
28845
28866
|
workflow: resolved,
|
|
28846
28867
|
first: started,
|
|
28847
28868
|
commandLabel,
|
|
@@ -28849,51 +28870,79 @@ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => ge
|
|
|
28849
28870
|
config: rest.config
|
|
28850
28871
|
});
|
|
28851
28872
|
if (plan.kind === "refusal") return yield* fail$3(new Error(plan.message));
|
|
28852
|
-
out
|
|
28873
|
+
yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
|
|
28853
28874
|
});
|
|
28854
|
-
const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
|
|
28855
28875
|
/**
|
|
28856
|
-
*
|
|
28857
|
-
*
|
|
28858
|
-
*
|
|
28876
|
+
* A start commits the whole tree (`git add -A`), so its script says up front
|
|
28877
|
+
* which paths that is. Comment lines only: control characters are masked so a
|
|
28878
|
+
* path can never end the comment and become a command.
|
|
28859
28879
|
*/
|
|
28860
|
-
const
|
|
28861
|
-
|
|
28862
|
-
const
|
|
28863
|
-
|
|
28864
|
-
|
|
28865
|
-
|
|
28866
|
-
|
|
28880
|
+
const listingCommitted = (script, changes) => {
|
|
28881
|
+
if (changes.length === 0) return script;
|
|
28882
|
+
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");
|
|
28883
|
+
const at = script.indexOf("\n\n");
|
|
28884
|
+
return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`;
|
|
28885
|
+
};
|
|
28886
|
+
const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
|
|
28887
|
+
/** A door's start: its args mapped to process settings, `extra` (`--var`) pinned on top. */
|
|
28888
|
+
const startThroughDoor = (name, door, args, extra, json, out) => gen(function* () {
|
|
28889
|
+
const label = `gtd start ${name}`;
|
|
28867
28890
|
const declared = door.args ?? [];
|
|
28868
28891
|
const required = declared.filter((arg) => arg.optional !== true).length;
|
|
28869
|
-
if (args.length < required || args.length > declared.length) return yield* fail$3(new GtdUsageError(
|
|
28892
|
+
if (args.length < required || args.length > declared.length) return yield* fail$3(new GtdUsageError(`${label}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd start ${doorSynopsis(name, door)}`));
|
|
28870
28893
|
const named = {};
|
|
28871
28894
|
declared.forEach((arg, i) => {
|
|
28872
28895
|
named[arg.name] = args[i];
|
|
28873
28896
|
});
|
|
28874
28897
|
const vars = yield* try_({
|
|
28875
28898
|
try: () => door.vars?.(named) ?? {},
|
|
28876
|
-
catch: (e) => /* @__PURE__ */ new Error(
|
|
28899
|
+
catch: (e) => /* @__PURE__ */ new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`)
|
|
28877
28900
|
});
|
|
28878
|
-
yield*
|
|
28901
|
+
yield* startWorkflow(door.workflow, {
|
|
28902
|
+
...vars,
|
|
28903
|
+
...extra
|
|
28904
|
+
}, json, out, label);
|
|
28879
28905
|
});
|
|
28880
|
-
/**
|
|
28881
|
-
|
|
28882
|
-
|
|
28883
|
-
|
|
28884
|
-
|
|
28885
|
-
|
|
28886
|
-
}
|
|
28887
|
-
|
|
28888
|
-
|
|
28889
|
-
|
|
28890
|
-
|
|
28906
|
+
/**
|
|
28907
|
+
* `gtd start <name> [args…]`: a door when `<name>` is one — so a door shadows
|
|
28908
|
+
* the workflow it shares a name with — else a workflow, which takes no args.
|
|
28909
|
+
* No name lists both.
|
|
28910
|
+
*/
|
|
28911
|
+
const runStartCommand = (command, json, out) => gen(function* () {
|
|
28912
|
+
const { name, args, vars } = command;
|
|
28913
|
+
if (name === void 0) return yield* runStartListCommand(json, out);
|
|
28914
|
+
const label = `gtd start ${name}`;
|
|
28915
|
+
const config = yield* (yield* ConfigService).load;
|
|
28916
|
+
const door = config.doors.get(name);
|
|
28917
|
+
if (door !== void 0) return yield* startThroughDoor(name, door, args, vars, json, out);
|
|
28918
|
+
if (config.workflowNamed(name) === void 0) return yield* fail$3(new GtdUsageError(`${label}: no workflow or door is named "${name}" — run \`gtd start\` to list them`));
|
|
28919
|
+
if (args.length > 0) return yield* fail$3(new GtdUsageError(`${label}: too many arguments — a workflow takes none, only --var <name>=<value>; got: ${args.join(", ")}`));
|
|
28920
|
+
yield* startWorkflow(name, { ...vars }, json, out, label);
|
|
28921
|
+
});
|
|
28922
|
+
/**
|
|
28923
|
+
* Bare `gtd start`: every workflow startable by name, then every door — plain
|
|
28924
|
+
* lines, or one JSON array. A workflow a door shadows is left out: its name
|
|
28925
|
+
* opens the door.
|
|
28926
|
+
*/
|
|
28927
|
+
const runStartListCommand = (json, out) => gen(function* () {
|
|
28928
|
+
const config = yield* (yield* ConfigService).load;
|
|
28929
|
+
const workflows = [...config.workflowNames].filter((name) => !config.doors.has(name)).sort();
|
|
28930
|
+
const doors = [...config.doors];
|
|
28931
|
+
const document = [...workflows.map((name) => ({
|
|
28932
|
+
kind: "workflow",
|
|
28933
|
+
name
|
|
28934
|
+
})), ...doors.map(([name, door]) => ({
|
|
28935
|
+
kind: "door",
|
|
28936
|
+
name,
|
|
28937
|
+
workflow: door.workflow,
|
|
28938
|
+
args: (door.args ?? []).map((arg) => ({
|
|
28891
28939
|
name: arg.name,
|
|
28892
28940
|
optional: arg.optional === true
|
|
28893
28941
|
}))
|
|
28894
|
-
}));
|
|
28895
|
-
if (json
|
|
28896
|
-
|
|
28942
|
+
}))];
|
|
28943
|
+
if (yield* writeJson(json, out, document)) return;
|
|
28944
|
+
if (document.length === 0) yield* (yield* Narrator).warn("gtd start: this config exports no workflow besides its default, which an ordinary start runs, and no door");
|
|
28945
|
+
out.write([...workflows.map((name) => `workflow ${name}\n`), ...doors.map(([n, d]) => `door ${doorSynopsis(n, d)} → ${d.workflow}\n`)].join(""));
|
|
28897
28946
|
});
|
|
28898
28947
|
/**
|
|
28899
28948
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
@@ -28908,7 +28957,7 @@ const runDoorsCommand = (json, out) => gen(function* () {
|
|
|
28908
28957
|
* Idempotent at the initial state. The one refusal is a process whose first
|
|
28909
28958
|
* commit is the repository's root commit: nothing earlier to rewind to.
|
|
28910
28959
|
*/
|
|
28911
|
-
const runAbandonCommand = (out) => gen(function* () {
|
|
28960
|
+
const runAbandonCommand = (json, out) => gen(function* () {
|
|
28912
28961
|
const git = yield* GitService;
|
|
28913
28962
|
const initial = (yield* (yield* ConfigService).load).workflow.initial;
|
|
28914
28963
|
const run = yield* currentRun;
|
|
@@ -28917,8 +28966,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28917
28966
|
kind: "outcome",
|
|
28918
28967
|
command: abandonNoopOutcome(initial)
|
|
28919
28968
|
}]).required;
|
|
28920
|
-
out
|
|
28921
|
-
return;
|
|
28969
|
+
return yield* writeScript(json, out, combinedScript(required, ""));
|
|
28922
28970
|
}
|
|
28923
28971
|
const restState = run.trace[run.trace.length - 1].state;
|
|
28924
28972
|
if (run.startParentHash === EMPTY_TREE) return yield* fail$3(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
|
|
@@ -28936,7 +28984,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28936
28984
|
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
28937
28985
|
}
|
|
28938
28986
|
]).required;
|
|
28939
|
-
out
|
|
28987
|
+
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28940
28988
|
});
|
|
28941
28989
|
/**
|
|
28942
28990
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
@@ -28945,7 +28993,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28945
28993
|
* refuses on a dirty working tree, no retained history, or HEAD having
|
|
28946
28994
|
* advanced past the retained tip with commits that would be lost.
|
|
28947
28995
|
*/
|
|
28948
|
-
const runRestoreCommand = (out) => gen(function* () {
|
|
28996
|
+
const runRestoreCommand = (json, out) => gen(function* () {
|
|
28949
28997
|
const git = yield* GitService;
|
|
28950
28998
|
if ((yield* currentRest).changes.length > 0) return yield* fail$3(/* @__PURE__ */ new Error("gtd restore: refuses on a dirty working tree — commit, stash, or discard your changes first."));
|
|
28951
28999
|
const retained = yield* readRetainedHistory(git);
|
|
@@ -28969,7 +29017,7 @@ const runRestoreCommand = (out) => gen(function* () {
|
|
|
28969
29017
|
command: restoredOutcome(tip, after.state)
|
|
28970
29018
|
}
|
|
28971
29019
|
]).required;
|
|
28972
|
-
out
|
|
29020
|
+
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28973
29021
|
});
|
|
28974
29022
|
/**
|
|
28975
29023
|
* The mode's own validate command for `file`: a declared `validate:` command
|
|
@@ -29028,26 +29076,35 @@ const runNextCommand = (json, out) => gen(function* () {
|
|
|
29028
29076
|
else if (json.kind === "select") yield* writeSelection(out, fields, json.path);
|
|
29029
29077
|
else {
|
|
29030
29078
|
const validateCommand = emitsValidatablePrompt(rendered) ? selfValidateCommand(rest.def, rest.state, rendered.mode, rendered.file) : void 0;
|
|
29079
|
+
if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
|
|
29080
|
+
const shown = renderBeatPlain({
|
|
29081
|
+
...fields,
|
|
29082
|
+
kind: "message"
|
|
29083
|
+
});
|
|
29084
|
+
yield* narrator.warn(shown.slice(0, shown.indexOf("\n\n")));
|
|
29085
|
+
}
|
|
29031
29086
|
out.write(renderBeatPlain(fields, validateCommand));
|
|
29032
29087
|
}
|
|
29033
29088
|
});
|
|
29034
29089
|
/** Emits the format-then-validate script for the driver to run; reads no file and executes nothing itself. The verdict lives in that script's future exit code, which prints the complete fix prompt rather than bare findings. */
|
|
29035
|
-
const runValidateCommand = (out) => gen(function* () {
|
|
29090
|
+
const runValidateCommand = (json, out) => gen(function* () {
|
|
29036
29091
|
const rest = yield* currentRest;
|
|
29037
|
-
const
|
|
29038
|
-
|
|
29092
|
+
const script = (yield* resolveValidateScript(rest))?.script ?? "";
|
|
29093
|
+
yield* writeScript(json, out, script, script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
29039
29094
|
});
|
|
29040
29095
|
/**
|
|
29041
29096
|
* Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
|
|
29042
29097
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
29043
29098
|
* `validate:` command invokes as a leaf step, and it formats nothing.
|
|
29044
29099
|
*
|
|
29045
|
-
* An
|
|
29046
|
-
*
|
|
29100
|
+
* An unknown mode is a usage error; an absent file is a refusal (the emitted
|
|
29101
|
+
* validate script guards on the file's presence before it calls this).
|
|
29102
|
+
* Findings ride the failing Effect's message, so the shared refusal path
|
|
29103
|
+
* reports them on stderr.
|
|
29047
29104
|
*/
|
|
29048
29105
|
const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(function* () {
|
|
29049
29106
|
const format = steeringFormatFor(mode);
|
|
29050
|
-
if (format === void 0) return yield* fail$3(
|
|
29107
|
+
if (format === void 0) return yield* fail$3(new GtdUsageError(`gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`));
|
|
29051
29108
|
if (openThreadsFlag) {
|
|
29052
29109
|
if (openQuestions) return yield* fail$3(/* @__PURE__ */ new Error("gtd check: --open-threads and --open-questions are mutually exclusive"));
|
|
29053
29110
|
if (format.openThreads === void 0) {
|
|
@@ -29060,11 +29117,15 @@ const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(func
|
|
|
29060
29117
|
if (mode !== "qa") return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: --open-questions only applies to mode "qa" — got "${mode}"`));
|
|
29061
29118
|
return yield* runOpenQuestionsCheckCommand(format, file);
|
|
29062
29119
|
}
|
|
29063
|
-
const content =
|
|
29064
|
-
const errors =
|
|
29120
|
+
const content = yield* existingFile(file);
|
|
29121
|
+
const errors = format.validate(content).map((finding) => formatFinding(file, finding));
|
|
29065
29122
|
if (errors.length === 0) return;
|
|
29066
29123
|
return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s)):\n` + errors.join("\n")));
|
|
29067
29124
|
});
|
|
29125
|
+
const existingFile = (file) => flatMap$2(Workspace, (workspace) => {
|
|
29126
|
+
const content = workspace.atPath(file);
|
|
29127
|
+
return content === void 0 ? fail$3(/* @__PURE__ */ new Error(`gtd check: no such file: ${file}`)) : succeed$3(content);
|
|
29128
|
+
});
|
|
29068
29129
|
/**
|
|
29069
29130
|
* The bare `gtd check --open-threads`: code-comment threads in the files the
|
|
29070
29131
|
* current process changed. The one `check` form that reads process state; no
|
|
@@ -29082,23 +29143,18 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
|
|
|
29082
29143
|
if (lines.length === 0) return;
|
|
29083
29144
|
return yield* fail$3(new Error(lines.join("\n")));
|
|
29084
29145
|
});
|
|
29085
|
-
/** Prints one `file:line: [^name]: <first H entry>` line per open thread
|
|
29146
|
+
/** Prints one `file:line: [^name]: <first H entry>` line per open thread. */
|
|
29086
29147
|
const runOpenThreadsCheckCommand = (format, file) => gen(function* () {
|
|
29087
|
-
const
|
|
29088
|
-
if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
|
|
29089
|
-
const errors = openThreads(format, content).map((t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`);
|
|
29148
|
+
const errors = openThreads(format, yield* existingFile(file)).map((t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`);
|
|
29090
29149
|
if (errors.length === 0) return;
|
|
29091
29150
|
return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open thread(s) in ${file}:\n` + errors.join("\n")));
|
|
29092
29151
|
});
|
|
29093
29152
|
/**
|
|
29094
29153
|
* Runs the SAME `unansweredQuestions` predicate a flow's `openQuestions()`
|
|
29095
|
-
* reads, so the gate script and the flow's own check stay in sync.
|
|
29096
|
-
* file is a non-zero exit here, unlike the structural path above.
|
|
29154
|
+
* reads, so the gate script and the flow's own check stay in sync.
|
|
29097
29155
|
*/
|
|
29098
29156
|
const runOpenQuestionsCheckCommand = (format, file) => gen(function* () {
|
|
29099
|
-
const
|
|
29100
|
-
if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
|
|
29101
|
-
const errors = unansweredQuestions(format, content).map((q) => `${file}:${q.headingLine + 1}: ${q.question}`);
|
|
29157
|
+
const errors = unansweredQuestions(format, yield* existingFile(file)).map((q) => `${file}:${q.headingLine + 1}: ${q.question}`);
|
|
29102
29158
|
if (errors.length === 0) return;
|
|
29103
29159
|
return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open question(s) unanswered in ${file}:\n` + errors.join("\n")));
|
|
29104
29160
|
});
|
|
@@ -29183,12 +29239,13 @@ const runUiCliCommand = (command, out) => gen(function* () {
|
|
|
29183
29239
|
* cwds (e.g. macOS /tmp → /private/tmp) match.
|
|
29184
29240
|
*/
|
|
29185
29241
|
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
|
|
29242
|
+
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));
|
|
29243
|
+
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
29244
|
});
|
|
29245
|
+
const shellWord = (path) => /^[\w@%+=:,./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`;
|
|
29189
29246
|
/** A command that derives workflow state needs at least one commit too — a commitless repository has no HEAD to resolve a `Rest` at. */
|
|
29190
29247
|
const assertRepositoryHasCommits = (git) => gen(function* () {
|
|
29191
|
-
if (!(yield* git.hasCommits())) return yield* fail$3(/* @__PURE__ */ new Error("gtd
|
|
29248
|
+
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
29249
|
});
|
|
29193
29250
|
/**
|
|
29194
29251
|
* What a command kind needs before it may run. Defined here, not in
|
|
@@ -29200,7 +29257,7 @@ const assertRepositoryHasCommits = (git) => gen(function* () {
|
|
|
29200
29257
|
const needsOf = (kind) => {
|
|
29201
29258
|
switch (kind) {
|
|
29202
29259
|
case "lsp":
|
|
29203
|
-
case "
|
|
29260
|
+
case "guide":
|
|
29204
29261
|
case "judgeRun":
|
|
29205
29262
|
case "strays": return "none";
|
|
29206
29263
|
case "check":
|
|
@@ -29226,18 +29283,16 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
29226
29283
|
...command.cost !== void 0 ? { cost: command.cost } : {},
|
|
29227
29284
|
...command.model !== void 0 ? { model: command.model } : {}
|
|
29228
29285
|
}, json, out);
|
|
29229
|
-
case "start": return runStartCommand(command
|
|
29230
|
-
case "
|
|
29231
|
-
case "
|
|
29232
|
-
case "abandon": return runAbandonCommand(out);
|
|
29233
|
-
case "restore": return runRestoreCommand(out);
|
|
29286
|
+
case "start": return runStartCommand(command, json, out);
|
|
29287
|
+
case "abandon": return runAbandonCommand(json, out);
|
|
29288
|
+
case "restore": return runRestoreCommand(json, out);
|
|
29234
29289
|
case "next": return runNextCommand(json, out);
|
|
29235
|
-
case "validate": return runValidateCommand(out);
|
|
29290
|
+
case "validate": return runValidateCommand(json, out);
|
|
29236
29291
|
case "check":
|
|
29237
29292
|
if (command.mode === void 0 || command.file === void 0) return runCodeThreadsCheckCommand();
|
|
29238
29293
|
return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false);
|
|
29239
29294
|
case "uncheck": return runUncheckCommand(command.file);
|
|
29240
|
-
case "
|
|
29295
|
+
case "guide": return runGuideCommand(out);
|
|
29241
29296
|
case "summary": return runSummaryCommand(out);
|
|
29242
29297
|
case "base": return runBaseCommand(out);
|
|
29243
29298
|
case "judge": return runJudgeCommand(json, out);
|
|
@@ -29247,7 +29302,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
29247
29302
|
case "historyShow": return runHistoryShowCommand(command.ref, json, out);
|
|
29248
29303
|
case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
|
|
29249
29304
|
case "historyJudges": return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out);
|
|
29250
|
-
case "historyFetch": return runHistoryFetchCommand(command.remote, out);
|
|
29305
|
+
case "historyFetch": return runHistoryFetchCommand(command.remote, json, out);
|
|
29251
29306
|
}
|
|
29252
29307
|
};
|
|
29253
29308
|
/**
|
|
@@ -29275,18 +29330,99 @@ const runCommand$1 = (command, json, out) => {
|
|
|
29275
29330
|
});
|
|
29276
29331
|
};
|
|
29277
29332
|
//#endregion
|
|
29333
|
+
//#region src/cli/Help.ts
|
|
29334
|
+
const HELP_WIDTH = 80;
|
|
29335
|
+
/**
|
|
29336
|
+
* Word-wraps prose to `width` columns, every line prefixed by `indent`.
|
|
29337
|
+
* Paragraphs are separated by a blank line; a paragraph whose lines start
|
|
29338
|
+
* with two spaces is an example and is kept verbatim (indented, not wrapped).
|
|
29339
|
+
*/
|
|
29340
|
+
const wrapParagraph = (paragraph, width, indent) => {
|
|
29341
|
+
const lines = [];
|
|
29342
|
+
let line = "";
|
|
29343
|
+
for (const word of paragraph.match(/\S*`[^`]*`\S*|\S+/g) ?? []) if (line !== "" && indent.length + line.length + 1 + word.length > width) {
|
|
29344
|
+
lines.push(indent + line);
|
|
29345
|
+
line = word;
|
|
29346
|
+
} else line = line === "" ? word : `${line} ${word}`;
|
|
29347
|
+
if (line !== "") lines.push(indent + line);
|
|
29348
|
+
return lines;
|
|
29349
|
+
};
|
|
29350
|
+
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)]);
|
|
29351
|
+
/** `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. */
|
|
29352
|
+
const definition = (term, description, column) => {
|
|
29353
|
+
const cell = ` ${term}`;
|
|
29354
|
+
const body = wrap(description, HELP_WIDTH, " ".repeat(column));
|
|
29355
|
+
if (cell.length + 1 >= column) return [cell, ...body];
|
|
29356
|
+
const [first = "", ...rest] = body;
|
|
29357
|
+
return [cell.padEnd(column) + first.trimStart(), ...rest];
|
|
29358
|
+
};
|
|
29359
|
+
/** Optimal-string-alignment distance: Levenshtein plus adjacent transpositions, so `nxet` is one edit from `next`. */
|
|
29360
|
+
const distance = (a, b) => {
|
|
29361
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
29362
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
|
|
29363
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
29364
|
+
let best = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
29365
|
+
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);
|
|
29366
|
+
d[i][j] = best;
|
|
29367
|
+
}
|
|
29368
|
+
return d[a.length][b.length];
|
|
29369
|
+
};
|
|
29370
|
+
/** The candidate closest to `word`, when it is a plausible typo of it: at most one edit per three characters, and at least one. */
|
|
29371
|
+
const closest = (word, candidates) => {
|
|
29372
|
+
const limit = Math.max(1, Math.floor(word.length / 3));
|
|
29373
|
+
let best;
|
|
29374
|
+
for (const name of candidates) {
|
|
29375
|
+
const d = distance(word, name);
|
|
29376
|
+
if (d <= limit && (best === void 0 || d < best.d)) best = {
|
|
29377
|
+
name,
|
|
29378
|
+
d
|
|
29379
|
+
};
|
|
29380
|
+
}
|
|
29381
|
+
return best?.name;
|
|
29382
|
+
};
|
|
29383
|
+
//#endregion
|
|
29278
29384
|
//#region src/cli/Cli.ts
|
|
29385
|
+
/** `<name>=<value>` occurrences of `flag` → one record, each name a setting name given at most once. */
|
|
29386
|
+
const decodeAssignments = (flag, raws) => {
|
|
29387
|
+
const vars = {};
|
|
29388
|
+
for (const raw of raws) {
|
|
29389
|
+
const eq = raw.indexOf("=");
|
|
29390
|
+
if (eq <= 0) return left(`gtd: ${flag} must be <name>=<value> with a non-empty name — got "${raw}"`);
|
|
29391
|
+
const name = raw.slice(0, eq);
|
|
29392
|
+
const value = raw.slice(eq + 1);
|
|
29393
|
+
if (!isSettingName(name)) return left(`gtd: ${flag} ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
29394
|
+
if (/[\r\n]/.test(value)) return left(`gtd: ${flag} ${name} must be a single-line value`);
|
|
29395
|
+
if (Object.hasOwn(vars, name)) return left(`gtd: ${flag} ${name} specified more than once`);
|
|
29396
|
+
if (!name.startsWith("flag_")) {
|
|
29397
|
+
vars[name] = value;
|
|
29398
|
+
continue;
|
|
29399
|
+
}
|
|
29400
|
+
const pinned = flagSettingValue(value);
|
|
29401
|
+
if (pinned === void 0) return left(`gtd: ${flag} ${name} must be 1, 0, true or false`);
|
|
29402
|
+
vars[name] = pinned;
|
|
29403
|
+
}
|
|
29404
|
+
return right(vars);
|
|
29405
|
+
};
|
|
29279
29406
|
const nonNegativeNumber = (raw, flag) => {
|
|
29280
29407
|
const n = Number(raw);
|
|
29281
29408
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return left(`gtd: ${flag} must be a non-negative number — got "${raw}"`);
|
|
29282
29409
|
return right(n);
|
|
29283
29410
|
};
|
|
29411
|
+
/** The commands whose only output is a script: `--json` prints `{ schema, script }`. */
|
|
29412
|
+
const SCRIPT_KINDS = /* @__PURE__ */ new Set([
|
|
29413
|
+
"start",
|
|
29414
|
+
"abandon",
|
|
29415
|
+
"restore",
|
|
29416
|
+
"validate",
|
|
29417
|
+
"historyFetch"
|
|
29418
|
+
]);
|
|
29419
|
+
const SCRIPT_JSON_HELP = "Print { schema, script }, or with --json=script the script alone";
|
|
29284
29420
|
const JSON_KINDS = /* @__PURE__ */ new Set([
|
|
29421
|
+
...SCRIPT_KINDS,
|
|
29285
29422
|
"next",
|
|
29286
29423
|
"land",
|
|
29287
29424
|
"judge",
|
|
29288
29425
|
"judgeAnswer",
|
|
29289
|
-
"doors",
|
|
29290
29426
|
"historyList",
|
|
29291
29427
|
"historyShow",
|
|
29292
29428
|
"historyMetrics",
|
|
@@ -29300,22 +29436,14 @@ const FLAGS = [
|
|
|
29300
29436
|
repeatable: false,
|
|
29301
29437
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
29302
29438
|
decode: ([raw]) => right(raw ?? ""),
|
|
29303
|
-
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land
|
|
29439
|
+
scopeError: "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`) — every other command prints plain text; see `gtd guide` for the driver protocol briefing",
|
|
29304
29440
|
valueHint: "<path>",
|
|
29305
|
-
help:
|
|
29306
|
-
|
|
29307
|
-
"
|
|
29308
|
-
|
|
29309
|
-
|
|
29310
|
-
|
|
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
|
-
]
|
|
29441
|
+
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",
|
|
29442
|
+
helpFor: {
|
|
29443
|
+
strays: "Print one entry of the array (--json=0), nothing past its end",
|
|
29444
|
+
...Object.fromEntries([...SCRIPT_KINDS].map((kind) => [kind, SCRIPT_JSON_HELP])),
|
|
29445
|
+
start: `${SCRIPT_JSON_HELP}. Bare, print the list as a JSON array of { kind, name } (a door adds workflow and args)`
|
|
29446
|
+
}
|
|
29319
29447
|
},
|
|
29320
29448
|
{
|
|
29321
29449
|
name: "--port",
|
|
@@ -29328,11 +29456,7 @@ const FLAGS = [
|
|
|
29328
29456
|
},
|
|
29329
29457
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
29330
29458
|
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
|
-
]
|
|
29459
|
+
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
29460
|
},
|
|
29337
29461
|
{
|
|
29338
29462
|
name: "--host",
|
|
@@ -29342,11 +29466,7 @@ const FLAGS = [
|
|
|
29342
29466
|
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
29467
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
29344
29468
|
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
|
-
]
|
|
29469
|
+
help: "Bind this address directly instead of publishing through tailscale serve"
|
|
29350
29470
|
},
|
|
29351
29471
|
{
|
|
29352
29472
|
name: "--self-signed",
|
|
@@ -29356,7 +29476,7 @@ const FLAGS = [
|
|
|
29356
29476
|
decode: () => right(true),
|
|
29357
29477
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
29358
29478
|
valueHint: "",
|
|
29359
|
-
help:
|
|
29479
|
+
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key"
|
|
29360
29480
|
},
|
|
29361
29481
|
{
|
|
29362
29482
|
name: "--dev",
|
|
@@ -29366,7 +29486,7 @@ const FLAGS = [
|
|
|
29366
29486
|
decode: () => right(true),
|
|
29367
29487
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
29368
29488
|
valueHint: "",
|
|
29369
|
-
help:
|
|
29489
|
+
help: "Serve local development sources instead of the packaged build"
|
|
29370
29490
|
},
|
|
29371
29491
|
{
|
|
29372
29492
|
name: "--provider",
|
|
@@ -29376,11 +29496,7 @@ const FLAGS = [
|
|
|
29376
29496
|
decode: ([raw]) => raw === "fixed" || raw === "jev" || raw === "llm" ? right(raw) : left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
29377
29497
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
29378
29498
|
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
|
-
]
|
|
29499
|
+
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
29500
|
},
|
|
29385
29501
|
{
|
|
29386
29502
|
name: "--answers",
|
|
@@ -29390,11 +29506,7 @@ const FLAGS = [
|
|
|
29390
29506
|
decode: ([raw]) => raw === void 0 || raw === "" ? left("gtd: --answers must be a non-empty path") : right(raw),
|
|
29391
29507
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
29392
29508
|
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
|
-
]
|
|
29509
|
+
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)"
|
|
29398
29510
|
},
|
|
29399
29511
|
{
|
|
29400
29512
|
name: "--cost",
|
|
@@ -29404,7 +29516,7 @@ const FLAGS = [
|
|
|
29404
29516
|
decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
|
|
29405
29517
|
scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
|
|
29406
29518
|
valueHint: "<n>",
|
|
29407
|
-
help:
|
|
29519
|
+
help: "Record the token cost of the invocation that just finished on the commit"
|
|
29408
29520
|
},
|
|
29409
29521
|
{
|
|
29410
29522
|
name: "--model",
|
|
@@ -29412,61 +29524,33 @@ const FLAGS = [
|
|
|
29412
29524
|
repeatable: false,
|
|
29413
29525
|
scope: (kind) => kind === "land" || kind === "judgeRun",
|
|
29414
29526
|
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
|
-
scopeError: "gtd: --model is only valid for `gtd land` (with --cost) or `gtd judge run`",
|
|
29527
|
+
scopeError: "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)",
|
|
29416
29528
|
valueHint: "<name>",
|
|
29417
|
-
help:
|
|
29418
|
-
|
|
29419
|
-
"
|
|
29420
|
-
"GTD_JUDGE_MODEL, then judge.json, then sonnet"
|
|
29421
|
-
|
|
29529
|
+
help: "",
|
|
29530
|
+
helpFor: {
|
|
29531
|
+
land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
|
|
29532
|
+
judgeRun: "The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)"
|
|
29533
|
+
}
|
|
29422
29534
|
},
|
|
29423
29535
|
{
|
|
29424
|
-
name: "--
|
|
29536
|
+
name: "--threshold",
|
|
29425
29537
|
arity: 1,
|
|
29426
|
-
repeatable:
|
|
29427
|
-
scope: (kind) => kind === "
|
|
29428
|
-
decode: (
|
|
29429
|
-
scopeError: "gtd: --
|
|
29430
|
-
valueHint: "<name>",
|
|
29431
|
-
help:
|
|
29538
|
+
repeatable: true,
|
|
29539
|
+
scope: (kind) => kind === "historyJudges",
|
|
29540
|
+
decode: (raws) => decodeAssignments("--threshold", raws),
|
|
29541
|
+
scopeError: "gtd: --threshold is only valid for `gtd history judges`",
|
|
29542
|
+
valueHint: "<name>=<value>",
|
|
29543
|
+
help: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)"
|
|
29432
29544
|
},
|
|
29433
29545
|
{
|
|
29434
29546
|
name: "--var",
|
|
29435
29547
|
arity: 1,
|
|
29436
29548
|
repeatable: true,
|
|
29437
|
-
scope: (kind) => kind === "start"
|
|
29438
|
-
decode: (raws) =>
|
|
29439
|
-
|
|
29440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
29441
|
-
for (const raw of raws) {
|
|
29442
|
-
const eq = raw.indexOf("=");
|
|
29443
|
-
if (eq <= 0) return left(`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`);
|
|
29444
|
-
const name = raw.slice(0, eq);
|
|
29445
|
-
const value = raw.slice(eq + 1);
|
|
29446
|
-
if (!isSettingName(name)) return left(`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
29447
|
-
if (/[\r\n]/.test(value)) return left(`gtd: --var ${name} must be a single-line value`);
|
|
29448
|
-
if (seen.has(name)) return left(`gtd: --var ${name} specified more than once`);
|
|
29449
|
-
seen.add(name);
|
|
29450
|
-
if (name.startsWith("flag_")) {
|
|
29451
|
-
const pinned = flagSettingValue(value);
|
|
29452
|
-
if (pinned === void 0) return left(`gtd: --var ${name} must be 1, 0, true or false`);
|
|
29453
|
-
vars[name] = pinned;
|
|
29454
|
-
continue;
|
|
29455
|
-
}
|
|
29456
|
-
vars[name] = value;
|
|
29457
|
-
}
|
|
29458
|
-
return right(vars);
|
|
29459
|
-
},
|
|
29460
|
-
scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
|
|
29549
|
+
scope: (kind) => kind === "start",
|
|
29550
|
+
decode: (raws) => decodeAssignments("--var", raws),
|
|
29551
|
+
scopeError: "gtd: --var is only valid for `gtd start <name>`",
|
|
29461
29552
|
valueHint: "<name>=<value>",
|
|
29462
|
-
help:
|
|
29463
|
-
"(with --workflow; repeatable) pin a process setting for the",
|
|
29464
|
-
"new process; the name must already be declared by the",
|
|
29465
|
-
"workflow's defaults (environment settings are not",
|
|
29466
|
-
"pinnable). (gtd history judges) re-decide past verdicts",
|
|
29467
|
-
"under this threshold: judgeIdenticalMinP or",
|
|
29468
|
-
"qualityTriageMinP"
|
|
29469
|
-
]
|
|
29553
|
+
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"
|
|
29470
29554
|
},
|
|
29471
29555
|
{
|
|
29472
29556
|
name: "--jsonl",
|
|
@@ -29476,8 +29560,8 @@ const FLAGS = [
|
|
|
29476
29560
|
decode: () => right(true),
|
|
29477
29561
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
29478
29562
|
valueHint: "",
|
|
29479
|
-
help:
|
|
29480
|
-
conflicts: ["--json", "--
|
|
29563
|
+
help: "Print one JSON line per verdict instead of the report",
|
|
29564
|
+
conflicts: ["--json", "--threshold"]
|
|
29481
29565
|
},
|
|
29482
29566
|
{
|
|
29483
29567
|
name: "--open-questions",
|
|
@@ -29487,12 +29571,7 @@ const FLAGS = [
|
|
|
29487
29571
|
decode: () => right(true),
|
|
29488
29572
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
29489
29573
|
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
|
-
]
|
|
29574
|
+
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain"
|
|
29496
29575
|
},
|
|
29497
29576
|
{
|
|
29498
29577
|
name: "--open-threads",
|
|
@@ -29502,15 +29581,7 @@ const FLAGS = [
|
|
|
29502
29581
|
decode: () => right(true),
|
|
29503
29582
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
29504
29583
|
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
|
-
]
|
|
29584
|
+
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
29585
|
},
|
|
29515
29586
|
{
|
|
29516
29587
|
name: "--verbose",
|
|
@@ -29520,11 +29591,7 @@ const FLAGS = [
|
|
|
29520
29591
|
decode: () => right(true),
|
|
29521
29592
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
29522
29593
|
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
|
-
]
|
|
29594
|
+
help: "Narrate on stderr what gtd resolved and where its config came from"
|
|
29528
29595
|
}
|
|
29529
29596
|
];
|
|
29530
29597
|
/**
|
|
@@ -29544,277 +29611,92 @@ const flagByToken = (token) => {
|
|
|
29544
29611
|
};
|
|
29545
29612
|
const COMMAND_ROWS = [
|
|
29546
29613
|
{
|
|
29547
|
-
token: "
|
|
29548
|
-
kind: "
|
|
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",
|
|
29614
|
+
token: "next",
|
|
29615
|
+
kind: "next",
|
|
29582
29616
|
arity: "none",
|
|
29583
|
-
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
|
|
29589
|
-
|
|
29590
|
-
]
|
|
29617
|
+
group: "Core loop",
|
|
29618
|
+
summary: "Print what should happen next",
|
|
29619
|
+
details: `Print what should happen next in the process, changing nothing — safe to poll.
|
|
29620
|
+
|
|
29621
|
+
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.
|
|
29622
|
+
|
|
29623
|
+
--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).`
|
|
29591
29624
|
},
|
|
29592
29625
|
{
|
|
29593
|
-
token: "
|
|
29594
|
-
kind: "
|
|
29626
|
+
token: "land",
|
|
29627
|
+
kind: "land",
|
|
29595
29628
|
arity: "none",
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
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
|
-
]
|
|
29629
|
+
group: "Core loop",
|
|
29630
|
+
summary: "Record what the tree now shows",
|
|
29631
|
+
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.
|
|
29632
|
+
|
|
29633
|
+
gtd writes nothing itself. Plain output names the commit it would make; --json=script prints the script that makes it:
|
|
29634
|
+
|
|
29635
|
+
s=$(gtd land --json=script) && sh -c "$s"
|
|
29636
|
+
|
|
29637
|
+
--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.`
|
|
29616
29638
|
},
|
|
29617
29639
|
{
|
|
29618
29640
|
token: "validate",
|
|
29619
29641
|
kind: "validate",
|
|
29620
29642
|
arity: "none",
|
|
29621
|
-
|
|
29622
|
-
|
|
29623
|
-
|
|
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
|
-
]
|
|
29643
|
+
group: "Core loop",
|
|
29644
|
+
summary: "Print the script that checks the steering file",
|
|
29645
|
+
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.`
|
|
29632
29646
|
},
|
|
29633
29647
|
{
|
|
29634
|
-
token: "
|
|
29635
|
-
kind: "
|
|
29636
|
-
arity:
|
|
29637
|
-
|
|
29648
|
+
token: "start",
|
|
29649
|
+
kind: "start",
|
|
29650
|
+
arity: {
|
|
29651
|
+
name: "name",
|
|
29652
|
+
variadic: "args",
|
|
29653
|
+
bare: true
|
|
29654
|
+
},
|
|
29655
|
+
group: "Start and stop",
|
|
29656
|
+
summary: "Start a workflow or door; bare, list them",
|
|
29657
|
+
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
29658
|
+
|
|
29659
|
+
<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.
|
|
29660
|
+
|
|
29661
|
+
s=$(gtd start <name> --json=script) && sh -c "$s"
|
|
29662
|
+
|
|
29663
|
+
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).`
|
|
29638
29664
|
},
|
|
29639
29665
|
{
|
|
29640
|
-
token: "
|
|
29641
|
-
kind: "
|
|
29666
|
+
token: "abandon",
|
|
29667
|
+
kind: "abandon",
|
|
29642
29668
|
arity: "none",
|
|
29643
|
-
|
|
29644
|
-
|
|
29645
|
-
|
|
29646
|
-
|
|
29647
|
-
|
|
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
|
-
]
|
|
29669
|
+
group: "Start and stop",
|
|
29670
|
+
summary: "Stop the process, keeping its changes",
|
|
29671
|
+
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.
|
|
29672
|
+
|
|
29673
|
+
s=$(gtd abandon --json=script) && sh -c "$s"`
|
|
29697
29674
|
},
|
|
29698
29675
|
{
|
|
29699
|
-
token: "
|
|
29700
|
-
kind: "
|
|
29676
|
+
token: "restore",
|
|
29677
|
+
kind: "restore",
|
|
29701
29678
|
arity: "none",
|
|
29702
|
-
|
|
29703
|
-
|
|
29704
|
-
|
|
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
|
-
]
|
|
29679
|
+
group: "Start and stop",
|
|
29680
|
+
summary: "Bring back the last abandoned process",
|
|
29681
|
+
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.`
|
|
29710
29682
|
},
|
|
29711
29683
|
{
|
|
29712
29684
|
token: "summary",
|
|
29713
29685
|
kind: "summary",
|
|
29714
29686
|
arity: "none",
|
|
29715
|
-
|
|
29716
|
-
|
|
29717
|
-
|
|
29718
|
-
|
|
29719
|
-
|
|
29720
|
-
"it.processCost/processCostByModel. Writes nothing: no git,",
|
|
29721
|
-
"no state transition, no file, no session identity — the",
|
|
29722
|
-
"driver pipes the output to a cold agent and does what it",
|
|
29723
|
-
"wants with the result (a squash, an amend, a PR body).",
|
|
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
|
-
]
|
|
29687
|
+
group: "Inspect",
|
|
29688
|
+
summary: "Print the prompt for the closing message",
|
|
29689
|
+
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.
|
|
29690
|
+
|
|
29691
|
+
Refuses when the workflow declares no summary template, or when the process has no commits.`
|
|
29729
29692
|
},
|
|
29730
29693
|
{
|
|
29731
29694
|
token: "base",
|
|
29732
29695
|
kind: "base",
|
|
29733
29696
|
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
|
-
]
|
|
29743
|
-
},
|
|
29744
|
-
{
|
|
29745
|
-
token: "door",
|
|
29746
|
-
kind: "door",
|
|
29747
|
-
arity: {
|
|
29748
|
-
name: "name",
|
|
29749
|
-
variadic: "args"
|
|
29750
|
-
},
|
|
29751
|
-
details: [
|
|
29752
|
-
"Start a process through a named shortcut, mapping the",
|
|
29753
|
-
"positional args to that workflow's process settings — the",
|
|
29754
|
-
"same start `gtd --workflow` makes, printed the same way.",
|
|
29755
|
-
"Each workflow the config exports brings its own doors (gtd's",
|
|
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
|
-
]
|
|
29761
|
-
},
|
|
29762
|
-
{
|
|
29763
|
-
token: "doors",
|
|
29764
|
-
kind: "doors",
|
|
29765
|
-
arity: "none",
|
|
29766
|
-
details: [
|
|
29767
|
-
"List every door, sorted: one `name [args] → workflow` line",
|
|
29768
|
-
"each. --json prints one JSON array of { name, workflow,",
|
|
29769
|
-
"args: [{ name, optional }] } instead; --json=<path> reads one",
|
|
29770
|
-
"value off it. Writes nothing"
|
|
29771
|
-
]
|
|
29772
|
-
},
|
|
29773
|
-
{
|
|
29774
|
-
token: "judge",
|
|
29775
|
-
kind: "judge",
|
|
29776
|
-
arity: "none",
|
|
29777
|
-
details: [
|
|
29778
|
-
"Print the resolved rest's pending judgment — the prepared",
|
|
29779
|
-
"state, its typed questions, and their criteria — the same",
|
|
29780
|
-
"judge field `gtd next --json` already carries. Read-only:",
|
|
29781
|
-
"resolves no session, writes nothing. Refuses (exit 1) when",
|
|
29782
|
-
"the resolved rest declares no judge:"
|
|
29783
|
-
]
|
|
29784
|
-
},
|
|
29785
|
-
{
|
|
29786
|
-
token: "judge answer",
|
|
29787
|
-
kind: "judgeAnswer",
|
|
29788
|
-
arity: "none",
|
|
29789
|
-
details: [
|
|
29790
|
-
"Read a verdict off stdin — one { id, answer, p } entry per",
|
|
29791
|
-
"question the pending judgment declared — and decode it",
|
|
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
|
-
]
|
|
29798
|
-
},
|
|
29799
|
-
{
|
|
29800
|
-
token: "judge run",
|
|
29801
|
-
kind: "judgeRun",
|
|
29802
|
-
arity: "none",
|
|
29803
|
-
details: [
|
|
29804
|
-
"Answer a judgment: read the `gtd judge --json` document off",
|
|
29805
|
-
"stdin, write a verdict [{ id, answer, p }] on stdout — what",
|
|
29806
|
-
"`gtd judge answer` decodes. --provider fixed answers from",
|
|
29807
|
-
"--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
|
|
29808
|
-
"does not cover are left out. --provider jev asks TypeSafe",
|
|
29809
|
-
"(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
|
|
29810
|
-
"on PATH (Claude Code only), model sonnet unless --model.",
|
|
29811
|
-
"No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
|
|
29812
|
-
"judge.json, else jev when TYPESAFE_API_KEY is set and no",
|
|
29813
|
-
"model is given, else llm; --model likewise falls back to",
|
|
29814
|
-
"GTD_JUDGE_MODEL, then the file's model. Needs no",
|
|
29815
|
-
"repository. jev and llm exit 1 when they cannot answer",
|
|
29816
|
-
"every question, with nothing on stdout"
|
|
29817
|
-
]
|
|
29697
|
+
group: "Inspect",
|
|
29698
|
+
summary: "Print the commit a review diff starts from",
|
|
29699
|
+
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
29700
|
},
|
|
29819
29701
|
{
|
|
29820
29702
|
token: "strays",
|
|
@@ -29823,45 +29705,27 @@ const COMMAND_ROWS = [
|
|
|
29823
29705
|
name: "range",
|
|
29824
29706
|
variadic: "glob"
|
|
29825
29707
|
},
|
|
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
|
-
]
|
|
29708
|
+
group: "Inspect",
|
|
29709
|
+
summary: "List changed files outside the globs",
|
|
29710
|
+
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.
|
|
29711
|
+
|
|
29712
|
+
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
29713
|
},
|
|
29841
29714
|
{
|
|
29842
29715
|
token: "history list",
|
|
29843
29716
|
kind: "historyList",
|
|
29844
29717
|
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
|
-
]
|
|
29718
|
+
group: "Inspect",
|
|
29719
|
+
summary: "List finished processes",
|
|
29720
|
+
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
29721
|
},
|
|
29853
29722
|
{
|
|
29854
29723
|
token: "history show",
|
|
29855
29724
|
kind: "historyShow",
|
|
29856
29725
|
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
|
-
]
|
|
29726
|
+
group: "Inspect",
|
|
29727
|
+
summary: "Replay one finished process",
|
|
29728
|
+
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
29729
|
},
|
|
29866
29730
|
{
|
|
29867
29731
|
token: "history metrics",
|
|
@@ -29870,18 +29734,9 @@ const COMMAND_ROWS = [
|
|
|
29870
29734
|
name: "ref",
|
|
29871
29735
|
many: true
|
|
29872
29736
|
},
|
|
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
|
-
]
|
|
29737
|
+
group: "Inspect",
|
|
29738
|
+
summary: "Report cost and rework of finished processes",
|
|
29739
|
+
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
29740
|
},
|
|
29886
29741
|
{
|
|
29887
29742
|
token: "history judges",
|
|
@@ -29890,18 +29745,9 @@ const COMMAND_ROWS = [
|
|
|
29890
29745
|
name: "ref",
|
|
29891
29746
|
many: true
|
|
29892
29747
|
},
|
|
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
|
-
]
|
|
29748
|
+
group: "Inspect",
|
|
29749
|
+
summary: "Calibrate the judges of finished processes",
|
|
29750
|
+
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
29751
|
},
|
|
29906
29752
|
{
|
|
29907
29753
|
token: "history fetch",
|
|
@@ -29910,69 +29756,201 @@ const COMMAND_ROWS = [
|
|
|
29910
29756
|
name: "remote",
|
|
29911
29757
|
optional: true
|
|
29912
29758
|
},
|
|
29913
|
-
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
|
|
29917
|
-
|
|
29759
|
+
group: "Inspect",
|
|
29760
|
+
summary: "Print a script that fetches finished processes",
|
|
29761
|
+
details: `Print a script that fetches every finished process kept on <remote> (default: origin) and reports how many arrived. gtd runs no git itself:
|
|
29762
|
+
|
|
29763
|
+
s=$(gtd history fetch --json=script) && sh -c "$s"`
|
|
29764
|
+
},
|
|
29765
|
+
{
|
|
29766
|
+
token: "ui",
|
|
29767
|
+
kind: "ui",
|
|
29768
|
+
arity: "none",
|
|
29769
|
+
group: "Inspect",
|
|
29770
|
+
summary: "Serve the web and phone client",
|
|
29771
|
+
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.`
|
|
29772
|
+
},
|
|
29773
|
+
{
|
|
29774
|
+
token: "guide",
|
|
29775
|
+
kind: "guide",
|
|
29776
|
+
arity: "none",
|
|
29777
|
+
group: "Driver plumbing",
|
|
29778
|
+
summary: "Print a briefing for writing a driver",
|
|
29779
|
+
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.`
|
|
29780
|
+
},
|
|
29781
|
+
{
|
|
29782
|
+
token: "judge",
|
|
29783
|
+
kind: "judge",
|
|
29784
|
+
arity: "none",
|
|
29785
|
+
group: "Driver plumbing",
|
|
29786
|
+
summary: "Print the pending judgment",
|
|
29787
|
+
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.`
|
|
29788
|
+
},
|
|
29789
|
+
{
|
|
29790
|
+
token: "judge answer",
|
|
29791
|
+
kind: "judgeAnswer",
|
|
29792
|
+
arity: "none",
|
|
29793
|
+
group: "Driver plumbing",
|
|
29794
|
+
summary: "Land a verdict read from stdin",
|
|
29795
|
+
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).`
|
|
29796
|
+
},
|
|
29797
|
+
{
|
|
29798
|
+
token: "judge run",
|
|
29799
|
+
kind: "judgeRun",
|
|
29800
|
+
arity: "none",
|
|
29801
|
+
group: "Driver plumbing",
|
|
29802
|
+
summary: "Answer a judgment with a model",
|
|
29803
|
+
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.
|
|
29804
|
+
|
|
29805
|
+
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).`
|
|
29806
|
+
},
|
|
29807
|
+
{
|
|
29808
|
+
token: "check",
|
|
29809
|
+
kind: "check",
|
|
29810
|
+
arity: { names: ["mode", "file"] },
|
|
29811
|
+
group: "Driver plumbing",
|
|
29812
|
+
summary: "Check a steering file's format",
|
|
29813
|
+
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.`
|
|
29814
|
+
},
|
|
29815
|
+
{
|
|
29816
|
+
token: "uncheck",
|
|
29817
|
+
kind: "uncheck",
|
|
29818
|
+
arity: { name: "file" },
|
|
29819
|
+
group: "Driver plumbing",
|
|
29820
|
+
summary: "Untick a review file's boxes",
|
|
29821
|
+
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.`
|
|
29822
|
+
},
|
|
29823
|
+
{
|
|
29824
|
+
token: "lsp",
|
|
29825
|
+
kind: "lsp",
|
|
29826
|
+
arity: "none",
|
|
29827
|
+
group: "Driver plumbing",
|
|
29828
|
+
summary: "Start the steering-file language server",
|
|
29829
|
+
details: `Start the language server for .gtd/ steering files on stdio.`
|
|
29918
29830
|
}
|
|
29919
29831
|
];
|
|
29920
29832
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
29921
|
-
|
|
29922
|
-
const
|
|
29923
|
-
|
|
29924
|
-
|
|
29925
|
-
|
|
29926
|
-
|
|
29927
|
-
const
|
|
29928
|
-
const
|
|
29929
|
-
const
|
|
29930
|
-
const
|
|
29833
|
+
/** Every word that may follow `gtd`, for "did you mean". */
|
|
29834
|
+
const COMMAND_WORDS = [.../* @__PURE__ */ new Set([
|
|
29835
|
+
...COMMAND_ROWS.map((r) => r.token.split(" ")[0]),
|
|
29836
|
+
"help",
|
|
29837
|
+
"version"
|
|
29838
|
+
])];
|
|
29839
|
+
const TOP_COLUMN = 28;
|
|
29840
|
+
const OPTION_COLUMN = 22;
|
|
29841
|
+
const synopsis = (row) => {
|
|
29842
|
+
const { token, arity } = row;
|
|
29843
|
+
if (arity === "none") return token;
|
|
29844
|
+
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`;
|
|
29845
|
+
if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`;
|
|
29846
|
+
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`;
|
|
29847
|
+
if ("optional" in arity) return `${token} [${arity.name}]`;
|
|
29848
|
+
if ("many" in arity) return `${token} [${arity.name}...]`;
|
|
29849
|
+
return `${token} <${arity.name}>`;
|
|
29850
|
+
};
|
|
29851
|
+
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--var", "--threshold"]);
|
|
29931
29852
|
const flagHeader = (row) => {
|
|
29932
29853
|
if (row.valueHint === "") return row.name;
|
|
29854
|
+
if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`;
|
|
29933
29855
|
return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
|
|
29934
29856
|
};
|
|
29857
|
+
const GLOBAL_OPTIONS = [
|
|
29858
|
+
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
29859
|
+
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
29860
|
+
["-V, --version", "Print the version"],
|
|
29861
|
+
["--", "End the options: every word after it is an argument"]
|
|
29862
|
+
];
|
|
29863
|
+
const EXTRA_TOPICS = [[
|
|
29864
|
+
"help [command]",
|
|
29865
|
+
"Print help for gtd or one command",
|
|
29866
|
+
"Print gtd's overview, or with <command> that command's own help."
|
|
29867
|
+
], [
|
|
29868
|
+
"version",
|
|
29869
|
+
"Print the version",
|
|
29870
|
+
"Print gtd's version."
|
|
29871
|
+
]];
|
|
29872
|
+
const EXAMPLES = ` $ gtd start list what can start
|
|
29873
|
+
$ gtd start <name> start a process
|
|
29874
|
+
$ gtd next see what happens next
|
|
29875
|
+
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
29876
|
+
$ gtd help land one command's details`;
|
|
29877
|
+
const lines = (block) => block.map((l) => `${l}\n`).join("");
|
|
29878
|
+
const GROUPS = [
|
|
29879
|
+
"Core loop",
|
|
29880
|
+
"Start and stop",
|
|
29881
|
+
"Inspect",
|
|
29882
|
+
"Driver plumbing"
|
|
29883
|
+
];
|
|
29935
29884
|
const renderHelp = () => {
|
|
29936
|
-
const
|
|
29937
|
-
const
|
|
29938
|
-
|
|
29885
|
+
const groups = GROUPS.map((group) => {
|
|
29886
|
+
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) => definition(synopsis(r), r.summary, TOP_COLUMN));
|
|
29887
|
+
const extra = group === "Driver plumbing" ? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN)) : [];
|
|
29888
|
+
return `${group}:\n${lines([...rows, ...extra])}`;
|
|
29939
29889
|
});
|
|
29940
|
-
return
|
|
29941
|
-
|
|
29942
|
-
|
|
29943
|
-
|
|
29944
|
-
|
|
29945
|
-
|
|
29946
|
-
|
|
29890
|
+
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 guide` to learn how to drive gtd."));
|
|
29891
|
+
};
|
|
29892
|
+
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
29893
|
+
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
29894
|
+
|
|
29895
|
+
gtd start <name> start a process
|
|
29896
|
+
gtd next print what should happen next
|
|
29897
|
+
gtd land record what the tree now shows
|
|
29898
|
+
|
|
29899
|
+
`;
|
|
29900
|
+
const renderCommandHelp = (row) => {
|
|
29901
|
+
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));
|
|
29902
|
+
const global = definition("-v, --verbose", "Narrate on stderr what gtd resolved", OPTION_COLUMN);
|
|
29903
|
+
return `Usage: gtd ${synopsis(row)}${options.length > 0 ? " [options]" : ""}\n\n` + lines(wrap(row.details)) + `\nOptions:\n${lines([...options, ...global])}`;
|
|
29904
|
+
};
|
|
29905
|
+
/** `gtd help <topic…>`'s text: one command's help, every `history` subcommand's for `history`, or undefined for an unknown topic. */
|
|
29906
|
+
const renderTopicHelp = (topic) => {
|
|
29907
|
+
const two = commandByToken(topic.slice(0, 2).join(" "));
|
|
29908
|
+
if (two !== void 0 && topic.length >= 2) return renderCommandHelp(two);
|
|
29909
|
+
const [first = ""] = topic;
|
|
29910
|
+
const one = commandByToken(first);
|
|
29911
|
+
if (one !== void 0) return renderCommandHelp(one);
|
|
29912
|
+
const extra = EXTRA_TOPICS.find(([term]) => term.split(" ")[0] === first);
|
|
29913
|
+
if (extra !== void 0) return `Usage: gtd ${extra[0]}\n\n${lines(wrap(extra[2]))}`;
|
|
29914
|
+
const family = COMMAND_ROWS.filter((r) => r.token.startsWith(`${first} `));
|
|
29915
|
+
if (first !== "" && family.length > 0) return family.map(renderCommandHelp).join("\n");
|
|
29916
|
+
};
|
|
29917
|
+
/** A usage error's tail: the nearest candidate when `word` looks like a typo of one, and where the full list is. */
|
|
29918
|
+
const didYouMean = (word, candidates, prefix = "", pointer = "`gtd help` for every command") => {
|
|
29919
|
+
const guess = closest(word, candidates);
|
|
29920
|
+
return guess === void 0 ? ` — run ${pointer}` : ` — did you mean \`${prefix}${guess}\`? Run ${pointer}`;
|
|
29921
|
+
};
|
|
29922
|
+
const unknownOption = (tok, jsonSeen) => {
|
|
29923
|
+
const candidates = tok.startsWith("--") ? FLAGS.map((f) => f.name) : [];
|
|
29924
|
+
return {
|
|
29925
|
+
kind: "error",
|
|
29926
|
+
message: `gtd: unknown option '${tok}'${didYouMean(tok.split("=")[0], candidates, "", "`gtd help <command>` for its options")}`,
|
|
29927
|
+
jsonSeen
|
|
29928
|
+
};
|
|
29947
29929
|
};
|
|
29948
29930
|
const tokenize = (tail) => {
|
|
29949
29931
|
const positionals = [];
|
|
29950
29932
|
const byFlag = /* @__PURE__ */ new Map();
|
|
29951
29933
|
const present = /* @__PURE__ */ new Set();
|
|
29952
29934
|
let jsonSeen = false;
|
|
29935
|
+
let afterBareJson;
|
|
29936
|
+
const end = tail.indexOf("--");
|
|
29953
29937
|
for (let i = 0; i < tail.length; i++) {
|
|
29954
29938
|
const original = tail[i];
|
|
29939
|
+
if (i === end) continue;
|
|
29955
29940
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original;
|
|
29956
|
-
if (!tok.startsWith("
|
|
29941
|
+
if (end !== -1 && i > end || !tok.startsWith("-") || tok === "-") {
|
|
29942
|
+
if (tail[i - 1] === "--json") afterBareJson ??= original;
|
|
29957
29943
|
positionals.push(original);
|
|
29958
29944
|
continue;
|
|
29959
29945
|
}
|
|
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
|
-
};
|
|
29946
|
+
const row = tok.startsWith("--") ? flagByToken(tok) : void 0;
|
|
29947
|
+
if (row === void 0) return unknownOption(tok, jsonSeen);
|
|
29966
29948
|
const eq = tok.indexOf("=");
|
|
29967
29949
|
const alreadyPresent = present.has(row.name);
|
|
29968
29950
|
present.add(row.name);
|
|
29969
29951
|
if (row.name === "--json") jsonSeen = true;
|
|
29970
29952
|
if (row.arity === 0) {
|
|
29971
|
-
if (eq !== -1) return
|
|
29972
|
-
kind: "error",
|
|
29973
|
-
message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
|
|
29974
|
-
jsonSeen
|
|
29975
|
-
};
|
|
29953
|
+
if (eq !== -1) return unknownOption(tok, jsonSeen);
|
|
29976
29954
|
if (!byFlag.has(row.name)) byFlag.set(row.name, []);
|
|
29977
29955
|
continue;
|
|
29978
29956
|
}
|
|
@@ -30023,7 +30001,8 @@ const tokenize = (tail) => {
|
|
|
30023
30001
|
positionals,
|
|
30024
30002
|
byFlag,
|
|
30025
30003
|
present,
|
|
30026
|
-
jsonSeen
|
|
30004
|
+
jsonSeen,
|
|
30005
|
+
...afterBareJson !== void 0 ? { afterBareJson } : {}
|
|
30027
30006
|
};
|
|
30028
30007
|
};
|
|
30029
30008
|
const usagePlan = (message, json, stdout = "") => ({
|
|
@@ -30046,7 +30025,7 @@ const tooMany = (cmd, rest, expected, max) => rest.length > max ? `gtd ${cmd}: t
|
|
|
30046
30025
|
const arityError = (cmd, rest, arity) => {
|
|
30047
30026
|
if (arity === "none") return tooMany(cmd, rest, "none", 0);
|
|
30048
30027
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names);
|
|
30049
|
-
if ("many" in arity) return void 0;
|
|
30028
|
+
if ("many" in arity || "bare" in arity) return void 0;
|
|
30050
30029
|
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1);
|
|
30051
30030
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
30052
30031
|
return "variadic" in arity ? void 0 : tooMany(cmd, rest, `one ${arity.name}`, 1);
|
|
@@ -30069,54 +30048,85 @@ const decodeFlags = (byFlag) => {
|
|
|
30069
30048
|
}
|
|
30070
30049
|
return right(bag);
|
|
30071
30050
|
};
|
|
30051
|
+
/** `gtd history judges`' thresholds: each a known judge threshold with a numeric value. */
|
|
30052
|
+
const judgeThresholds = (thresholds) => {
|
|
30053
|
+
const unknown = Object.keys(thresholds).find((name) => !Object.hasOwn(THRESHOLDS, name));
|
|
30054
|
+
if (unknown !== void 0) return left(`gtd history judges: --threshold ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`);
|
|
30055
|
+
const nan = Object.entries(thresholds).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
|
|
30056
|
+
if (nan !== void 0) return left(`gtd history judges: --threshold ${nan[0]} must be a number — got "${nan[1]}"`);
|
|
30057
|
+
return right(thresholds);
|
|
30058
|
+
};
|
|
30072
30059
|
/** 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. */
|
|
30073
30060
|
const buildLandCommand = (bag) => ({
|
|
30074
30061
|
kind: "land",
|
|
30075
30062
|
...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
|
|
30076
30063
|
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
30077
30064
|
});
|
|
30078
|
-
const
|
|
30079
|
-
|
|
30065
|
+
const HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
|
|
30066
|
+
/** The options part of argv: everything before a `--`, which ends the options. */
|
|
30067
|
+
const optionsOf = (argv) => {
|
|
30068
|
+
const end = argv.indexOf("--", 2);
|
|
30069
|
+
return end === -1 ? argv : argv.slice(0, end);
|
|
30070
|
+
};
|
|
30071
|
+
/** `--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. */
|
|
30072
|
+
const helpPlan = (argv) => {
|
|
30073
|
+
const tokenized = tokenize(optionsOf(argv).slice(2).filter((tok) => !HELP_FLAGS.has(tok)));
|
|
30074
|
+
const topic = tokenized.kind === "ok" ? tokenized.positionals : [];
|
|
30075
|
+
return {
|
|
30076
|
+
kind: "output",
|
|
30077
|
+
stdout: renderTopicHelp(topic) ?? renderHelp()
|
|
30078
|
+
};
|
|
30079
|
+
};
|
|
30080
|
+
/** `gtd help [topic…]`: an unknown topic is a usage error naming the nearest command. */
|
|
30081
|
+
const helpCommandPlan = (topic, json) => {
|
|
30082
|
+
if (topic.length === 0) return {
|
|
30080
30083
|
kind: "output",
|
|
30081
30084
|
stdout: renderHelp()
|
|
30082
30085
|
};
|
|
30083
|
-
|
|
30086
|
+
const text = renderTopicHelp(topic);
|
|
30087
|
+
if (text !== void 0) return {
|
|
30088
|
+
kind: "output",
|
|
30089
|
+
stdout: text
|
|
30090
|
+
};
|
|
30091
|
+
return usagePlan(`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0], COMMAND_WORDS, "gtd help ")}`, json);
|
|
30092
|
+
};
|
|
30093
|
+
const parseArgv = (argv) => {
|
|
30094
|
+
const options = optionsOf(argv);
|
|
30095
|
+
if (options.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv);
|
|
30096
|
+
if (options.includes("--version") || options.includes("-V")) return {
|
|
30084
30097
|
kind: "output",
|
|
30085
30098
|
stdout: `${GTD_VERSION}\n`
|
|
30086
30099
|
};
|
|
30087
30100
|
const tokenized = tokenize(argv.slice(2));
|
|
30088
30101
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
|
|
30089
|
-
const { positionals, byFlag, present, jsonSeen } = tokenized;
|
|
30102
|
+
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized;
|
|
30090
30103
|
const first = positionals[0];
|
|
30091
|
-
const workflowRaw = byFlag.get("--workflow")?.[0];
|
|
30092
|
-
const workflowPresent = workflowRaw !== void 0;
|
|
30093
30104
|
if (first === "version") return {
|
|
30094
30105
|
kind: "output",
|
|
30095
30106
|
stdout: `${GTD_VERSION}\n`
|
|
30096
30107
|
};
|
|
30097
|
-
if (first === "help") return
|
|
30098
|
-
kind: "output",
|
|
30099
|
-
stdout: renderHelp()
|
|
30100
|
-
};
|
|
30108
|
+
if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen);
|
|
30101
30109
|
const isHistory = first === "history";
|
|
30102
30110
|
const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
|
|
30103
|
-
if (isHistory && row === void 0)
|
|
30111
|
+
if (isHistory && row === void 0) {
|
|
30112
|
+
const subcommands = COMMAND_ROWS.filter((r) => r.token.startsWith("history ")).map((r) => r.token.split(" ")[1]);
|
|
30113
|
+
const typo = closest(positionals[1] ?? "", subcommands);
|
|
30114
|
+
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);
|
|
30115
|
+
}
|
|
30104
30116
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
|
|
30105
|
-
const selectsStart = workflowPresent && first === void 0;
|
|
30106
30117
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run";
|
|
30107
|
-
|
|
30108
|
-
if (row === void 0 && !selectsStart) {
|
|
30118
|
+
if (row === void 0 || first === void 0) {
|
|
30109
30119
|
const violation = scopeViolation(void 0, present);
|
|
30110
30120
|
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);
|
|
30121
|
+
if (first === void 0) return usagePlan("gtd: missing command — run `gtd help` for every command, or `gtd guide` to learn how to drive gtd", jsonSeen, SHORT_USAGE);
|
|
30122
|
+
return usagePlan(`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`, jsonSeen);
|
|
30113
30123
|
}
|
|
30114
|
-
const
|
|
30115
|
-
|
|
30116
|
-
|
|
30117
|
-
|
|
30118
|
-
const
|
|
30119
|
-
|
|
30124
|
+
const kind = judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row.kind;
|
|
30125
|
+
const restPositionals = positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
|
|
30126
|
+
const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
|
|
30127
|
+
if (arityMsg !== void 0) {
|
|
30128
|
+
const hint = afterBareJson !== void 0 && restPositionals.includes(afterBareJson) ? ` — to print one field, write --json=${afterBareJson}` : "";
|
|
30129
|
+
return usagePlan(arityMsg + hint, jsonSeen);
|
|
30120
30130
|
}
|
|
30121
30131
|
const violation = scopeViolation(kind, present);
|
|
30122
30132
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
@@ -30140,30 +30150,20 @@ const parseArgv = (argv) => {
|
|
|
30140
30150
|
};
|
|
30141
30151
|
}
|
|
30142
30152
|
if (kind === "start") {
|
|
30143
|
-
const
|
|
30153
|
+
const [name, ...args] = restPositionals;
|
|
30154
|
+
if (name === void 0 && bag["--var"] !== void 0) return usagePlan("gtd start: --var needs a <name> — run `gtd start` to list the workflows and doors", jsonSeen);
|
|
30144
30155
|
return {
|
|
30145
30156
|
kind: "command",
|
|
30146
30157
|
command: {
|
|
30147
30158
|
kind: "start",
|
|
30148
|
-
|
|
30149
|
-
|
|
30150
|
-
vars: bag["--var"] ?? {}
|
|
30151
|
-
label
|
|
30159
|
+
...name !== void 0 ? { name } : {},
|
|
30160
|
+
args,
|
|
30161
|
+
vars: bag["--var"] ?? {}
|
|
30152
30162
|
},
|
|
30153
30163
|
json,
|
|
30154
30164
|
verbose
|
|
30155
30165
|
};
|
|
30156
30166
|
}
|
|
30157
|
-
if (kind === "door") return {
|
|
30158
|
-
kind: "command",
|
|
30159
|
-
command: {
|
|
30160
|
-
kind: "door",
|
|
30161
|
-
name: restPositionals[0],
|
|
30162
|
-
args: restPositionals.slice(1)
|
|
30163
|
-
},
|
|
30164
|
-
json,
|
|
30165
|
-
verbose
|
|
30166
|
-
};
|
|
30167
30167
|
if (kind === "ui") return {
|
|
30168
30168
|
kind: "command",
|
|
30169
30169
|
command: {
|
|
@@ -30229,17 +30229,14 @@ const parseArgv = (argv) => {
|
|
|
30229
30229
|
verbose
|
|
30230
30230
|
};
|
|
30231
30231
|
if (kind === "historyJudges") {
|
|
30232
|
-
const
|
|
30233
|
-
|
|
30234
|
-
if (unknown !== void 0) return usagePlan(`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`, jsonSeen);
|
|
30235
|
-
const nan = Object.entries(vars).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
|
|
30236
|
-
if (nan !== void 0) return usagePlan(`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`, jsonSeen);
|
|
30232
|
+
const thresholds = judgeThresholds(bag["--threshold"] ?? {});
|
|
30233
|
+
if (isLeft(thresholds)) return usagePlan(thresholds.left, jsonSeen);
|
|
30237
30234
|
return {
|
|
30238
30235
|
kind: "command",
|
|
30239
30236
|
command: {
|
|
30240
30237
|
kind: "historyJudges",
|
|
30241
30238
|
refs: restPositionals,
|
|
30242
|
-
vars,
|
|
30239
|
+
vars: thresholds.right,
|
|
30243
30240
|
jsonl: bag["--jsonl"] ?? false
|
|
30244
30241
|
},
|
|
30245
30242
|
json,
|
|
@@ -31128,7 +31125,7 @@ const recognizeWrite = (repo, script) => {
|
|
|
31128
31125
|
const [, content, to] = words$1(lines[3]);
|
|
31129
31126
|
const remove = words$1(lines.find((l) => l.startsWith("rm -rf -- ")) ?? "");
|
|
31130
31127
|
if (path === void 0 || content === void 0 || to !== path) return void 0;
|
|
31131
|
-
if (writeScript(path, content, remove) !== script) return void 0;
|
|
31128
|
+
if (writeScript$1(path, content, remove) !== script) return void 0;
|
|
31132
31129
|
repo.writeFile(path, content);
|
|
31133
31130
|
removeAll(repo, remove);
|
|
31134
31131
|
return ok();
|
|
@@ -31765,7 +31762,10 @@ const recognizeGtdCheck = (repo, block) => {
|
|
|
31765
31762
|
error: `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`
|
|
31766
31763
|
};
|
|
31767
31764
|
const content = repo.readFile(file);
|
|
31768
|
-
if (content === void 0) return {
|
|
31765
|
+
if (content === void 0) return {
|
|
31766
|
+
kind: "failed",
|
|
31767
|
+
error: `gtd check: no such file: ${file}`
|
|
31768
|
+
};
|
|
31769
31769
|
const findings = format.validate(content).map((finding) => formatFinding(file, finding));
|
|
31770
31770
|
if (findings.length === 0) return { kind: "noop" };
|
|
31771
31771
|
return {
|
|
@@ -31788,12 +31788,10 @@ const recognizeGtdUncheck = (repo, block) => {
|
|
|
31788
31788
|
return { kind: "noop" };
|
|
31789
31789
|
};
|
|
31790
31790
|
/**
|
|
31791
|
-
*
|
|
31792
|
-
*
|
|
31793
|
-
* script, so recognizing it as a no-op (like the retry-helper function
|
|
31794
|
-
* definition) keeps the `@inmem` tier green.
|
|
31791
|
+
* A block of nothing but comments is a no-op in `sh` too — `combinedScript`'s
|
|
31792
|
+
* leading `DID_NOT_RUN_COMMENT`, and the list of paths a start commits.
|
|
31795
31793
|
*/
|
|
31796
|
-
const
|
|
31794
|
+
const recognizeCommentBlock = (block) => block.split("\n").every((line) => line.startsWith("#")) ? { kind: "noop" } : void 0;
|
|
31797
31795
|
/** `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
31796
|
const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0].replace(/\\n$/, "")}\n`;
|
|
31799
31797
|
const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`;
|
|
@@ -31907,7 +31905,7 @@ const recognizeWriteLines = (repo, block) => {
|
|
|
31907
31905
|
/** 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
31906
|
const recognizersFor = (repo, commands, git) => [
|
|
31909
31907
|
(block) => recognizeSetFlags(block),
|
|
31910
|
-
(block) =>
|
|
31908
|
+
(block) => recognizeCommentBlock(block),
|
|
31911
31909
|
(block) => recognizePrecondition(repo, block),
|
|
31912
31910
|
(block) => recognizeFileExistsGuard(repo, block),
|
|
31913
31911
|
(block) => recognizeBinaryGuard(commands, block),
|
|
@@ -31985,6 +31983,52 @@ const unrecognized = (block, output = "") => ({
|
|
|
31985
31983
|
});
|
|
31986
31984
|
Narrator.layer(() => {}, false);
|
|
31987
31985
|
//#endregion
|
|
31986
|
+
//#region src/testkit/imports.ts
|
|
31987
|
+
const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
|
|
31988
|
+
const STATEMENT = /^[ \t]*(import|export)([ \t]+type)?[ \t]+([\w$* \t,]*(?:\{[^}]*\})?[\w$* \t,]*?)\s*\bfrom\s*(["'])([^"'\n]+)\4[ \t]*;?|^[ \t]*import\s*(["'])([^"'\n]+)\6[ \t]*;?/gm;
|
|
31989
|
+
const bindings = (list) => list.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith("type ")).map((entry) => {
|
|
31990
|
+
const [name, as] = entry.split(/\s+as\s+/);
|
|
31991
|
+
return {
|
|
31992
|
+
name,
|
|
31993
|
+
as: as ?? name
|
|
31994
|
+
};
|
|
31995
|
+
});
|
|
31996
|
+
const exportLine = (as, value) => as === "default" ? `export default ${value}` : `export const ${as} = ${value}`;
|
|
31997
|
+
const exportLines = (module, m, read, head, braces) => {
|
|
31998
|
+
if (braces !== void 0) return bindings(braces).map((b) => exportLine(b.as, read(b.name)));
|
|
31999
|
+
if (/^\*\s+as\s+/.test(head)) return [exportLine(head.replace(/^\*\s+as\s+/, ""), m)];
|
|
32000
|
+
return Object.keys(module).filter((name) => name !== "default" && IDENTIFIER$1.test(name)).map((name) => exportLine(name, read(name)));
|
|
32001
|
+
};
|
|
32002
|
+
const importLines = (m, read, head, braces) => [...head.split(",").map((part) => part.trim()).filter((part) => part !== "").map((part) => part.startsWith("*") ? `const ${part.replace(/^\*\s+as\s+/, "")} = ${m}` : `const ${part} = ${read("default")}`), ...braces === void 0 ? [] : bindings(braces).map((b) => `const ${b.as} = ${read(b.name)}`)];
|
|
32003
|
+
/**
|
|
32004
|
+
* `source` with each import or re-export of a specifier in `modules` reading
|
|
32005
|
+
* that module off `lookup(specifier)`, an expression evaluated when the
|
|
32006
|
+
* config runs. A string config is evaluated in memory, where no resolver
|
|
32007
|
+
* knows the specifier, so the binding is made by hand.
|
|
32008
|
+
*/
|
|
32009
|
+
const importsFrom = (source, modules, lookup) => {
|
|
32010
|
+
let n = 0;
|
|
32011
|
+
const rewrite = (kind, clause, specifier, module) => {
|
|
32012
|
+
const m = `__testkit_module_${++n}`;
|
|
32013
|
+
const read = (name) => {
|
|
32014
|
+
if (!(name in module)) throw new Error(`testkit: module "${specifier}" has no export "${name}"`);
|
|
32015
|
+
return `${m}[${JSON.stringify(name)}]`;
|
|
32016
|
+
};
|
|
32017
|
+
const text = clause.trim();
|
|
32018
|
+
const braces = /\{([^}]*)\}/.exec(text)?.[1];
|
|
32019
|
+
const head = text.replace(/\{[^}]*\}/, "").replace(/,\s*$/, "").trim();
|
|
32020
|
+
const lines = kind === "export" ? exportLines(module, m, read, head, braces) : importLines(m, read, head, braces);
|
|
32021
|
+
return [`const ${m} = ${lookup(specifier)}`, ...lines].join("\n");
|
|
32022
|
+
};
|
|
32023
|
+
return source.replace(STATEMENT, (statement, kind, typeOnly, clause, _q, from, _bq, bare) => {
|
|
32024
|
+
const specifier = from ?? bare;
|
|
32025
|
+
const module = modules[specifier];
|
|
32026
|
+
if (module === void 0) return statement;
|
|
32027
|
+
if (typeOnly !== void 0 || bare !== void 0) return "";
|
|
32028
|
+
return rewrite(kind, clause, specifier, module);
|
|
32029
|
+
});
|
|
32030
|
+
};
|
|
32031
|
+
//#endregion
|
|
31988
32032
|
//#region src/testkit/TestRepo.ts
|
|
31989
32033
|
const REGISTRY = Symbol.for("@pmelab/gtd/testkit:modules");
|
|
31990
32034
|
const registry = () => {
|
|
@@ -31993,9 +32037,10 @@ const registry = () => {
|
|
|
31993
32037
|
};
|
|
31994
32038
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
31995
32039
|
let registered = 0;
|
|
32040
|
+
const lookup = (key) => `globalThis[Symbol.for(${JSON.stringify(REGISTRY.description)})].get(${JSON.stringify(key)})`;
|
|
31996
32041
|
/** A config whose exports are module `id`'s, read off the registry when the engine loads it. */
|
|
31997
32042
|
const moduleSource = (id, module) => {
|
|
31998
|
-
const lines = [`const m =
|
|
32043
|
+
const lines = [`const m = ${lookup(id)}`];
|
|
31999
32044
|
for (const name of Object.keys(module)) if (name === "default") lines.push(`export default m.default`);
|
|
32000
32045
|
else if (IDENTIFIER.test(name)) lines.push(`export const ${name} = m[${JSON.stringify(name)}]`);
|
|
32001
32046
|
else throw new Error(`testkit: export name "${name}" is not an identifier`);
|
|
@@ -32003,6 +32048,48 @@ const moduleSource = (id, module) => {
|
|
|
32003
32048
|
};
|
|
32004
32049
|
/** An actor that runs the step's own script, for steps whose script `runScript` understands. */
|
|
32005
32050
|
const runsItsScript = (beat, repo) => repo.runScript(beat.content);
|
|
32051
|
+
/**
|
|
32052
|
+
* A judge's verdict, keyed by question id: `verdict({ ready: true })`, or with
|
|
32053
|
+
* a confidence, `verdict({ ready: { answer: true, p: 0.6 } })`. `p` defaults to 0.9.
|
|
32054
|
+
*/
|
|
32055
|
+
const verdict = (answers) => ({ answers: Object.entries(answers).map(([id, entry]) => typeof entry === "object" ? {
|
|
32056
|
+
id,
|
|
32057
|
+
answer: entry.answer,
|
|
32058
|
+
p: entry.p ?? .9
|
|
32059
|
+
} : {
|
|
32060
|
+
id,
|
|
32061
|
+
answer: entry,
|
|
32062
|
+
p: .9
|
|
32063
|
+
}) });
|
|
32064
|
+
/** `actor`, at a visit whose beat content contains `text`; any other visit changes nothing. */
|
|
32065
|
+
const whenPrompted = (text, actor) => (beat, repo) => beat.content.includes(text) ? actor(beat, repo) : void 0;
|
|
32066
|
+
/** Each actor in order, at every visit; the first verdict any of them returns is the result. */
|
|
32067
|
+
const inTurn = (actors) => async (beat, repo) => {
|
|
32068
|
+
let result;
|
|
32069
|
+
for (const actor of actors) {
|
|
32070
|
+
const returned = await actor(beat, repo);
|
|
32071
|
+
if (result === void 0 && returned !== void 0) result = returned;
|
|
32072
|
+
}
|
|
32073
|
+
return result;
|
|
32074
|
+
};
|
|
32075
|
+
/** At a judge step, `answer` when the pending questions (as `gtd judge` prints them) contain `text`. */
|
|
32076
|
+
const verdictWhen = (text, answer) => async (_, repo) => {
|
|
32077
|
+
const { stdout } = await repo.gtd("judge");
|
|
32078
|
+
return stdout.includes(text) ? answer : void 0;
|
|
32079
|
+
};
|
|
32080
|
+
const isVerdict = (value) => typeof value === "object" && value !== null && Array.isArray(value.answers);
|
|
32081
|
+
const TRAILER = /^([A-Za-z0-9][A-Za-z0-9-]*): ?(.*)$/;
|
|
32082
|
+
const trailersOf = (message) => {
|
|
32083
|
+
const paragraphs = message.trimEnd().split(/\n\s*\n/);
|
|
32084
|
+
const last = paragraphs.length > 1 ? paragraphs.at(-1).split("\n") : [];
|
|
32085
|
+
const trailers = {};
|
|
32086
|
+
if (last.length === 0 || !last.every((line) => TRAILER.test(line))) return trailers;
|
|
32087
|
+
for (const line of last) {
|
|
32088
|
+
const [, key, value] = TRAILER.exec(line);
|
|
32089
|
+
(trailers[key] ??= []).push(value);
|
|
32090
|
+
}
|
|
32091
|
+
return trailers;
|
|
32092
|
+
};
|
|
32006
32093
|
const words = (text) => [...text.matchAll(/'((?:[^']|'\\'')*)'/g)].map((m) => m[1].replaceAll("'\\''", "'"));
|
|
32007
32094
|
const fileOf = (repo, path, what) => {
|
|
32008
32095
|
const content = repo.readFile(path);
|
|
@@ -32047,17 +32134,61 @@ const runLines = (repo, commands, script) => {
|
|
|
32047
32134
|
if (!applied.ok) throw cannotRun(applied.error ?? "");
|
|
32048
32135
|
};
|
|
32049
32136
|
const failed = (what, result) => /* @__PURE__ */ new Error(`${what} exited ${result.exitCode}\n${result.stderr}${result.stdout}`);
|
|
32137
|
+
const ANSWER = {
|
|
32138
|
+
noul: "true",
|
|
32139
|
+
choice: "\"<option>\"",
|
|
32140
|
+
score: "0.5"
|
|
32141
|
+
};
|
|
32142
|
+
/** The verdict a judge beat's questions take, as a template to copy. */
|
|
32143
|
+
const expectedVerdict = (doc) => {
|
|
32144
|
+
let questions = [];
|
|
32145
|
+
try {
|
|
32146
|
+
questions = JSON.parse(doc.judge ?? "").questions;
|
|
32147
|
+
} catch {}
|
|
32148
|
+
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");
|
|
32149
|
+
};
|
|
32150
|
+
const quoted = (names) => names.map((name) => `"${name}"`).join(", ");
|
|
32151
|
+
/** Who acts at `doc`: its scripted actor, or a check's own script under `checks: "run"`. A stall has none. */
|
|
32152
|
+
const actorAt = (doc, actors, checks) => {
|
|
32153
|
+
if (doc.kind === "stalled") return {
|
|
32154
|
+
scripted: false,
|
|
32155
|
+
actor: void 0
|
|
32156
|
+
};
|
|
32157
|
+
if (Object.hasOwn(actors, doc.state)) return {
|
|
32158
|
+
scripted: true,
|
|
32159
|
+
actor: actors[doc.state]
|
|
32160
|
+
};
|
|
32161
|
+
return {
|
|
32162
|
+
scripted: false,
|
|
32163
|
+
actor: checks === "run" && doc.actor === "check" ? runsItsScript : void 0
|
|
32164
|
+
};
|
|
32165
|
+
};
|
|
32166
|
+
/** A strict drive's rest: refused at an unscripted check step, or with an actor no drive reached. */
|
|
32167
|
+
const strictRest = (doc, actors, reached) => {
|
|
32168
|
+
if (doc.kind !== "stalled" && doc.actor === "check") throw new Error(`strict drive: check step "${doc.state}" has no actor — script it, or drive with checks: "run"`);
|
|
32169
|
+
const never = reached === void 0 ? [] : Object.keys(actors).filter((step) => !reached.has(step));
|
|
32170
|
+
if (never.length > 0) throw new Error(`strict drive: the process rests at "${doc.state}", and never reached the actors for ${quoted(never)} — script a step only for the drive that reaches it`);
|
|
32171
|
+
return doc;
|
|
32172
|
+
};
|
|
32173
|
+
const refuseVerdict = (doc, returned) => {
|
|
32174
|
+
if (doc.actor !== "judge" && returned !== void 0) throw new Error(`strict drive: the actor for ${doc.actor} step "${doc.state}" returned a verdict; only a judge step takes one`);
|
|
32175
|
+
};
|
|
32176
|
+
const refuseNothingLanded = (doc, nothing) => {
|
|
32177
|
+
if (doc.actor === "human" && nothing) throw new Error(`strict drive: the actor for human step "${doc.state}" changed nothing, so nothing landed and the drive would repeat the step`);
|
|
32178
|
+
};
|
|
32050
32179
|
const testRepo = (options) => {
|
|
32051
32180
|
const repo = new InMemRepo();
|
|
32052
32181
|
const env = { ...options.env };
|
|
32053
32182
|
const { workflow } = options;
|
|
32054
32183
|
const id = `module-${++registered}`;
|
|
32055
|
-
|
|
32184
|
+
const modules = Object.entries(options.modules ?? {});
|
|
32185
|
+
const moduleKey = (specifier) => `${id}:${specifier}`;
|
|
32186
|
+
repo.writeFile("gtd.config.ts", typeof workflow !== "string" ? moduleSource(id, workflow) : modules.length === 0 ? workflow : importsFrom(workflow, options.modules, (specifier) => lookup(moduleKey(specifier))));
|
|
32056
32187
|
for (const [path, content] of Object.entries(options.files ?? {})) repo.writeFile(path, content);
|
|
32057
32188
|
repo.seedCommit("chore: initial commit");
|
|
32058
32189
|
const canned = [...options.commands ?? []];
|
|
32059
|
-
const
|
|
32060
|
-
const hit = canned.find(({ run }) => typeof run === "string" ? run === command : run.test(command));
|
|
32190
|
+
const table = (first = []) => ({ get: (command) => {
|
|
32191
|
+
const hit = [...first, ...canned].find(({ run }) => typeof run === "string" ? run === command : run.test(command));
|
|
32061
32192
|
if (hit === void 0) return void 0;
|
|
32062
32193
|
const { exit = 0, output = "", effect } = hit;
|
|
32063
32194
|
return {
|
|
@@ -32066,10 +32197,16 @@ const testRepo = (options) => {
|
|
|
32066
32197
|
output,
|
|
32067
32198
|
effect: () => effect?.(self)
|
|
32068
32199
|
};
|
|
32069
|
-
} };
|
|
32200
|
+
} });
|
|
32201
|
+
const commands = table();
|
|
32202
|
+
const seenEnv = () => options.ambientFlags === false ? {
|
|
32203
|
+
...Object.fromEntries(Object.keys(process.env).filter((name) => name.startsWith("GTD_FLAG_")).map((name) => [name, void 0])),
|
|
32204
|
+
...env
|
|
32205
|
+
} : env;
|
|
32070
32206
|
const gtd = async (...args) => {
|
|
32071
|
-
const { io, result } = makeCapturingCliIo(repo,
|
|
32207
|
+
const { io, result } = makeCapturingCliIo(repo, seenEnv());
|
|
32072
32208
|
if (typeof workflow !== "string") registry().set(id, workflow);
|
|
32209
|
+
for (const [specifier, module] of modules) registry().set(moduleKey(specifier), module);
|
|
32073
32210
|
try {
|
|
32074
32211
|
await runPromise(runCli([
|
|
32075
32212
|
"node",
|
|
@@ -32078,6 +32215,7 @@ const testRepo = (options) => {
|
|
|
32078
32215
|
], io));
|
|
32079
32216
|
} finally {
|
|
32080
32217
|
registry().delete(id);
|
|
32218
|
+
for (const [specifier] of modules) registry().delete(moduleKey(specifier));
|
|
32081
32219
|
}
|
|
32082
32220
|
return result();
|
|
32083
32221
|
};
|
|
@@ -32101,10 +32239,10 @@ const testRepo = (options) => {
|
|
|
32101
32239
|
if (!applied.ok) throw new Error(`${what}: its script failed — ${applied.error ?? ""}`);
|
|
32102
32240
|
};
|
|
32103
32241
|
/** Runs a write command, then the script its `--json=script` twin prints. */
|
|
32104
|
-
const runWrite = async (args, stdin) => {
|
|
32242
|
+
const runWrite = async (args, stdin, refused = (error) => error) => {
|
|
32105
32243
|
const call = (...extra) => stdin === void 0 ? gtd(...args, ...extra) : withStdin(stdin, () => gtd(...args, ...extra));
|
|
32106
32244
|
const reported = await call();
|
|
32107
|
-
if (reported.exitCode !== 0) throw failed(`gtd ${args.join(" ")}`, reported);
|
|
32245
|
+
if (reported.exitCode !== 0) throw refused(failed(`gtd ${args.join(" ")}`, reported));
|
|
32108
32246
|
const script = await call("--json=script");
|
|
32109
32247
|
apply(`gtd ${args.join(" ")}`, script.stdout);
|
|
32110
32248
|
return reported;
|
|
@@ -32121,19 +32259,42 @@ const testRepo = (options) => {
|
|
|
32121
32259
|
return JSON.parse(result.stdout);
|
|
32122
32260
|
};
|
|
32123
32261
|
/** Lands what an actor did: a judge's verdict answered, anything else validated and landed. */
|
|
32124
|
-
const act = async (doc,
|
|
32262
|
+
const act = async (doc, returned) => {
|
|
32125
32263
|
if (doc.actor !== "judge") {
|
|
32126
32264
|
if (doc.validate !== void 0) apply(`validating ${doc.file ?? doc.state}`, doc.validate);
|
|
32127
32265
|
await runWrite(["land"]);
|
|
32128
32266
|
return;
|
|
32129
32267
|
}
|
|
32130
|
-
|
|
32131
|
-
|
|
32268
|
+
const actor = `the actor for judge step "${doc.state}"`;
|
|
32269
|
+
if (returned === void 0) throw new Error(`${actor} returned no verdict\n${expectedVerdict(doc)}`);
|
|
32270
|
+
const sent = isVerdict(returned) ? JSON.stringify(returned.answers) : returned;
|
|
32271
|
+
if (typeof sent !== "string") throw new Error(`${actor} returned ${Array.isArray(sent) ? "an array" : typeof sent === "object" ? "an object" : `a ${typeof sent}`}, not a JSON string — JSON.stringify it\n${expectedVerdict(doc)}`);
|
|
32272
|
+
await runWrite(["judge", "answer"], sent, (error) => /* @__PURE__ */ new Error(`${actor} sent a verdict gtd refused:\n${sent}\n${expectedVerdict(doc)}\n${error.message}`));
|
|
32273
|
+
};
|
|
32274
|
+
const reached = /* @__PURE__ */ new Set();
|
|
32275
|
+
/** One beat of a drive: the beat it rests at, or `undefined` to read the next. */
|
|
32276
|
+
const beat = async (doc, actors, { strict = true, checks, unreachedActors = "fail" }) => {
|
|
32277
|
+
if (doc.kind === "capture") {
|
|
32278
|
+
await runWrite(["land"]);
|
|
32279
|
+
return;
|
|
32280
|
+
}
|
|
32281
|
+
const { scripted, actor } = actorAt(doc, actors, checks);
|
|
32282
|
+
if (actor === void 0) return strict ? strictRest(doc, actors, unreachedActors === "fail" ? reached : void 0) : doc;
|
|
32283
|
+
if (scripted) reached.add(doc.state);
|
|
32284
|
+
const returned = await actor(doc, self);
|
|
32285
|
+
if (strict) refuseVerdict(doc, returned);
|
|
32286
|
+
const before = repo.commitHistory().length;
|
|
32287
|
+
await act(doc, returned);
|
|
32288
|
+
if (strict) refuseNothingLanded(doc, repo.commitHistory().length === before);
|
|
32132
32289
|
};
|
|
32133
32290
|
const self = {
|
|
32134
32291
|
command: (command) => {
|
|
32135
32292
|
canned.push(command);
|
|
32136
32293
|
},
|
|
32294
|
+
setEnv: (name, value) => {
|
|
32295
|
+
if (value === void 0) delete env[name];
|
|
32296
|
+
else env[name] = value;
|
|
32297
|
+
},
|
|
32137
32298
|
branch: (name, ref = "HEAD") => repo.createBranch(name, ref),
|
|
32138
32299
|
checkout: (name) => repo.checkoutBranch(name),
|
|
32139
32300
|
ref: (name, ref = "HEAD") => {
|
|
@@ -32148,43 +32309,47 @@ const testRepo = (options) => {
|
|
|
32148
32309
|
remove: (path) => repo.deleteFile(path),
|
|
32149
32310
|
commit: (message) => repo.seedCommit(message),
|
|
32150
32311
|
show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
|
|
32151
|
-
runScript: (script) => {
|
|
32152
|
-
const
|
|
32153
|
-
|
|
32312
|
+
runScript: (script, { commands: first = [] } = {}) => {
|
|
32313
|
+
const scriptCommands = first.length === 0 ? commands : table(first);
|
|
32314
|
+
const flow = applyFlowScript(repo, scriptCommands, script);
|
|
32315
|
+
if (flow === void 0) runLines(repo, scriptCommands, script);
|
|
32154
32316
|
else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`);
|
|
32155
32317
|
},
|
|
32156
32318
|
gtd,
|
|
32157
|
-
start: (
|
|
32158
|
-
"
|
|
32159
|
-
|
|
32319
|
+
start: (name, ...args) => begin([
|
|
32320
|
+
"start",
|
|
32321
|
+
name,
|
|
32160
32322
|
...args
|
|
32161
32323
|
]),
|
|
32162
32324
|
door: (name, ...args) => begin([
|
|
32163
|
-
"
|
|
32325
|
+
"start",
|
|
32164
32326
|
name,
|
|
32165
32327
|
...args
|
|
32166
32328
|
]),
|
|
32167
32329
|
land: () => runWrite(["land"]),
|
|
32168
32330
|
next,
|
|
32169
|
-
drive: async (actors = {},
|
|
32170
|
-
|
|
32331
|
+
drive: async (actors = {}, options = {}) => {
|
|
32332
|
+
const { maxBeats = 50 } = options;
|
|
32333
|
+
const passed = [];
|
|
32334
|
+
for (let n = 0; n < maxBeats; n++) {
|
|
32171
32335
|
const doc = await next();
|
|
32172
|
-
|
|
32173
|
-
|
|
32174
|
-
|
|
32175
|
-
continue;
|
|
32176
|
-
}
|
|
32177
|
-
const actor = actors[doc.state];
|
|
32178
|
-
if (actor === void 0) return doc;
|
|
32179
|
-
await act(doc, await actor(doc, self));
|
|
32336
|
+
passed.push(doc.state);
|
|
32337
|
+
const rest = await beat(doc, actors, options);
|
|
32338
|
+
if (rest !== void 0) return rest;
|
|
32180
32339
|
}
|
|
32181
|
-
throw new Error(`the process did not rest within ${maxBeats} beats`);
|
|
32340
|
+
throw new Error(`the process did not rest within ${maxBeats} beats; it passed through:\n${passed.join(" → ")}`);
|
|
32182
32341
|
},
|
|
32183
32342
|
subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
|
|
32184
32343
|
messages: () => repo.commitHistory().map(({ message }) => message),
|
|
32185
|
-
hashes: () => repo.commitHistory().map(({ hash }) => hash)
|
|
32344
|
+
hashes: () => repo.commitHistory().map(({ hash }) => hash),
|
|
32345
|
+
commits: () => repo.commitHistory().map(({ hash, message }) => ({
|
|
32346
|
+
hash,
|
|
32347
|
+
subject: message.split("\n")[0] ?? "",
|
|
32348
|
+
message,
|
|
32349
|
+
trailers: trailersOf(message)
|
|
32350
|
+
}))
|
|
32186
32351
|
};
|
|
32187
32352
|
return self;
|
|
32188
32353
|
};
|
|
32189
32354
|
//#endregion
|
|
32190
|
-
export {
|
|
32355
|
+
export { verdictWhen as a, verdict as i, runsItsScript as n, whenPrompted as o, testRepo as r, inTurn as t };
|