@pmelab/gtd 59.0.1 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +37 -33
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +2 -6
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +169 -146
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
|
@@ -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) => ({
|
|
@@ -25762,8 +25753,8 @@ const refuseLegacyRc = gen(function* () {
|
|
|
25762
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]));
|
|
@@ -25802,16 +25793,15 @@ const load = gen(function* () {
|
|
|
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"
|
|
25976
25975
|
]);
|
|
25977
|
-
/**
|
|
25976
|
+
/** Names that are never a workflow. */
|
|
25977
|
+
const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
|
|
25978
|
+
"default",
|
|
25979
|
+
"config",
|
|
25980
|
+
...RETIRED_EXPORTS
|
|
25981
|
+
]);
|
|
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`. */
|
|
@@ -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
|
|
@@ -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
|
*/
|
|
@@ -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,19 +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
28856
|
if (resolved === void 0) {
|
|
28839
28857
|
const startable = config.workflowNames.length > 0 ? `startable: ${config.workflowNames.join(", ")}` : "this config exports no workflow besides its default, which an ordinary start runs";
|
|
28840
|
-
return yield* fail$3(new GtdUsageError(
|
|
28858
|
+
return yield* fail$3(new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`));
|
|
28841
28859
|
}
|
|
28842
28860
|
const rest = yield* restAt(void 0, varOverrides);
|
|
28843
28861
|
const started = yield* startRefusal(rest, resolved, varOverrides);
|
|
28844
28862
|
const plan = yield* planStart({
|
|
28845
28863
|
state: rest.state,
|
|
28846
28864
|
idle: noProcessUnderway(rest)
|
|
28847
|
-
},
|
|
28865
|
+
}, "human", {
|
|
28848
28866
|
workflow: resolved,
|
|
28849
28867
|
first: started,
|
|
28850
28868
|
commandLabel,
|
|
@@ -28852,7 +28870,7 @@ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => ge
|
|
|
28852
28870
|
config: rest.config
|
|
28853
28871
|
});
|
|
28854
28872
|
if (plan.kind === "refusal") return yield* fail$3(new Error(plan.message));
|
|
28855
|
-
out
|
|
28873
|
+
yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
|
|
28856
28874
|
});
|
|
28857
28875
|
/**
|
|
28858
28876
|
* A start commits the whole tree (`git add -A`), so its script says up front
|
|
@@ -28866,50 +28884,65 @@ const listingCommitted = (script, changes) => {
|
|
|
28866
28884
|
return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`;
|
|
28867
28885
|
};
|
|
28868
28886
|
const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
|
|
28869
|
-
|
|
28870
|
-
|
|
28871
|
-
|
|
28872
|
-
};
|
|
28873
|
-
/**
|
|
28874
|
-
* `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
|
|
28875
|
-
* process settings its `vars` derive from the positional args. A wrong name or
|
|
28876
|
-
* arity is a usage error naming what would have worked.
|
|
28877
|
-
*/
|
|
28878
|
-
const runDoorCommand = (name, args, out) => gen(function* () {
|
|
28879
|
-
const config = yield* (yield* ConfigService).load;
|
|
28880
|
-
const door = name === void 0 ? void 0 : config.doors.get(name);
|
|
28881
|
-
if (name === void 0 || door === void 0) return yield* fail$3(noSuchDoor(name, config.doors));
|
|
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}`;
|
|
28882
28890
|
const declared = door.args ?? [];
|
|
28883
28891
|
const required = declared.filter((arg) => arg.optional !== true).length;
|
|
28884
|
-
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)}`));
|
|
28885
28893
|
const named = {};
|
|
28886
28894
|
declared.forEach((arg, i) => {
|
|
28887
28895
|
named[arg.name] = args[i];
|
|
28888
28896
|
});
|
|
28889
28897
|
const vars = yield* try_({
|
|
28890
28898
|
try: () => door.vars?.(named) ?? {},
|
|
28891
|
-
catch: (e) => /* @__PURE__ */ new Error(
|
|
28899
|
+
catch: (e) => /* @__PURE__ */ new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`)
|
|
28892
28900
|
});
|
|
28893
|
-
yield*
|
|
28901
|
+
yield* startWorkflow(door.workflow, {
|
|
28902
|
+
...vars,
|
|
28903
|
+
...extra
|
|
28904
|
+
}, json, out, label);
|
|
28894
28905
|
});
|
|
28895
|
-
/**
|
|
28896
|
-
|
|
28897
|
-
|
|
28898
|
-
|
|
28899
|
-
|
|
28900
|
-
|
|
28901
|
-
|
|
28902
|
-
|
|
28903
|
-
const
|
|
28904
|
-
|
|
28905
|
-
|
|
28906
|
-
|
|
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) => ({
|
|
28907
28939
|
name: arg.name,
|
|
28908
28940
|
optional: arg.optional === true
|
|
28909
28941
|
}))
|
|
28910
|
-
}));
|
|
28911
|
-
if (json
|
|
28912
|
-
|
|
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(""));
|
|
28913
28946
|
});
|
|
28914
28947
|
/**
|
|
28915
28948
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
@@ -28924,7 +28957,7 @@ const runDoorsCommand = (json, out) => gen(function* () {
|
|
|
28924
28957
|
* Idempotent at the initial state. The one refusal is a process whose first
|
|
28925
28958
|
* commit is the repository's root commit: nothing earlier to rewind to.
|
|
28926
28959
|
*/
|
|
28927
|
-
const runAbandonCommand = (out) => gen(function* () {
|
|
28960
|
+
const runAbandonCommand = (json, out) => gen(function* () {
|
|
28928
28961
|
const git = yield* GitService;
|
|
28929
28962
|
const initial = (yield* (yield* ConfigService).load).workflow.initial;
|
|
28930
28963
|
const run = yield* currentRun;
|
|
@@ -28933,8 +28966,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28933
28966
|
kind: "outcome",
|
|
28934
28967
|
command: abandonNoopOutcome(initial)
|
|
28935
28968
|
}]).required;
|
|
28936
|
-
out
|
|
28937
|
-
return;
|
|
28969
|
+
return yield* writeScript(json, out, combinedScript(required, ""));
|
|
28938
28970
|
}
|
|
28939
28971
|
const restState = run.trace[run.trace.length - 1].state;
|
|
28940
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`));
|
|
@@ -28952,7 +28984,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28952
28984
|
command: abandonedOutcome(restState, run.startParentHash, initial)
|
|
28953
28985
|
}
|
|
28954
28986
|
]).required;
|
|
28955
|
-
out
|
|
28987
|
+
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28956
28988
|
});
|
|
28957
28989
|
/**
|
|
28958
28990
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
@@ -28961,7 +28993,7 @@ const runAbandonCommand = (out) => gen(function* () {
|
|
|
28961
28993
|
* refuses on a dirty working tree, no retained history, or HEAD having
|
|
28962
28994
|
* advanced past the retained tip with commits that would be lost.
|
|
28963
28995
|
*/
|
|
28964
|
-
const runRestoreCommand = (out) => gen(function* () {
|
|
28996
|
+
const runRestoreCommand = (json, out) => gen(function* () {
|
|
28965
28997
|
const git = yield* GitService;
|
|
28966
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."));
|
|
28967
28999
|
const retained = yield* readRetainedHistory(git);
|
|
@@ -28985,7 +29017,7 @@ const runRestoreCommand = (out) => gen(function* () {
|
|
|
28985
29017
|
command: restoredOutcome(tip, after.state)
|
|
28986
29018
|
}
|
|
28987
29019
|
]).required;
|
|
28988
|
-
out
|
|
29020
|
+
yield* writeScript(json, out, combinedScript(required, ""));
|
|
28989
29021
|
});
|
|
28990
29022
|
/**
|
|
28991
29023
|
* The mode's own validate command for `file`: a declared `validate:` command
|
|
@@ -29055,22 +29087,24 @@ const runNextCommand = (json, out) => gen(function* () {
|
|
|
29055
29087
|
}
|
|
29056
29088
|
});
|
|
29057
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. */
|
|
29058
|
-
const runValidateCommand = (out) => gen(function* () {
|
|
29090
|
+
const runValidateCommand = (json, out) => gen(function* () {
|
|
29059
29091
|
const rest = yield* currentRest;
|
|
29060
|
-
const
|
|
29061
|
-
|
|
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`);
|
|
29062
29094
|
});
|
|
29063
29095
|
/**
|
|
29064
29096
|
* Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
|
|
29065
29097
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
29066
29098
|
* `validate:` command invokes as a leaf step, and it formats nothing.
|
|
29067
29099
|
*
|
|
29068
|
-
* An
|
|
29069
|
-
*
|
|
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.
|
|
29070
29104
|
*/
|
|
29071
29105
|
const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(function* () {
|
|
29072
29106
|
const format = steeringFormatFor(mode);
|
|
29073
|
-
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(", ")}`));
|
|
29074
29108
|
if (openThreadsFlag) {
|
|
29075
29109
|
if (openQuestions) return yield* fail$3(/* @__PURE__ */ new Error("gtd check: --open-threads and --open-questions are mutually exclusive"));
|
|
29076
29110
|
if (format.openThreads === void 0) {
|
|
@@ -29083,11 +29117,15 @@ const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(func
|
|
|
29083
29117
|
if (mode !== "qa") return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: --open-questions only applies to mode "qa" — got "${mode}"`));
|
|
29084
29118
|
return yield* runOpenQuestionsCheckCommand(format, file);
|
|
29085
29119
|
}
|
|
29086
|
-
const content =
|
|
29087
|
-
const errors =
|
|
29120
|
+
const content = yield* existingFile(file);
|
|
29121
|
+
const errors = format.validate(content).map((finding) => formatFinding(file, finding));
|
|
29088
29122
|
if (errors.length === 0) return;
|
|
29089
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")));
|
|
29090
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
|
+
});
|
|
29091
29129
|
/**
|
|
29092
29130
|
* The bare `gtd check --open-threads`: code-comment threads in the files the
|
|
29093
29131
|
* current process changed. The one `check` form that reads process state; no
|
|
@@ -29105,23 +29143,18 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
|
|
|
29105
29143
|
if (lines.length === 0) return;
|
|
29106
29144
|
return yield* fail$3(new Error(lines.join("\n")));
|
|
29107
29145
|
});
|
|
29108
|
-
/** 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. */
|
|
29109
29147
|
const runOpenThreadsCheckCommand = (format, file) => gen(function* () {
|
|
29110
|
-
const
|
|
29111
|
-
if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
|
|
29112
|
-
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}`);
|
|
29113
29149
|
if (errors.length === 0) return;
|
|
29114
29150
|
return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open thread(s) in ${file}:\n` + errors.join("\n")));
|
|
29115
29151
|
});
|
|
29116
29152
|
/**
|
|
29117
29153
|
* Runs the SAME `unansweredQuestions` predicate a flow's `openQuestions()`
|
|
29118
|
-
* reads, so the gate script and the flow's own check stay in sync.
|
|
29119
|
-
* 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.
|
|
29120
29155
|
*/
|
|
29121
29156
|
const runOpenQuestionsCheckCommand = (format, file) => gen(function* () {
|
|
29122
|
-
const
|
|
29123
|
-
if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
|
|
29124
|
-
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}`);
|
|
29125
29158
|
if (errors.length === 0) return;
|
|
29126
29159
|
return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open question(s) unanswered in ${file}:\n` + errors.join("\n")));
|
|
29127
29160
|
});
|
|
@@ -29224,7 +29257,7 @@ const assertRepositoryHasCommits = (git) => gen(function* () {
|
|
|
29224
29257
|
const needsOf = (kind) => {
|
|
29225
29258
|
switch (kind) {
|
|
29226
29259
|
case "lsp":
|
|
29227
|
-
case "
|
|
29260
|
+
case "guide":
|
|
29228
29261
|
case "judgeRun":
|
|
29229
29262
|
case "strays": return "none";
|
|
29230
29263
|
case "check":
|
|
@@ -29250,18 +29283,16 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
29250
29283
|
...command.cost !== void 0 ? { cost: command.cost } : {},
|
|
29251
29284
|
...command.model !== void 0 ? { model: command.model } : {}
|
|
29252
29285
|
}, json, out);
|
|
29253
|
-
case "start": return runStartCommand(command
|
|
29254
|
-
case "
|
|
29255
|
-
case "
|
|
29256
|
-
case "abandon": return runAbandonCommand(out);
|
|
29257
|
-
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);
|
|
29258
29289
|
case "next": return runNextCommand(json, out);
|
|
29259
|
-
case "validate": return runValidateCommand(out);
|
|
29290
|
+
case "validate": return runValidateCommand(json, out);
|
|
29260
29291
|
case "check":
|
|
29261
29292
|
if (command.mode === void 0 || command.file === void 0) return runCodeThreadsCheckCommand();
|
|
29262
29293
|
return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false);
|
|
29263
29294
|
case "uncheck": return runUncheckCommand(command.file);
|
|
29264
|
-
case "
|
|
29295
|
+
case "guide": return runGuideCommand(out);
|
|
29265
29296
|
case "summary": return runSummaryCommand(out);
|
|
29266
29297
|
case "base": return runBaseCommand(out);
|
|
29267
29298
|
case "judge": return runJudgeCommand(json, out);
|
|
@@ -29271,7 +29302,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
29271
29302
|
case "historyShow": return runHistoryShowCommand(command.ref, json, out);
|
|
29272
29303
|
case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
|
|
29273
29304
|
case "historyJudges": return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out);
|
|
29274
|
-
case "historyFetch": return runHistoryFetchCommand(command.remote, out);
|
|
29305
|
+
case "historyFetch": return runHistoryFetchCommand(command.remote, json, out);
|
|
29275
29306
|
}
|
|
29276
29307
|
};
|
|
29277
29308
|
/**
|
|
@@ -29351,17 +29382,47 @@ const closest = (word, candidates) => {
|
|
|
29351
29382
|
};
|
|
29352
29383
|
//#endregion
|
|
29353
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
|
+
};
|
|
29354
29406
|
const nonNegativeNumber = (raw, flag) => {
|
|
29355
29407
|
const n = Number(raw);
|
|
29356
29408
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return left(`gtd: ${flag} must be a non-negative number — got "${raw}"`);
|
|
29357
29409
|
return right(n);
|
|
29358
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";
|
|
29359
29420
|
const JSON_KINDS = /* @__PURE__ */ new Set([
|
|
29421
|
+
...SCRIPT_KINDS,
|
|
29360
29422
|
"next",
|
|
29361
29423
|
"land",
|
|
29362
29424
|
"judge",
|
|
29363
29425
|
"judgeAnswer",
|
|
29364
|
-
"doors",
|
|
29365
29426
|
"historyList",
|
|
29366
29427
|
"historyShow",
|
|
29367
29428
|
"historyMetrics",
|
|
@@ -29375,10 +29436,14 @@ const FLAGS = [
|
|
|
29375
29436
|
repeatable: false,
|
|
29376
29437
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
29377
29438
|
decode: ([raw]) => right(raw ?? ""),
|
|
29378
|
-
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",
|
|
29379
29440
|
valueHint: "<path>",
|
|
29380
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",
|
|
29381
|
-
helpFor: {
|
|
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
|
+
}
|
|
29382
29447
|
},
|
|
29383
29448
|
{
|
|
29384
29449
|
name: "--port",
|
|
@@ -29459,58 +29524,33 @@ const FLAGS = [
|
|
|
29459
29524
|
repeatable: false,
|
|
29460
29525
|
scope: (kind) => kind === "land" || kind === "judgeRun",
|
|
29461
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),
|
|
29462
|
-
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)",
|
|
29463
29528
|
valueHint: "<name>",
|
|
29464
29529
|
help: "",
|
|
29465
29530
|
helpFor: {
|
|
29466
|
-
land: "With --cost: the model that cost was spent on",
|
|
29531
|
+
land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
|
|
29467
29532
|
judgeRun: "The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)"
|
|
29468
29533
|
}
|
|
29469
29534
|
},
|
|
29470
29535
|
{
|
|
29471
|
-
name: "--
|
|
29536
|
+
name: "--threshold",
|
|
29472
29537
|
arity: 1,
|
|
29473
|
-
repeatable:
|
|
29474
|
-
scope: (kind) => kind === "
|
|
29475
|
-
decode: (
|
|
29476
|
-
scopeError: "gtd: --
|
|
29477
|
-
valueHint: "<name>",
|
|
29478
|
-
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)"
|
|
29479
29544
|
},
|
|
29480
29545
|
{
|
|
29481
29546
|
name: "--var",
|
|
29482
29547
|
arity: 1,
|
|
29483
29548
|
repeatable: true,
|
|
29484
|
-
scope: (kind) => kind === "start"
|
|
29485
|
-
decode: (raws) =>
|
|
29486
|
-
|
|
29487
|
-
const seen = /* @__PURE__ */ new Set();
|
|
29488
|
-
for (const raw of raws) {
|
|
29489
|
-
const eq = raw.indexOf("=");
|
|
29490
|
-
if (eq <= 0) return left(`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`);
|
|
29491
|
-
const name = raw.slice(0, eq);
|
|
29492
|
-
const value = raw.slice(eq + 1);
|
|
29493
|
-
if (!isSettingName(name)) return left(`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
|
|
29494
|
-
if (/[\r\n]/.test(value)) return left(`gtd: --var ${name} must be a single-line value`);
|
|
29495
|
-
if (seen.has(name)) return left(`gtd: --var ${name} specified more than once`);
|
|
29496
|
-
seen.add(name);
|
|
29497
|
-
if (name.startsWith("flag_")) {
|
|
29498
|
-
const pinned = flagSettingValue(value);
|
|
29499
|
-
if (pinned === void 0) return left(`gtd: --var ${name} must be 1, 0, true or false`);
|
|
29500
|
-
vars[name] = pinned;
|
|
29501
|
-
continue;
|
|
29502
|
-
}
|
|
29503
|
-
vars[name] = value;
|
|
29504
|
-
}
|
|
29505
|
-
return right(vars);
|
|
29506
|
-
},
|
|
29507
|
-
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>`",
|
|
29508
29552
|
valueHint: "<name>=<value>",
|
|
29509
|
-
help: ""
|
|
29510
|
-
helpFor: {
|
|
29511
|
-
start: "Pin one of the workflow's declared settings for the new process (repeatable)",
|
|
29512
|
-
historyJudges: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)"
|
|
29513
|
-
}
|
|
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"
|
|
29514
29554
|
},
|
|
29515
29555
|
{
|
|
29516
29556
|
name: "--jsonl",
|
|
@@ -29521,7 +29561,7 @@ const FLAGS = [
|
|
|
29521
29561
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
29522
29562
|
valueHint: "",
|
|
29523
29563
|
help: "Print one JSON line per verdict instead of the report",
|
|
29524
|
-
conflicts: ["--json", "--
|
|
29564
|
+
conflicts: ["--json", "--threshold"]
|
|
29525
29565
|
},
|
|
29526
29566
|
{
|
|
29527
29567
|
name: "--open-questions",
|
|
@@ -29602,28 +29642,25 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
29602
29642
|
arity: "none",
|
|
29603
29643
|
group: "Core loop",
|
|
29604
29644
|
summary: "Print the script that checks the steering file",
|
|
29605
|
-
details: `Print the script that formats, then validates, the current step's steering file with its mode's commands. gtd runs nothing: the script's exit code is the verdict, and on a failure it prints a ready-to-send fix prompt. Prints "nothing to validate" when the step declares no file to check.`
|
|
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.`
|
|
29606
29646
|
},
|
|
29607
29647
|
{
|
|
29608
|
-
token: "
|
|
29609
|
-
kind: "
|
|
29648
|
+
token: "start",
|
|
29649
|
+
kind: "start",
|
|
29610
29650
|
arity: {
|
|
29611
29651
|
name: "name",
|
|
29612
|
-
variadic: "args"
|
|
29652
|
+
variadic: "args",
|
|
29653
|
+
bare: true
|
|
29613
29654
|
},
|
|
29614
29655
|
group: "Start and stop",
|
|
29615
|
-
summary: "Start a
|
|
29616
|
-
details: `Start a process
|
|
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.
|
|
29617
29658
|
|
|
29618
|
-
|
|
29619
|
-
|
|
29620
|
-
|
|
29621
|
-
|
|
29622
|
-
|
|
29623
|
-
arity: "none",
|
|
29624
|
-
group: "Start and stop",
|
|
29625
|
-
summary: "List the doors",
|
|
29626
|
-
details: `List every door, sorted: one \`name [args] → workflow\` line each. --json prints a JSON array of { name, workflow, args: [{ name, optional }] }. Writes nothing.`
|
|
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).`
|
|
29627
29664
|
},
|
|
29628
29665
|
{
|
|
29629
29666
|
token: "abandon",
|
|
@@ -29633,7 +29670,7 @@ With no <name>, an unknown one, or the wrong number of args, it lists the doors
|
|
|
29633
29670
|
summary: "Stop the process, keeping its changes",
|
|
29634
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.
|
|
29635
29672
|
|
|
29636
|
-
s=$(gtd abandon) && sh -c "$s"`
|
|
29673
|
+
s=$(gtd abandon --json=script) && sh -c "$s"`
|
|
29637
29674
|
},
|
|
29638
29675
|
{
|
|
29639
29676
|
token: "restore",
|
|
@@ -29723,7 +29760,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
29723
29760
|
summary: "Print a script that fetches finished processes",
|
|
29724
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:
|
|
29725
29762
|
|
|
29726
|
-
s=$(gtd history fetch) && sh -c "$s"`
|
|
29763
|
+
s=$(gtd history fetch --json=script) && sh -c "$s"`
|
|
29727
29764
|
},
|
|
29728
29765
|
{
|
|
29729
29766
|
token: "ui",
|
|
@@ -29734,8 +29771,8 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
29734
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.`
|
|
29735
29772
|
},
|
|
29736
29773
|
{
|
|
29737
|
-
token: "
|
|
29738
|
-
kind: "
|
|
29774
|
+
token: "guide",
|
|
29775
|
+
kind: "guide",
|
|
29739
29776
|
arity: "none",
|
|
29740
29777
|
group: "Driver plumbing",
|
|
29741
29778
|
summary: "Print a briefing for writing a driver",
|
|
@@ -29765,7 +29802,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
29765
29802
|
summary: "Answer a judgment with a model",
|
|
29766
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.
|
|
29767
29804
|
|
|
29768
|
-
The fixed answerer reads --answers and leaves out the questions it does not cover; jev asks TypeSafe ($TYPESAFE_API_KEY); llm asks the \`claude\` CLI on PATH. jev and llm exit 1, printing nothing, when they cannot answer every question.`
|
|
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).`
|
|
29769
29806
|
},
|
|
29770
29807
|
{
|
|
29771
29808
|
token: "check",
|
|
@@ -29773,7 +29810,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
|
|
|
29773
29810
|
arity: { names: ["mode", "file"] },
|
|
29774
29811
|
group: "Driver plumbing",
|
|
29775
29812
|
summary: "Check a steering file's format",
|
|
29776
|
-
details: `Run the steering format <mode> (qa, review or findings) over <file> and print each finding on a line of its own; exits 1 when there are any. Reads no workflow and runs from any directory — emitted validation scripts call it.`
|
|
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.`
|
|
29777
29814
|
},
|
|
29778
29815
|
{
|
|
29779
29816
|
token: "uncheck",
|
|
@@ -29805,25 +29842,23 @@ const synopsis = (row) => {
|
|
|
29805
29842
|
const { token, arity } = row;
|
|
29806
29843
|
if (arity === "none") return token;
|
|
29807
29844
|
if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`;
|
|
29845
|
+
if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`;
|
|
29808
29846
|
if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`;
|
|
29809
29847
|
if ("optional" in arity) return `${token} [${arity.name}]`;
|
|
29810
29848
|
if ("many" in arity) return `${token} [${arity.name}...]`;
|
|
29811
29849
|
return `${token} <${arity.name}>`;
|
|
29812
29850
|
};
|
|
29813
|
-
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--
|
|
29851
|
+
const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--var", "--threshold"]);
|
|
29814
29852
|
const flagHeader = (row) => {
|
|
29815
29853
|
if (row.valueHint === "") return row.name;
|
|
29816
29854
|
if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`;
|
|
29817
29855
|
return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
|
|
29818
29856
|
};
|
|
29819
|
-
const START_ROW = {
|
|
29820
|
-
term: "--workflow <name>",
|
|
29821
|
-
summary: "Start a process on a workflow; --var <name>=<value> pins one of its settings"
|
|
29822
|
-
};
|
|
29823
29857
|
const GLOBAL_OPTIONS = [
|
|
29824
29858
|
["-v, --verbose", "Narrate on stderr what gtd resolved"],
|
|
29825
29859
|
["-h, --help", "Print help; gtd <command> --help for one command"],
|
|
29826
|
-
["-V, --version", "Print the version"]
|
|
29860
|
+
["-V, --version", "Print the version"],
|
|
29861
|
+
["--", "End the options: every word after it is an argument"]
|
|
29827
29862
|
];
|
|
29828
29863
|
const EXTRA_TOPICS = [[
|
|
29829
29864
|
"help [command]",
|
|
@@ -29834,7 +29869,8 @@ const EXTRA_TOPICS = [[
|
|
|
29834
29869
|
"Print the version",
|
|
29835
29870
|
"Print gtd's version."
|
|
29836
29871
|
]];
|
|
29837
|
-
const EXAMPLES = ` $ gtd
|
|
29872
|
+
const EXAMPLES = ` $ gtd start list what can start
|
|
29873
|
+
$ gtd start <name> start a process
|
|
29838
29874
|
$ gtd next see what happens next
|
|
29839
29875
|
$ s=$(gtd land --json=script) && sh -c "$s" land the turn
|
|
29840
29876
|
$ gtd help land one command's details`;
|
|
@@ -29848,20 +29884,15 @@ const GROUPS = [
|
|
|
29848
29884
|
const renderHelp = () => {
|
|
29849
29885
|
const groups = GROUPS.map((group) => {
|
|
29850
29886
|
const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) => definition(synopsis(r), r.summary, TOP_COLUMN));
|
|
29851
|
-
const start = group === "Start and stop" ? definition(START_ROW.term, START_ROW.summary, TOP_COLUMN) : [];
|
|
29852
29887
|
const extra = group === "Driver plumbing" ? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN)) : [];
|
|
29853
|
-
return `${group}:\n${lines([
|
|
29854
|
-
...start,
|
|
29855
|
-
...rows,
|
|
29856
|
-
...extra
|
|
29857
|
-
])}`;
|
|
29888
|
+
return `${group}:\n${lines([...rows, ...extra])}`;
|
|
29858
29889
|
});
|
|
29859
|
-
return "Usage: gtd <command> [options]\n\n" + lines(wrap("gtd reads a repository's git state and prints what should happen next in its workflow. It never acts on it: a driver, or you, runs what it prints.")) + "\n" + groups.join("\n") + "\nOptions:\n" + lines(GLOBAL_OPTIONS.flatMap(([term, text]) => definition(term, text, TOP_COLUMN))) + `\nExamples:\n${EXAMPLES}\n\n` + lines(wrap("Run `gtd help <command>` for a command's details and options, and `gtd
|
|
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."));
|
|
29860
29891
|
};
|
|
29861
29892
|
/** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
|
|
29862
29893
|
const SHORT_USAGE = `Usage: gtd <command> [options]
|
|
29863
29894
|
|
|
29864
|
-
gtd
|
|
29895
|
+
gtd start <name> start a process
|
|
29865
29896
|
gtd next print what should happen next
|
|
29866
29897
|
gtd land record what the tree now shows
|
|
29867
29898
|
|
|
@@ -29902,10 +29933,12 @@ const tokenize = (tail) => {
|
|
|
29902
29933
|
const present = /* @__PURE__ */ new Set();
|
|
29903
29934
|
let jsonSeen = false;
|
|
29904
29935
|
let afterBareJson;
|
|
29936
|
+
const end = tail.indexOf("--");
|
|
29905
29937
|
for (let i = 0; i < tail.length; i++) {
|
|
29906
29938
|
const original = tail[i];
|
|
29939
|
+
if (i === end) continue;
|
|
29907
29940
|
const tok = SINGLE_DASH_ALIASES.get(original) ?? original;
|
|
29908
|
-
if (!tok.startsWith("-") || tok === "-") {
|
|
29941
|
+
if (end !== -1 && i > end || !tok.startsWith("-") || tok === "-") {
|
|
29909
29942
|
if (tail[i - 1] === "--json") afterBareJson ??= original;
|
|
29910
29943
|
positionals.push(original);
|
|
29911
29944
|
continue;
|
|
@@ -29992,7 +30025,7 @@ const tooMany = (cmd, rest, expected, max) => rest.length > max ? `gtd ${cmd}: t
|
|
|
29992
30025
|
const arityError = (cmd, rest, arity) => {
|
|
29993
30026
|
if (arity === "none") return tooMany(cmd, rest, "none", 0);
|
|
29994
30027
|
if ("names" in arity) return pairArityError(cmd, rest, arity.names);
|
|
29995
|
-
if ("many" in arity) return void 0;
|
|
30028
|
+
if ("many" in arity || "bare" in arity) return void 0;
|
|
29996
30029
|
if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1);
|
|
29997
30030
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
29998
30031
|
return "variadic" in arity ? void 0 : tooMany(cmd, rest, `one ${arity.name}`, 1);
|
|
@@ -30015,6 +30048,14 @@ const decodeFlags = (byFlag) => {
|
|
|
30015
30048
|
}
|
|
30016
30049
|
return right(bag);
|
|
30017
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
|
+
};
|
|
30018
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. */
|
|
30019
30060
|
const buildLandCommand = (bag) => ({
|
|
30020
30061
|
kind: "land",
|
|
@@ -30022,9 +30063,14 @@ const buildLandCommand = (bag) => ({
|
|
|
30022
30063
|
...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
|
|
30023
30064
|
});
|
|
30024
30065
|
const HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
|
|
30025
|
-
/**
|
|
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. */
|
|
30026
30072
|
const helpPlan = (argv) => {
|
|
30027
|
-
const tokenized = tokenize(argv.slice(2).filter((tok) => !HELP_FLAGS.has(tok)));
|
|
30073
|
+
const tokenized = tokenize(optionsOf(argv).slice(2).filter((tok) => !HELP_FLAGS.has(tok)));
|
|
30028
30074
|
const topic = tokenized.kind === "ok" ? tokenized.positionals : [];
|
|
30029
30075
|
return {
|
|
30030
30076
|
kind: "output",
|
|
@@ -30045,8 +30091,9 @@ const helpCommandPlan = (topic, json) => {
|
|
|
30045
30091
|
return usagePlan(`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0], COMMAND_WORDS, "gtd help ")}`, json);
|
|
30046
30092
|
};
|
|
30047
30093
|
const parseArgv = (argv) => {
|
|
30048
|
-
|
|
30049
|
-
if (
|
|
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 {
|
|
30050
30097
|
kind: "output",
|
|
30051
30098
|
stdout: `${GTD_VERSION}\n`
|
|
30052
30099
|
};
|
|
@@ -30054,8 +30101,6 @@ const parseArgv = (argv) => {
|
|
|
30054
30101
|
if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
|
|
30055
30102
|
const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized;
|
|
30056
30103
|
const first = positionals[0];
|
|
30057
|
-
const workflowRaw = byFlag.get("--workflow")?.[0];
|
|
30058
|
-
const workflowPresent = workflowRaw !== void 0;
|
|
30059
30104
|
if (first === "version") return {
|
|
30060
30105
|
kind: "output",
|
|
30061
30106
|
stdout: `${GTD_VERSION}\n`
|
|
@@ -30069,24 +30114,19 @@ const parseArgv = (argv) => {
|
|
|
30069
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);
|
|
30070
30115
|
}
|
|
30071
30116
|
const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
|
|
30072
|
-
const selectsStart = workflowPresent && first === void 0;
|
|
30073
30117
|
const judgeRun = row?.kind === "judge" && positionals[1] === "run";
|
|
30074
|
-
|
|
30075
|
-
if (row === void 0 && !selectsStart) {
|
|
30118
|
+
if (row === void 0 || first === void 0) {
|
|
30076
30119
|
const violation = scopeViolation(void 0, present);
|
|
30077
30120
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
30078
|
-
if (first === void 0) return usagePlan("gtd: missing command — run `gtd help` for every command, or `gtd
|
|
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);
|
|
30079
30122
|
return usagePlan(`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`, jsonSeen);
|
|
30080
30123
|
}
|
|
30081
|
-
const
|
|
30082
|
-
|
|
30083
|
-
|
|
30084
|
-
|
|
30085
|
-
const
|
|
30086
|
-
|
|
30087
|
-
const hint = afterBareJson !== void 0 && restPositionals.includes(afterBareJson) ? ` — to print one field, write --json=${afterBareJson}` : "";
|
|
30088
|
-
return usagePlan(arityMsg + hint, jsonSeen);
|
|
30089
|
-
}
|
|
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);
|
|
30090
30130
|
}
|
|
30091
30131
|
const violation = scopeViolation(kind, present);
|
|
30092
30132
|
if (violation !== void 0) return usagePlan(violation, jsonSeen);
|
|
@@ -30110,30 +30150,20 @@ const parseArgv = (argv) => {
|
|
|
30110
30150
|
};
|
|
30111
30151
|
}
|
|
30112
30152
|
if (kind === "start") {
|
|
30113
|
-
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);
|
|
30114
30155
|
return {
|
|
30115
30156
|
kind: "command",
|
|
30116
30157
|
command: {
|
|
30117
30158
|
kind: "start",
|
|
30118
|
-
|
|
30119
|
-
|
|
30120
|
-
vars: bag["--var"] ?? {}
|
|
30121
|
-
label
|
|
30159
|
+
...name !== void 0 ? { name } : {},
|
|
30160
|
+
args,
|
|
30161
|
+
vars: bag["--var"] ?? {}
|
|
30122
30162
|
},
|
|
30123
30163
|
json,
|
|
30124
30164
|
verbose
|
|
30125
30165
|
};
|
|
30126
30166
|
}
|
|
30127
|
-
if (kind === "door") return {
|
|
30128
|
-
kind: "command",
|
|
30129
|
-
command: {
|
|
30130
|
-
kind: "door",
|
|
30131
|
-
...restPositionals[0] !== void 0 ? { name: restPositionals[0] } : {},
|
|
30132
|
-
args: restPositionals.slice(1)
|
|
30133
|
-
},
|
|
30134
|
-
json,
|
|
30135
|
-
verbose
|
|
30136
|
-
};
|
|
30137
30167
|
if (kind === "ui") return {
|
|
30138
30168
|
kind: "command",
|
|
30139
30169
|
command: {
|
|
@@ -30199,17 +30229,14 @@ const parseArgv = (argv) => {
|
|
|
30199
30229
|
verbose
|
|
30200
30230
|
};
|
|
30201
30231
|
if (kind === "historyJudges") {
|
|
30202
|
-
const
|
|
30203
|
-
|
|
30204
|
-
if (unknown !== void 0) return usagePlan(`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`, jsonSeen);
|
|
30205
|
-
const nan = Object.entries(vars).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
|
|
30206
|
-
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);
|
|
30207
30234
|
return {
|
|
30208
30235
|
kind: "command",
|
|
30209
30236
|
command: {
|
|
30210
30237
|
kind: "historyJudges",
|
|
30211
30238
|
refs: restPositionals,
|
|
30212
|
-
vars,
|
|
30239
|
+
vars: thresholds.right,
|
|
30213
30240
|
jsonl: bag["--jsonl"] ?? false
|
|
30214
30241
|
},
|
|
30215
30242
|
json,
|
|
@@ -31098,7 +31125,7 @@ const recognizeWrite = (repo, script) => {
|
|
|
31098
31125
|
const [, content, to] = words$1(lines[3]);
|
|
31099
31126
|
const remove = words$1(lines.find((l) => l.startsWith("rm -rf -- ")) ?? "");
|
|
31100
31127
|
if (path === void 0 || content === void 0 || to !== path) return void 0;
|
|
31101
|
-
if (writeScript(path, content, remove) !== script) return void 0;
|
|
31128
|
+
if (writeScript$1(path, content, remove) !== script) return void 0;
|
|
31102
31129
|
repo.writeFile(path, content);
|
|
31103
31130
|
removeAll(repo, remove);
|
|
31104
31131
|
return ok();
|
|
@@ -31735,7 +31762,10 @@ const recognizeGtdCheck = (repo, block) => {
|
|
|
31735
31762
|
error: `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`
|
|
31736
31763
|
};
|
|
31737
31764
|
const content = repo.readFile(file);
|
|
31738
|
-
if (content === void 0) return {
|
|
31765
|
+
if (content === void 0) return {
|
|
31766
|
+
kind: "failed",
|
|
31767
|
+
error: `gtd check: no such file: ${file}`
|
|
31768
|
+
};
|
|
31739
31769
|
const findings = format.validate(content).map((finding) => formatFinding(file, finding));
|
|
31740
31770
|
if (findings.length === 0) return { kind: "noop" };
|
|
31741
31771
|
return {
|
|
@@ -31953,6 +31983,52 @@ const unrecognized = (block, output = "") => ({
|
|
|
31953
31983
|
});
|
|
31954
31984
|
Narrator.layer(() => {}, false);
|
|
31955
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
|
|
31956
32032
|
//#region src/testkit/TestRepo.ts
|
|
31957
32033
|
const REGISTRY = Symbol.for("@pmelab/gtd/testkit:modules");
|
|
31958
32034
|
const registry = () => {
|
|
@@ -31961,9 +32037,10 @@ const registry = () => {
|
|
|
31961
32037
|
};
|
|
31962
32038
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
31963
32039
|
let registered = 0;
|
|
32040
|
+
const lookup = (key) => `globalThis[Symbol.for(${JSON.stringify(REGISTRY.description)})].get(${JSON.stringify(key)})`;
|
|
31964
32041
|
/** A config whose exports are module `id`'s, read off the registry when the engine loads it. */
|
|
31965
32042
|
const moduleSource = (id, module) => {
|
|
31966
|
-
const lines = [`const m =
|
|
32043
|
+
const lines = [`const m = ${lookup(id)}`];
|
|
31967
32044
|
for (const name of Object.keys(module)) if (name === "default") lines.push(`export default m.default`);
|
|
31968
32045
|
else if (IDENTIFIER.test(name)) lines.push(`export const ${name} = m[${JSON.stringify(name)}]`);
|
|
31969
32046
|
else throw new Error(`testkit: export name "${name}" is not an identifier`);
|
|
@@ -31971,6 +32048,48 @@ const moduleSource = (id, module) => {
|
|
|
31971
32048
|
};
|
|
31972
32049
|
/** An actor that runs the step's own script, for steps whose script `runScript` understands. */
|
|
31973
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
|
+
};
|
|
31974
32093
|
const words = (text) => [...text.matchAll(/'((?:[^']|'\\'')*)'/g)].map((m) => m[1].replaceAll("'\\''", "'"));
|
|
31975
32094
|
const fileOf = (repo, path, what) => {
|
|
31976
32095
|
const content = repo.readFile(path);
|
|
@@ -32028,17 +32147,48 @@ const expectedVerdict = (doc) => {
|
|
|
32028
32147
|
} catch {}
|
|
32029
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");
|
|
32030
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
|
+
};
|
|
32031
32179
|
const testRepo = (options) => {
|
|
32032
32180
|
const repo = new InMemRepo();
|
|
32033
32181
|
const env = { ...options.env };
|
|
32034
32182
|
const { workflow } = options;
|
|
32035
32183
|
const id = `module-${++registered}`;
|
|
32036
|
-
|
|
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))));
|
|
32037
32187
|
for (const [path, content] of Object.entries(options.files ?? {})) repo.writeFile(path, content);
|
|
32038
32188
|
repo.seedCommit("chore: initial commit");
|
|
32039
32189
|
const canned = [...options.commands ?? []];
|
|
32040
|
-
const
|
|
32041
|
-
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));
|
|
32042
32192
|
if (hit === void 0) return void 0;
|
|
32043
32193
|
const { exit = 0, output = "", effect } = hit;
|
|
32044
32194
|
return {
|
|
@@ -32047,10 +32197,16 @@ const testRepo = (options) => {
|
|
|
32047
32197
|
output,
|
|
32048
32198
|
effect: () => effect?.(self)
|
|
32049
32199
|
};
|
|
32050
|
-
} };
|
|
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;
|
|
32051
32206
|
const gtd = async (...args) => {
|
|
32052
|
-
const { io, result } = makeCapturingCliIo(repo,
|
|
32207
|
+
const { io, result } = makeCapturingCliIo(repo, seenEnv());
|
|
32053
32208
|
if (typeof workflow !== "string") registry().set(id, workflow);
|
|
32209
|
+
for (const [specifier, module] of modules) registry().set(moduleKey(specifier), module);
|
|
32054
32210
|
try {
|
|
32055
32211
|
await runPromise(runCli([
|
|
32056
32212
|
"node",
|
|
@@ -32059,6 +32215,7 @@ const testRepo = (options) => {
|
|
|
32059
32215
|
], io));
|
|
32060
32216
|
} finally {
|
|
32061
32217
|
registry().delete(id);
|
|
32218
|
+
for (const [specifier] of modules) registry().delete(moduleKey(specifier));
|
|
32062
32219
|
}
|
|
32063
32220
|
return result();
|
|
32064
32221
|
};
|
|
@@ -32102,21 +32259,42 @@ const testRepo = (options) => {
|
|
|
32102
32259
|
return JSON.parse(result.stdout);
|
|
32103
32260
|
};
|
|
32104
32261
|
/** Lands what an actor did: a judge's verdict answered, anything else validated and landed. */
|
|
32105
|
-
const act = async (doc,
|
|
32262
|
+
const act = async (doc, returned) => {
|
|
32106
32263
|
if (doc.actor !== "judge") {
|
|
32107
32264
|
if (doc.validate !== void 0) apply(`validating ${doc.file ?? doc.state}`, doc.validate);
|
|
32108
32265
|
await runWrite(["land"]);
|
|
32109
32266
|
return;
|
|
32110
32267
|
}
|
|
32111
32268
|
const actor = `the actor for judge step "${doc.state}"`;
|
|
32112
|
-
if (
|
|
32113
|
-
|
|
32114
|
-
|
|
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);
|
|
32115
32289
|
};
|
|
32116
32290
|
const self = {
|
|
32117
32291
|
command: (command) => {
|
|
32118
32292
|
canned.push(command);
|
|
32119
32293
|
},
|
|
32294
|
+
setEnv: (name, value) => {
|
|
32295
|
+
if (value === void 0) delete env[name];
|
|
32296
|
+
else env[name] = value;
|
|
32297
|
+
},
|
|
32120
32298
|
branch: (name, ref = "HEAD") => repo.createBranch(name, ref),
|
|
32121
32299
|
checkout: (name) => repo.checkoutBranch(name),
|
|
32122
32300
|
ref: (name, ref = "HEAD") => {
|
|
@@ -32131,45 +32309,47 @@ const testRepo = (options) => {
|
|
|
32131
32309
|
remove: (path) => repo.deleteFile(path),
|
|
32132
32310
|
commit: (message) => repo.seedCommit(message),
|
|
32133
32311
|
show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
|
|
32134
|
-
runScript: (script) => {
|
|
32135
|
-
const
|
|
32136
|
-
|
|
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);
|
|
32137
32316
|
else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`);
|
|
32138
32317
|
},
|
|
32139
32318
|
gtd,
|
|
32140
|
-
start: (
|
|
32141
|
-
"
|
|
32142
|
-
|
|
32319
|
+
start: (name, ...args) => begin([
|
|
32320
|
+
"start",
|
|
32321
|
+
name,
|
|
32143
32322
|
...args
|
|
32144
32323
|
]),
|
|
32145
32324
|
door: (name, ...args) => begin([
|
|
32146
|
-
"
|
|
32325
|
+
"start",
|
|
32147
32326
|
name,
|
|
32148
32327
|
...args
|
|
32149
32328
|
]),
|
|
32150
32329
|
land: () => runWrite(["land"]),
|
|
32151
32330
|
next,
|
|
32152
|
-
drive: async (actors = {},
|
|
32331
|
+
drive: async (actors = {}, options = {}) => {
|
|
32332
|
+
const { maxBeats = 50 } = options;
|
|
32153
32333
|
const passed = [];
|
|
32154
|
-
for (let
|
|
32334
|
+
for (let n = 0; n < maxBeats; n++) {
|
|
32155
32335
|
const doc = await next();
|
|
32156
32336
|
passed.push(doc.state);
|
|
32157
|
-
|
|
32158
|
-
if (
|
|
32159
|
-
await runWrite(["land"]);
|
|
32160
|
-
continue;
|
|
32161
|
-
}
|
|
32162
|
-
const actor = actors[doc.state];
|
|
32163
|
-
if (actor === void 0) return doc;
|
|
32164
|
-
await act(doc, await actor(doc, self));
|
|
32337
|
+
const rest = await beat(doc, actors, options);
|
|
32338
|
+
if (rest !== void 0) return rest;
|
|
32165
32339
|
}
|
|
32166
32340
|
throw new Error(`the process did not rest within ${maxBeats} beats; it passed through:\n${passed.join(" → ")}`);
|
|
32167
32341
|
},
|
|
32168
32342
|
subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
|
|
32169
32343
|
messages: () => repo.commitHistory().map(({ message }) => message),
|
|
32170
|
-
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
|
+
}))
|
|
32171
32351
|
};
|
|
32172
32352
|
return self;
|
|
32173
32353
|
};
|
|
32174
32354
|
//#endregion
|
|
32175
|
-
export {
|
|
32355
|
+
export { verdictWhen as a, verdict as i, runsItsScript as n, whenPrompted as o, testRepo as r, inTurn as t };
|