@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.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +37 -33
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/{testkit-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
  7. package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +751 -645
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +245 -36
  12. package/dist/testkit.mjs +3 -2
  13. package/dist/web/main.js +234 -232
  14. package/dist/wire.mjs +2 -2
  15. package/dist/workflow-kit.mjs +3 -3
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +66 -60
  18. package/src/cli/Cli.test.ts +183 -182
  19. package/src/cli/Cli.ts +164 -186
  20. package/src/cli/History.ts +9 -4
  21. package/src/cli/Judges.ts +1 -1
  22. package/src/cli/Output.test.ts +35 -0
  23. package/src/cli/Output.ts +31 -1
  24. package/src/cli/Program.test.ts +65 -79
  25. package/src/cli/Program.ts +157 -118
  26. package/src/edge/Edge.test.ts +64 -56
  27. package/src/edge/Edge.ts +3 -3
  28. package/src/flows/factory.test.ts +14 -0
  29. package/src/flows/factory.ts +22 -5
  30. package/src/flows/helpers.test.ts +35 -0
  31. package/src/flows/helpers.ts +19 -10
  32. package/src/flows/index.test.ts +66 -0
  33. package/src/flows/index.ts +109 -5
  34. package/src/flows/internal.ts +7 -0
  35. package/src/flows/runtime.test.ts +192 -1
  36. package/src/flows/runtime.ts +188 -54
  37. package/src/gherkin/git-control.feature +6 -6
  38. package/src/gherkin/index.test.ts +127 -0
  39. package/src/gherkin/index.ts +370 -51
  40. package/src/gherkin/messages.test.ts +38 -4
  41. package/src/gherkin/messages.ts +21 -4
  42. package/src/gherkin/more-steps.feature +170 -0
  43. package/src/gherkin/scenario-end.feature +101 -0
  44. package/src/gherkin/testkit.feature +83 -7
  45. package/src/install/Install.test.ts +7 -7
  46. package/src/install/Install.ts +8 -8
  47. package/src/replay/Replay.test.ts +13 -2
  48. package/src/replay/Replay.ts +19 -30
  49. package/src/replay/judgeDiffShape.test.ts +1 -1
  50. package/src/steering/Footnotes.test.ts +1 -1
  51. package/src/steering/SteeringFormats.test.ts +1 -1
  52. package/src/steering/freeform.test.ts +1 -1
  53. package/src/steering/index.test.ts +27 -1
  54. package/src/steering/index.ts +6 -104
  55. package/src/steering/internal.ts +11 -0
  56. package/src/steering/qa.test.ts +7 -2
  57. package/src/steering/reads.test.ts +42 -0
  58. package/src/steering/reads.ts +43 -0
  59. package/src/steering/registry.ts +93 -0
  60. package/src/steering/review.test.ts +2 -2
  61. package/src/step/planStart.test.ts +18 -16
  62. package/src/step/planStart.ts +9 -9
  63. package/src/testkit/TestRepo.test.ts +245 -7
  64. package/src/testkit/TestRepo.ts +271 -40
  65. package/src/testkit/imports.test.ts +58 -0
  66. package/src/testkit/imports.ts +102 -0
  67. package/src/testkit/index.test.ts +28 -0
  68. package/src/testkit/index.ts +14 -2
  69. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  70. package/src/twins/EmittedScripts.test.ts +4 -2
  71. package/src/twins/InMemRepo.ts +1 -0
  72. package/src/wire/Script.ts +11 -0
  73. package/src/wire/WireSchema.test.ts +3 -0
  74. package/src/wire/golden/1/script.json +4 -0
  75. package/src/wire/index.ts +1 -0
  76. package/src/workflow/SteeringFormats.test.ts +1 -1
  77. package/src/workflow/compile.ts +2 -2
  78. package/src/workflow/discovery.test.ts +3 -3
  79. package/src/workflow/load.test.ts +256 -116
  80. package/src/workflow/load.ts +169 -146
  81. package/src/workflow-kit/index.test.ts +25 -0
  82. package/src/workflow-kit/index.ts +37 -4
  83. package/src/workflow-kit/review.ts +2 -2
@@ -1,6 +1,6 @@
1
- import { answered, applyScript, checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, numeric, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports, writeScript } from "../flows.mjs";
2
- import { E as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING } from "./wire-BTUhRGXj.mjs";
3
- import { F as formatFinding, N as headingSectionBodies, P as headingSections, a as unansweredQuestions, j as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./steering-CCNvngSH.mjs";
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 door "$@")"
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 --workflow\`/\`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 door <name> [args...]\`, captured by command substitution — never a
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 door ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
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 doors\` lists the doors a repository offers (those of the workflows its
24600
- config exports — gtd-workflows' \`default\` brings \`fix\` and
24601
- \`review [base]\` — plus any the config declares). Refuses with
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 --workflow <name>\`, \`gtd abandon\`,
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: (text) => headingSections(text),
25407
- sectionBodies: (text) => headingSectionBodies(text),
25408
- openQuestions: (text) => {
25409
- const qa = steeringFormatFor("qa");
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 its `defaults` export",
25766
- " env → an option of the workflow factory in gtd.config.ts, or its `envDefaults` export",
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 install\` says where to get one`,
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.file.summary,
25809
- base: (w, v) => entry.file.base?.(w, v) ?? entry.base?.(w, v),
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.file.skills,
25802
+ skills: entry.skills,
25813
25803
  configuredSkills: compiled.skills,
25814
- access: entry.file.access,
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
- const stringRecord = (exports, name) => {
25937
- const value = exports[name] ?? {};
25938
- if (typeof value !== "object" || value === null || Object.values(value).some((entry) => typeof entry !== "string")) throw new Error(`the "${name}" export is not a record of strings`);
25939
- return value;
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(`the "skills" export is not a record of skill-name arrays`);
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
- /** Read a module's optional `skills` export — a scope full name -> skill list record, or a function of the vars returning one. */
25947
- const skillsExport = (exports) => {
25948
- const value = exports["skills"] ?? {};
25949
- if (typeof value === "function") return (vars) => checkSkillsRecord(value(vars));
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
- /** Read a module's optional `access` export — a scope full name -> `{ read?, write? }` record, or a function of the vars returning one. */
25954
- const accessExport = (exports) => {
25955
- const value = exports["access"] ?? {};
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
- return () => value;
25958
+ const record = checkAccessRecord(value ?? {}, what);
25959
+ return () => record;
25958
25960
  };
25959
- const optionalFunction = (exports, name) => {
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(`the "${name}" export is not a function`);
25963
+ if (typeof value !== "function") throw new Error(`${what} is not a function`);
25963
25964
  return value;
25964
25965
  };
25965
- const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
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
- "config"
25973
+ "summary",
25974
+ "base"
25976
25975
  ]);
25977
- /** The rules every declared setting meets, whether a file's export or a factory's build. */
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. The
26039
- * `doors` export adds to them and declares a name several workflows clash on.
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, declared) => {
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 if (!declared.has(name)) clashes.set(name, built.map((door) => door.workflow));
26040
+ else clashes.set(name, built.map((door) => door.workflow));
26058
26041
  }
26059
26042
  return {
26060
- doors: new Map([...doors, ...declared]),
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 exported function or factory workflow but
26075
- * the reserved names is a workflow named by its export. `cache` is jiti's OWN
26076
- * per-call module cache (unrelated to the instance's `moduleCache: false`) —
26077
- * passing an empty object in has the side effect of collecting every real file
26078
- * jiti resolved evaluating this module, module AND every relative import it
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 = fileFrom(exports, module.filepath, files);
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, doorsFrom(exports));
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: checkDoors(derived.doors, catalogue),
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: [...nameRejections(rejections), ...[...derived.clashes].map(([name, builders]) => {
26110
- const names = [...builders].sort().map((b) => JSON.stringify(b));
26111
- return {
26112
- severity: "warning",
26113
- message: `door "${name}" is built by workflows ${names.slice(0, -1).join(", ")} and ${names.at(-1)} and none is exported under that name — declare it in the "doors" export`,
26114
- path: ["doors", name],
26115
- origin: file.origin
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) => typeof value === "function" || isFactoryWorkflow(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 (`gtd install` scaffolds one)");
26167
- if (!isWorkflow(value)) throw new Error("the default export is not a function");
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 plain flow as-is, a factory validated and built. */
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
- flow: value,
26175
- file,
26176
- defaults: file.defaults,
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
- /** A factory's settings over its file's, held to the rules a file's own exports are. */
26192
- const builtSettings = (name, file, build) => {
26193
- checkSettings(build.defaults ?? {}, build.envDefaults ?? {}, (kind) => `workflow "${name}" builds ${kind}`);
26194
- const defaults = {
26195
- ...file.defaults,
26196
- ...build.defaults
26197
- };
26198
- const envDefaults = {
26199
- ...file.envDefaults,
26200
- ...build.envDefaults
26201
- };
26202
- checkSettings(defaults, envDefaults, (kind) => `workflow "${name}" builds ${kind}`);
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 refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
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 --workflow <name>`: start a brand NEW process on a workflow, writing an
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
- const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, Object.keys(workflow.vars), Object.keys(workflow.env));
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 --workflow` writes — no landing ever produces one — and opens an
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 --workflow <name>` can start a process here: the workflow must
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) => sync$1(() => {
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 runInstallCommand = (out) => gen(function* () {
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 install: 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)`);
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 --workflow`'s emitted script. Every OTHER
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 runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => gen(function* () {
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(`gtd --workflow: unknown workflow "${workflow}" — ${startable}`));
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
- }, actor, {
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.write(listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
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
- const noSuchDoor = (name, doors) => {
28870
- const listing = doors.size > 0 ? `doors: ${[...doors].map(([n, d]) => doorSynopsis(n, d)).join(", ")}` : "this config has no doors";
28871
- return new GtdUsageError(name === void 0 ? `gtd door: missing door name — ${listing}` : `gtd door: unknown door "${name}" — ${listing}`);
28872
- };
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(`gtd door ${name}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd door ${doorSynopsis(name, door)}`));
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(`gtd door ${name}: ${e instanceof Error ? e.message : String(e)}`)
28899
+ catch: (e) => /* @__PURE__ */ new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`)
28892
28900
  });
28893
- yield* runStartCommand("human", door.workflow, { ...vars }, out, `gtd door ${name}`);
28901
+ yield* startWorkflow(door.workflow, {
28902
+ ...vars,
28903
+ ...extra
28904
+ }, json, out, label);
28894
28905
  });
28895
- /** `gtd doors`: every door and its args, sorted by name — plain lines, or one JSON array. */
28896
- const runDoorsCommand = (json, out) => gen(function* () {
28897
- const doors = [...(yield* (yield* ConfigService).load).doors];
28898
- if (json.kind === "off") {
28899
- if (doors.length === 0) yield* (yield* Narrator).warn("gtd doors: this config has no doors — a workflow factory's `doors` option or the config's `doors` export adds one");
28900
- out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""));
28901
- return;
28902
- }
28903
- const document = doors.map(([n, d]) => ({
28904
- name: n,
28905
- workflow: d.workflow,
28906
- args: (d.args ?? []).map((arg) => ({
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.kind === "document") out.write(`${JSON.stringify(document)}\n`);
28912
- else yield* writeSelection(out, document, json.path);
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.write(combinedScript(required, ""));
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.write(combinedScript(required, ""));
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.write(combinedScript(required, ""));
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 resolved = yield* resolveValidateScript(rest);
29061
- out.write(resolved !== void 0 && resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
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 absent file exits 0, like `gtd validate`. Findings ride the failing
29069
- * Effect's message, so the shared refusal path reports them on stderr.
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(/* @__PURE__ */ new Error(`gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`));
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 = (yield* Workspace).atPath(file);
29087
- const errors = content !== void 0 ? format.validate(content).map((finding) => formatFinding(file, finding)) : [];
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; a missing file fails, like `--open-questions`. */
29146
+ /** Prints one `file:line: [^name]: <first H entry>` line per open thread. */
29109
29147
  const runOpenThreadsCheckCommand = (format, file) => gen(function* () {
29110
- const content = (yield* Workspace).atPath(file);
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. A missing
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 content = (yield* Workspace).atPath(file);
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 "install":
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.actor, command.workflow, command.vars, out, command.label);
29254
- case "door": return runDoorCommand(command.name, command.args, out);
29255
- case "doors": return runDoorsCommand(json, out);
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 "install": return runInstallCommand(out);
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`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd history judges`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
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: { strays: "Print one entry of the array (--json=0), nothing past its end" }
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: "--workflow",
29536
+ name: "--threshold",
29472
29537
  arity: 1,
29473
- repeatable: false,
29474
- scope: (kind) => kind === "start",
29475
- decode: ([raw]) => right(raw ?? ""),
29476
- scopeError: "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` form; landing and starting are different verbs",
29477
- valueHint: "<name>",
29478
- help: "Start a new process on the named workflow"
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" || kind === "historyJudges",
29485
- decode: (raws) => {
29486
- const vars = {};
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", "--var"]
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: "door",
29609
- kind: "door",
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 process through a named shortcut",
29616
- details: `Start a process through a door: a named shortcut to a workflow that maps its positional args to the workflow's settings. It prints the same start script \`gtd --workflow\` does. Each workflow the config exports brings its own doors, plus the config's \`doors\` export.
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
- With no <name>, an unknown one, or the wrong number of args, it lists the doors and exits 2.`
29619
- },
29620
- {
29621
- token: "doors",
29622
- kind: "doors",
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: "install",
29738
- kind: "install",
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(["--workflow", "--var"]);
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 --workflow <name> start a process
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 install` to learn how to drive 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 --workflow <name> start a process
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
- /** `--help`/`-h` anywhere: the named command's own help, else the overview — never a usage error, so a typo'd command still gets help. */
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
- if (argv.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv);
30049
- if (argv.includes("--version") || argv.includes("-V")) return {
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
- const kind = selectsStart ? "start" : judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row?.kind;
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 install` to learn how to drive gtd", jsonSeen, SHORT_USAGE);
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 restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
30082
- if (kind === "start" && first === void 0) {
30083
- if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
30084
- } else {
30085
- const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") || kind === "door" && restPositionals.length === 0 ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
30086
- if (arityMsg !== void 0) {
30087
- const hint = afterBareJson !== void 0 && restPositionals.includes(afterBareJson) ? ` — to print one field, write --json=${afterBareJson}` : "";
30088
- return usagePlan(arityMsg + hint, jsonSeen);
30089
- }
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 label = `gtd --workflow ${workflowRaw}`;
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
- actor: "human",
30119
- workflow: workflowRaw,
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 vars = bag["--var"] ?? {};
30203
- const unknown = Object.keys(vars).find((name) => !Object.hasOwn(THRESHOLDS, name));
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 { kind: "noop" };
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 = globalThis[Symbol.for(${JSON.stringify(REGISTRY.description)})].get(${JSON.stringify(id)})`];
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
- repo.writeFile("gtd.config.ts", typeof workflow === "string" ? workflow : moduleSource(id, workflow));
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 commands = { get: (command) => {
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, env);
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, verdict) => {
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 (verdict === void 0) throw new Error(`${actor} returned no verdict\n${expectedVerdict(doc)}`);
32113
- if (typeof verdict !== "string") throw new Error(`${actor} returned ${Array.isArray(verdict) ? "an array" : typeof verdict === "object" ? "an object" : `a ${typeof verdict}`}, not a JSON string — JSON.stringify it\n${expectedVerdict(doc)}`);
32114
- await runWrite(["judge", "answer"], verdict, (error) => /* @__PURE__ */ new Error(`${actor} sent a verdict gtd refused:\n${verdict}\n${expectedVerdict(doc)}\n${error.message}`));
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 flow = applyFlowScript(repo, commands, script);
32136
- if (flow === void 0) runLines(repo, commands, script);
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: (workflow, ...args) => begin([
32141
- "--workflow",
32142
- workflow,
32319
+ start: (name, ...args) => begin([
32320
+ "start",
32321
+ name,
32143
32322
  ...args
32144
32323
  ]),
32145
32324
  door: (name, ...args) => begin([
32146
- "door",
32325
+ "start",
32147
32326
  name,
32148
32327
  ...args
32149
32328
  ]),
32150
32329
  land: () => runWrite(["land"]),
32151
32330
  next,
32152
- drive: async (actors = {}, { maxBeats = 50 } = {}) => {
32331
+ drive: async (actors = {}, options = {}) => {
32332
+ const { maxBeats = 50 } = options;
32153
32333
  const passed = [];
32154
- for (let beat = 0; beat < maxBeats; beat++) {
32334
+ for (let n = 0; n < maxBeats; n++) {
32155
32335
  const doc = await next();
32156
32336
  passed.push(doc.state);
32157
- if (doc.kind === "stalled") return doc;
32158
- if (doc.kind === "capture") {
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 { testRepo as n, InMemRepo as r, runsItsScript as t };
32355
+ export { verdictWhen as a, verdict as i, runsItsScript as n, whenPrompted as o, testRepo as r, inTurn as t };