@pmelab/gtd 59.0.0 → 60.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +42 -38
  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-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
  7. package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +1101 -1025
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +371 -82
  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 +68 -61
  18. package/src/cli/Cli.test.ts +311 -229
  19. package/src/cli/Cli.ts +486 -585
  20. package/src/cli/Help.test.ts +47 -0
  21. package/src/cli/Help.ts +70 -0
  22. package/src/cli/History.ts +9 -4
  23. package/src/cli/Judges.ts +1 -1
  24. package/src/cli/Output.test.ts +35 -0
  25. package/src/cli/Output.ts +31 -1
  26. package/src/cli/Program.test.ts +131 -82
  27. package/src/cli/Program.ts +197 -111
  28. package/src/edge/Edge.test.ts +64 -56
  29. package/src/edge/Edge.ts +4 -4
  30. package/src/emit/Emit.test.ts +1 -1
  31. package/src/emit/Emit.ts +1 -1
  32. package/src/flows/factory.test.ts +14 -0
  33. package/src/flows/factory.ts +25 -6
  34. package/src/flows/helpers.test.ts +35 -0
  35. package/src/flows/helpers.ts +19 -10
  36. package/src/flows/index.test.ts +66 -0
  37. package/src/flows/index.ts +109 -5
  38. package/src/flows/internal.ts +7 -0
  39. package/src/flows/runtime.test.ts +192 -1
  40. package/src/flows/runtime.ts +188 -54
  41. package/src/gherkin/git-control.feature +6 -6
  42. package/src/gherkin/index.test.ts +127 -0
  43. package/src/gherkin/index.ts +481 -98
  44. package/src/gherkin/messages.test.ts +124 -0
  45. package/src/gherkin/messages.ts +101 -0
  46. package/src/gherkin/more-steps.feature +170 -0
  47. package/src/gherkin/scenario-end.feature +101 -0
  48. package/src/gherkin/testkit.feature +83 -7
  49. package/src/install/Install.test.ts +7 -7
  50. package/src/install/Install.ts +8 -8
  51. package/src/replay/Replay.test.ts +13 -2
  52. package/src/replay/Replay.ts +19 -30
  53. package/src/replay/judgeDiffShape.test.ts +1 -1
  54. package/src/steering/Footnotes.test.ts +1 -1
  55. package/src/steering/SteeringFormats.test.ts +1 -1
  56. package/src/steering/freeform.test.ts +1 -1
  57. package/src/steering/index.test.ts +27 -1
  58. package/src/steering/index.ts +6 -104
  59. package/src/steering/internal.ts +11 -0
  60. package/src/steering/qa.test.ts +7 -2
  61. package/src/steering/reads.test.ts +42 -0
  62. package/src/steering/reads.ts +43 -0
  63. package/src/steering/registry.ts +93 -0
  64. package/src/steering/review.test.ts +2 -2
  65. package/src/step/planStart.test.ts +18 -16
  66. package/src/step/planStart.ts +9 -9
  67. package/src/testkit/TestRepo.test.ts +289 -6
  68. package/src/testkit/TestRepo.ts +317 -41
  69. package/src/testkit/imports.test.ts +58 -0
  70. package/src/testkit/imports.ts +102 -0
  71. package/src/testkit/index.test.ts +28 -0
  72. package/src/testkit/index.ts +14 -2
  73. package/src/twins/EmittedScriptRecognizer.ts +7 -14
  74. package/src/twins/EmittedScripts.test.ts +4 -2
  75. package/src/twins/InMemRepo.ts +1 -0
  76. package/src/wire/Land.ts +1 -1
  77. package/src/wire/Script.ts +11 -0
  78. package/src/wire/WireSchema.test.ts +3 -0
  79. package/src/wire/golden/1/script.json +4 -0
  80. package/src/wire/index.ts +1 -0
  81. package/src/workflow/SteeringFormats.test.ts +1 -1
  82. package/src/workflow/compile.ts +2 -2
  83. package/src/workflow/discovery.test.ts +3 -3
  84. package/src/workflow/load.test.ts +256 -116
  85. package/src/workflow/load.ts +175 -152
  86. package/src/workflow/skills.test.ts +5 -5
  87. package/src/workflow/skills.ts +1 -1
  88. package/src/workflow-kit/index.test.ts +25 -0
  89. package/src/workflow-kit/index.ts +37 -4
  90. 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 } from "./wire-C12dahrH.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) => ({
@@ -25743,7 +25734,7 @@ const dedupeDiagnostics = (diagnostics) => {
25743
25734
  };
25744
25735
  //#endregion
25745
25736
  //#region src/workflow/load.ts
25746
- const configError = (diagnostics) => new GtdError(`gtd config:\n${diagnostics.filter((d) => d.severity === "error").map((d) => ` - ${formatDiagnostic(d)}`).join("\n")}`);
25737
+ const configError = (diagnostics) => new GtdError(`gtd: config:\n${diagnostics.filter((d) => d.severity === "error").map((d) => ` - ${formatDiagnostic(d)}`).join("\n")}`);
25747
25738
  const failOnErrors = (diagnostics) => diagnostics.some((d) => d.severity === "error") ? fail$3(configError(diagnostics)) : _void;
25748
25739
  const MIGRATION_DOC = "https://github.com/pmelab/gtd/blob/main/docs/configuration.md#migrating-from-gtdrc";
25749
25740
  /**
@@ -25759,11 +25750,11 @@ const refuseLegacyRc = gen(function* () {
25759
25750
  const found = files.filter((f) => f.owned).map((f) => f.path);
25760
25751
  if (found.length === 0) return;
25761
25752
  return yield* fail$3(new GtdError([
25762
- "gtd config:",
25753
+ "gtd: config:",
25763
25754
  ...found.map((file) => ` - ${file}: no longer read — delete it`),
25764
25755
  " gtd 23 removed the .gtdrc files; move each key they set:",
25765
- " vars → an option of the workflow factory in gtd.config.ts, or 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]));
@@ -25795,23 +25786,22 @@ const load = gen(function* () {
25795
25786
  });
25796
25787
  const loaded = yield* try_({
25797
25788
  try: () => loadWorkflows(module, shipped),
25798
- catch: (e) => new GtdError(`gtd config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
25789
+ catch: (e) => new GtdError(`gtd: config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`)
25799
25790
  });
25800
25791
  const compiled = loaded.config;
25801
25792
  const diagnostics = dedupeDiagnostics(sortDiagnostics([...compiled.diagnostics, ...loaded.diagnostics]));
25802
25793
  yield* failOnErrors(diagnostics);
25803
25794
  if (loaded.default === void 0) return yield* die("unreachable: a default fails to build only with an error reported");
25804
25795
  const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
25805
- const steering = loaded.file.steering;
25806
25796
  const define = (entry) => ({
25807
25797
  flow: entry.flow,
25808
- summary: entry.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"
25975
+ ]);
25976
+ /** Names that are never a workflow. */
25977
+ const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
25978
+ "default",
25979
+ "config",
25980
+ ...RETIRED_EXPORTS
25976
25981
  ]);
25977
- /** The rules every declared setting meets, whether a file's export or a factory's build. */
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`. */
@@ -26300,7 +26302,7 @@ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$2(promise(() => r
26300
26302
  })), (outcome) => {
26301
26303
  if (outcome.kind === "rest") return succeed$3(outcome.rest.name);
26302
26304
  const problem = outcome.kind === "failed" || outcome.kind === "refused" ? outcome.message.replace(/^gtd: /, "") : "the default entry must begin at a step — that step is where a finished process waits";
26303
- return fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: ${problem}`));
26305
+ return fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: ${problem}`));
26304
26306
  });
26305
26307
  /**
26306
26308
  * The default entry's first step — where a finished process waits — found the
@@ -26316,20 +26318,20 @@ const firstStep = (loaded, head) => gen(function* () {
26316
26318
  const env = watchedEmptySettings();
26317
26319
  const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
26318
26320
  const reads = [...settingReads("process", vars), ...settingReads("environment", env)];
26319
- if (reads.length > 0) return yield* fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
26321
+ if (reads.length > 0) return yield* fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start reads ${reads.join(" and ")} — that step is where a finished process waits, so it must not depend on a setting`));
26320
26322
  if (outcome._tag === "Left") return yield* fail$3(outcome.left);
26321
26323
  const name = outcome.right;
26322
26324
  if (!empty.read()) return name;
26323
26325
  const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
26324
26326
  if (atHead === name) return name;
26325
- return yield* fail$3(new GtdError(`gtd config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
26327
+ return yield* fail$3(new GtdError(`gtd: config:\n - ${loaded.file.origin}: the flow's first step on an ordinary start depends on the repository's files ("${name}" without them, "${atHead}" at HEAD) — that step is where a finished process waits, so it must not change as files do`));
26326
26328
  });
26327
26329
  var ConfigService = class ConfigService extends Tag("ConfigService")() {
26328
26330
  static Live = succeed$2(ConfigService, { load });
26329
26331
  };
26330
26332
  //#endregion
26331
26333
  //#region src/workflow/skills.ts
26332
- const fail = (origin, message) => new GtdError(`gtd config:\n - ${origin}: ${message}`);
26334
+ const fail = (origin, message) => new GtdError(`gtd: config:\n - ${origin}: ${message}`);
26333
26335
  const checkShape = (value) => {
26334
26336
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("skills export must return an object of scope -> skill names");
26335
26337
  for (const [scope, names] of Object.entries(value)) if (!Array.isArray(names) || names.some((n) => typeof n !== "string")) throw new Error(`skills export: "${scope}" must be an array of skill names`);
@@ -26518,10 +26520,10 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
26518
26520
  const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
26519
26521
  if (environmentOnly.length > 0) return yield* fail$3(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it as a workflow option in gtd.config.ts or with a GTD_<NAME> environment variable`));
26520
26522
  const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
26521
- if (undeclared.length > 0) return 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
@@ -27113,7 +27114,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
27113
27114
  const step = outcome.rest;
27114
27115
  yield* (yield* Narrator).narrate(`rest resolved: ${step.name} (awaits ${step.actor})`);
27115
27116
  const stepDef = stepDefOf(step);
27116
- if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
27117
+ if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
27117
27118
  const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
27118
27119
  status: normalizeStatus(e.status),
27119
27120
  path: e.path
@@ -27143,7 +27144,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
27143
27144
  });
27144
27145
  const currentRest = restAt(void 0);
27145
27146
  /**
27146
- * Whether `gtd --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
  */
@@ -27665,7 +27666,7 @@ const emitScripts = (required = [], optional = []) => ({
27665
27666
  optional: assembleScript(optional)
27666
27667
  });
27667
27668
  /** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
27668
- const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn";
27669
+ const DID_NOT_RUN_COMMENT = "# gtd emitted this and did NOT run it — run it with `sh` to apply it";
27669
27670
  const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip";
27670
27671
  const PRESENTATION_FAILURE_WARNING = "printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2";
27671
27672
  /**
@@ -27960,6 +27961,21 @@ const writeJson = (json, out, document) => gen(function* () {
27960
27961
  else yield* writeSelection(out, document, json.path);
27961
27962
  return true;
27962
27963
  });
27964
+ /** Exactly one trailing newline: a script embedded in a `--json` document is never re-normalized at its tail. */
27965
+ const normalizeScriptNewline = (script) => script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`;
27966
+ /**
27967
+ * A script-printing command's output: the script itself (or `plain`, when
27968
+ * there is none to run), `{ schema, script }` under `--json`, or one field.
27969
+ */
27970
+ const writeScript = (json, out, script, plain = script) => gen(function* () {
27971
+ if (json.kind === "off") {
27972
+ out.write(plain);
27973
+ return;
27974
+ }
27975
+ const fields = scriptFields(normalizeScriptNewline(script));
27976
+ if (json.kind === "document") out.write(renderScriptJson(fields));
27977
+ else yield* writeSelection(out, fields, json.path);
27978
+ });
27963
27979
  //#endregion
27964
27980
  //#region src/cli/Strays.ts
27965
27981
  const resolveSide = (git, side) => git.resolveRef(`${side}^{commit}`).pipe(mapError$1(() => new GtdError(`gtd strays: unknown revision '${side}'`)));
@@ -28054,9 +28070,7 @@ const runHistoryShowCommand = (ref, json, out) => gen(function* () {
28054
28070
  out.write(`${lines.join("\n")}\n`);
28055
28071
  });
28056
28072
  /** Prints the script and never runs git: gtd's promise that it only decides and prints. */
28057
- const runHistoryFetchCommand = (remote, out) => 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,16 +28850,19 @@ const runLandCommand = (opts, json, out) => gen(function* () {
28832
28850
  * `base` renders to something that is not an ancestor of (and differs from)
28833
28851
  * HEAD.
28834
28852
  */
28835
- const 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
- if (resolved === void 0) return yield* fail$3(new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — startable: ${config.workflowNames.join(", ")}`));
28856
+ if (resolved === void 0) {
28857
+ const startable = config.workflowNames.length > 0 ? `startable: ${config.workflowNames.join(", ")}` : "this config exports no workflow besides its default, which an ordinary start runs";
28858
+ return yield* fail$3(new GtdUsageError(`${commandLabel}: unknown workflow "${workflow}" — ${startable}`));
28859
+ }
28839
28860
  const rest = yield* restAt(void 0, varOverrides);
28840
28861
  const started = yield* startRefusal(rest, resolved, varOverrides);
28841
28862
  const plan = yield* planStart({
28842
28863
  state: rest.state,
28843
28864
  idle: noProcessUnderway(rest)
28844
- }, actor, {
28865
+ }, "human", {
28845
28866
  workflow: resolved,
28846
28867
  first: started,
28847
28868
  commandLabel,
@@ -28849,51 +28870,79 @@ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => ge
28849
28870
  config: rest.config
28850
28871
  });
28851
28872
  if (plan.kind === "refusal") return yield* fail$3(new Error(plan.message));
28852
- out.write(landingScript(ScriptSurface.render(plan.steps)));
28873
+ yield* writeScript(json, out, listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes));
28853
28874
  });
28854
- const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
28855
28875
  /**
28856
- * `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
28857
- * process settings its `vars` derive from the positional args. A wrong name or
28858
- * arity is a usage error naming what would have worked.
28876
+ * A start commits the whole tree (`git add -A`), so its script says up front
28877
+ * which paths that is. Comment lines only: control characters are masked so a
28878
+ * path can never end the comment and become a command.
28859
28879
  */
28860
- const runDoorCommand = (name, args, out) => gen(function* () {
28861
- const config = yield* (yield* ConfigService).load;
28862
- const door = config.doors.get(name);
28863
- if (door === void 0) {
28864
- const known = [...config.doors];
28865
- return yield* fail$3(new GtdUsageError(`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`));
28866
- }
28880
+ const listingCommitted = (script, changes) => {
28881
+ if (changes.length === 0) return script;
28882
+ const listing = ["# commits every change in the working tree (git add -A):", ...changes.map((change) => `# ${change.status} ${change.path.replace(/\p{Cc}/gu, "?")}`)].join("\n");
28883
+ const at = script.indexOf("\n\n");
28884
+ return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`;
28885
+ };
28886
+ const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
28887
+ /** A door's start: its args mapped to process settings, `extra` (`--var`) pinned on top. */
28888
+ const startThroughDoor = (name, door, args, extra, json, out) => gen(function* () {
28889
+ const label = `gtd start ${name}`;
28867
28890
  const declared = door.args ?? [];
28868
28891
  const required = declared.filter((arg) => arg.optional !== true).length;
28869
- if (args.length < required || args.length > declared.length) return yield* fail$3(new GtdUsageError(`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)}`));
28870
28893
  const named = {};
28871
28894
  declared.forEach((arg, i) => {
28872
28895
  named[arg.name] = args[i];
28873
28896
  });
28874
28897
  const vars = yield* try_({
28875
28898
  try: () => door.vars?.(named) ?? {},
28876
- catch: (e) => /* @__PURE__ */ new Error(`gtd door ${name}: ${e instanceof Error ? e.message : String(e)}`)
28899
+ catch: (e) => /* @__PURE__ */ new Error(`${label}: ${e instanceof Error ? e.message : String(e)}`)
28877
28900
  });
28878
- yield* runStartCommand("human", door.workflow, { ...vars }, out, `gtd door ${name}`);
28901
+ yield* startWorkflow(door.workflow, {
28902
+ ...vars,
28903
+ ...extra
28904
+ }, json, out, label);
28879
28905
  });
28880
- /** `gtd doors`: every door and its args, sorted by name — plain lines, or one JSON array. */
28881
- const runDoorsCommand = (json, out) => gen(function* () {
28882
- const doors = [...(yield* (yield* ConfigService).load).doors];
28883
- if (json.kind === "off") {
28884
- out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""));
28885
- return;
28886
- }
28887
- const document = doors.map(([n, d]) => ({
28888
- name: n,
28889
- workflow: d.workflow,
28890
- 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) => ({
28891
28939
  name: arg.name,
28892
28940
  optional: arg.optional === true
28893
28941
  }))
28894
- }));
28895
- if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
28896
- 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(""));
28897
28946
  });
28898
28947
  /**
28899
28948
  * Ends the process underway without completing it. Nothing is discarded: a
@@ -28908,7 +28957,7 @@ const runDoorsCommand = (json, out) => gen(function* () {
28908
28957
  * Idempotent at the initial state. The one refusal is a process whose first
28909
28958
  * commit is the repository's root commit: nothing earlier to rewind to.
28910
28959
  */
28911
- const runAbandonCommand = (out) => gen(function* () {
28960
+ const runAbandonCommand = (json, out) => gen(function* () {
28912
28961
  const git = yield* GitService;
28913
28962
  const initial = (yield* (yield* ConfigService).load).workflow.initial;
28914
28963
  const run = yield* currentRun;
@@ -28917,8 +28966,7 @@ const runAbandonCommand = (out) => gen(function* () {
28917
28966
  kind: "outcome",
28918
28967
  command: abandonNoopOutcome(initial)
28919
28968
  }]).required;
28920
- out.write(combinedScript(required, ""));
28921
- return;
28969
+ return yield* writeScript(json, out, combinedScript(required, ""));
28922
28970
  }
28923
28971
  const restState = run.trace[run.trace.length - 1].state;
28924
28972
  if (run.startParentHash === EMPTY_TREE) return yield* fail$3(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
@@ -28936,7 +28984,7 @@ const runAbandonCommand = (out) => gen(function* () {
28936
28984
  command: abandonedOutcome(restState, run.startParentHash, initial)
28937
28985
  }
28938
28986
  ]).required;
28939
- out.write(combinedScript(required, ""));
28987
+ yield* writeScript(json, out, combinedScript(required, ""));
28940
28988
  });
28941
28989
  /**
28942
28990
  * `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
@@ -28945,7 +28993,7 @@ const runAbandonCommand = (out) => gen(function* () {
28945
28993
  * refuses on a dirty working tree, no retained history, or HEAD having
28946
28994
  * advanced past the retained tip with commits that would be lost.
28947
28995
  */
28948
- const runRestoreCommand = (out) => gen(function* () {
28996
+ const runRestoreCommand = (json, out) => gen(function* () {
28949
28997
  const git = yield* GitService;
28950
28998
  if ((yield* currentRest).changes.length > 0) return yield* fail$3(/* @__PURE__ */ new Error("gtd restore: refuses on a dirty working tree — commit, stash, or discard your changes first."));
28951
28999
  const retained = yield* readRetainedHistory(git);
@@ -28969,7 +29017,7 @@ const runRestoreCommand = (out) => gen(function* () {
28969
29017
  command: restoredOutcome(tip, after.state)
28970
29018
  }
28971
29019
  ]).required;
28972
- out.write(combinedScript(required, ""));
29020
+ yield* writeScript(json, out, combinedScript(required, ""));
28973
29021
  });
28974
29022
  /**
28975
29023
  * The mode's own validate command for `file`: a declared `validate:` command
@@ -29028,26 +29076,35 @@ const runNextCommand = (json, out) => gen(function* () {
29028
29076
  else if (json.kind === "select") yield* writeSelection(out, fields, json.path);
29029
29077
  else {
29030
29078
  const validateCommand = emitsValidatablePrompt(rendered) ? selfValidateCommand(rest.def, rest.state, rendered.mode, rendered.file) : void 0;
29079
+ if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
29080
+ const shown = renderBeatPlain({
29081
+ ...fields,
29082
+ kind: "message"
29083
+ });
29084
+ yield* narrator.warn(shown.slice(0, shown.indexOf("\n\n")));
29085
+ }
29031
29086
  out.write(renderBeatPlain(fields, validateCommand));
29032
29087
  }
29033
29088
  });
29034
29089
  /** Emits the format-then-validate script for the driver to run; reads no file and executes nothing itself. The verdict lives in that script's future exit code, which prints the complete fix prompt rather than bare findings. */
29035
- const runValidateCommand = (out) => gen(function* () {
29090
+ const runValidateCommand = (json, out) => gen(function* () {
29036
29091
  const rest = yield* currentRest;
29037
- const resolved = yield* resolveValidateScript(rest);
29038
- 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`);
29039
29094
  });
29040
29095
  /**
29041
29096
  * Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
29042
29097
  * Resolves no workflow state and reads no config — this is what a seeded
29043
29098
  * `validate:` command invokes as a leaf step, and it formats nothing.
29044
29099
  *
29045
- * An absent file exits 0, like `gtd validate`. Findings ride the failing
29046
- * 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.
29047
29104
  */
29048
29105
  const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(function* () {
29049
29106
  const format = steeringFormatFor(mode);
29050
- if (format === void 0) return yield* fail$3(/* @__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(", ")}`));
29051
29108
  if (openThreadsFlag) {
29052
29109
  if (openQuestions) return yield* fail$3(/* @__PURE__ */ new Error("gtd check: --open-threads and --open-questions are mutually exclusive"));
29053
29110
  if (format.openThreads === void 0) {
@@ -29060,11 +29117,15 @@ const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(func
29060
29117
  if (mode !== "qa") return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: --open-questions only applies to mode "qa" — got "${mode}"`));
29061
29118
  return yield* runOpenQuestionsCheckCommand(format, file);
29062
29119
  }
29063
- const content = (yield* Workspace).atPath(file);
29064
- 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));
29065
29122
  if (errors.length === 0) return;
29066
29123
  return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s)):\n` + errors.join("\n")));
29067
29124
  });
29125
+ const existingFile = (file) => flatMap$2(Workspace, (workspace) => {
29126
+ const content = workspace.atPath(file);
29127
+ return content === void 0 ? fail$3(/* @__PURE__ */ new Error(`gtd check: no such file: ${file}`)) : succeed$3(content);
29128
+ });
29068
29129
  /**
29069
29130
  * The bare `gtd check --open-threads`: code-comment threads in the files the
29070
29131
  * current process changed. The one `check` form that reads process state; no
@@ -29082,23 +29143,18 @@ const runCodeThreadsCheckCommand = () => gen(function* () {
29082
29143
  if (lines.length === 0) return;
29083
29144
  return yield* fail$3(new Error(lines.join("\n")));
29084
29145
  });
29085
- /** Prints one `file:line: [^name]: <first H entry>` line per open thread; a missing file fails, like `--open-questions`. */
29146
+ /** Prints one `file:line: [^name]: <first H entry>` line per open thread. */
29086
29147
  const runOpenThreadsCheckCommand = (format, file) => gen(function* () {
29087
- const content = (yield* Workspace).atPath(file);
29088
- if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
29089
- const errors = openThreads(format, content).map((t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`);
29148
+ const errors = openThreads(format, yield* existingFile(file)).map((t) => `${file}:${t.line + 1}: [^${t.name}]: ${t.firstMe}`);
29090
29149
  if (errors.length === 0) return;
29091
29150
  return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open thread(s) in ${file}:\n` + errors.join("\n")));
29092
29151
  });
29093
29152
  /**
29094
29153
  * Runs the SAME `unansweredQuestions` predicate a flow's `openQuestions()`
29095
- * reads, so the gate script and the flow's own check stay in sync. A missing
29096
- * file is a non-zero exit here, unlike the structural path above.
29154
+ * reads, so the gate script and the flow's own check stay in sync.
29097
29155
  */
29098
29156
  const runOpenQuestionsCheckCommand = (format, file) => gen(function* () {
29099
- const content = (yield* Workspace).atPath(file);
29100
- if (content === void 0) return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${file} does not exist`));
29101
- const errors = unansweredQuestions(format, content).map((q) => `${file}:${q.headingLine + 1}: ${q.question}`);
29157
+ const errors = unansweredQuestions(format, yield* existingFile(file)).map((q) => `${file}:${q.headingLine + 1}: ${q.question}`);
29102
29158
  if (errors.length === 0) return;
29103
29159
  return yield* fail$3(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open question(s) unanswered in ${file}:\n` + errors.join("\n")));
29104
29160
  });
@@ -29183,12 +29239,13 @@ const runUiCliCommand = (command, out) => gen(function* () {
29183
29239
  * cwds (e.g. macOS /tmp → /private/tmp) match.
29184
29240
  */
29185
29241
  const assertRunningFromRepoRoot = (git, host) => gen(function* () {
29186
- const topLevel = yield* git.topLevel();
29187
- if ((yield* host.realPath(topLevel)) !== (yield* host.realPath(host.root))) return yield* fail$3(/* @__PURE__ */ new Error(`gtd must be run from the repository root (${topLevel}); the current directory is ${host.root}`));
29242
+ const topLevel = yield* git.topLevel().pipe(mapError$1((e) => /not a git repository/.test(e.message) ? /* @__PURE__ */ new Error("gtd: not a git repository — run gtd inside one (`git init` makes one)") : e));
29243
+ if ((yield* host.realPath(topLevel)) !== (yield* host.realPath(host.root))) return yield* fail$3(/* @__PURE__ */ new Error(`gtd: run gtd from the repository root — cd ${shellWord(topLevel)} (the current directory is ${host.root})`));
29188
29244
  });
29245
+ const shellWord = (path) => /^[\w@%+=:,./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`;
29189
29246
  /** A command that derives workflow state needs at least one commit too — a commitless repository has no HEAD to resolve a `Rest` at. */
29190
29247
  const assertRepositoryHasCommits = (git) => gen(function* () {
29191
- if (!(yield* git.hasCommits())) return yield* fail$3(/* @__PURE__ */ new Error("gtd requires a repository with at least one commit — make an initial commit, then run gtd again"));
29248
+ if (!(yield* git.hasCommits())) return yield* fail$3(/* @__PURE__ */ new Error("gtd: the repository has no commits yet — make an initial commit, then run gtd again"));
29192
29249
  });
29193
29250
  /**
29194
29251
  * What a command kind needs before it may run. Defined here, not in
@@ -29200,7 +29257,7 @@ const assertRepositoryHasCommits = (git) => gen(function* () {
29200
29257
  const needsOf = (kind) => {
29201
29258
  switch (kind) {
29202
29259
  case "lsp":
29203
- case "install":
29260
+ case "guide":
29204
29261
  case "judgeRun":
29205
29262
  case "strays": return "none";
29206
29263
  case "check":
@@ -29226,18 +29283,16 @@ const dispatchVoidCommand = (command, json, out) => {
29226
29283
  ...command.cost !== void 0 ? { cost: command.cost } : {},
29227
29284
  ...command.model !== void 0 ? { model: command.model } : {}
29228
29285
  }, json, out);
29229
- case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
29230
- case "door": return runDoorCommand(command.name, command.args, out);
29231
- case "doors": return runDoorsCommand(json, out);
29232
- case "abandon": return runAbandonCommand(out);
29233
- case "restore": return runRestoreCommand(out);
29286
+ case "start": return runStartCommand(command, json, out);
29287
+ case "abandon": return runAbandonCommand(json, out);
29288
+ case "restore": return runRestoreCommand(json, out);
29234
29289
  case "next": return runNextCommand(json, out);
29235
- case "validate": return runValidateCommand(out);
29290
+ case "validate": return runValidateCommand(json, out);
29236
29291
  case "check":
29237
29292
  if (command.mode === void 0 || command.file === void 0) return runCodeThreadsCheckCommand();
29238
29293
  return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false);
29239
29294
  case "uncheck": return runUncheckCommand(command.file);
29240
- case "install": return runInstallCommand(out);
29295
+ case "guide": return runGuideCommand(out);
29241
29296
  case "summary": return runSummaryCommand(out);
29242
29297
  case "base": return runBaseCommand(out);
29243
29298
  case "judge": return runJudgeCommand(json, out);
@@ -29247,7 +29302,7 @@ const dispatchVoidCommand = (command, json, out) => {
29247
29302
  case "historyShow": return runHistoryShowCommand(command.ref, json, out);
29248
29303
  case "historyMetrics": return runHistoryMetricsCommand(command.refs, json, out);
29249
29304
  case "historyJudges": return runHistoryJudgesCommand(command.refs, command.vars, command.jsonl, json, out);
29250
- case "historyFetch": return runHistoryFetchCommand(command.remote, out);
29305
+ case "historyFetch": return runHistoryFetchCommand(command.remote, json, out);
29251
29306
  }
29252
29307
  };
29253
29308
  /**
@@ -29275,18 +29330,99 @@ const runCommand$1 = (command, json, out) => {
29275
29330
  });
29276
29331
  };
29277
29332
  //#endregion
29333
+ //#region src/cli/Help.ts
29334
+ const HELP_WIDTH = 80;
29335
+ /**
29336
+ * Word-wraps prose to `width` columns, every line prefixed by `indent`.
29337
+ * Paragraphs are separated by a blank line; a paragraph whose lines start
29338
+ * with two spaces is an example and is kept verbatim (indented, not wrapped).
29339
+ */
29340
+ const wrapParagraph = (paragraph, width, indent) => {
29341
+ const lines = [];
29342
+ let line = "";
29343
+ for (const word of paragraph.match(/\S*`[^`]*`\S*|\S+/g) ?? []) if (line !== "" && indent.length + line.length + 1 + word.length > width) {
29344
+ lines.push(indent + line);
29345
+ line = word;
29346
+ } else line = line === "" ? word : `${line} ${word}`;
29347
+ if (line !== "") lines.push(indent + line);
29348
+ return lines;
29349
+ };
29350
+ const wrap = (text, width = HELP_WIDTH, indent = "") => text.split("\n\n").flatMap((paragraph, i) => [...i > 0 ? [""] : [], ...paragraph.startsWith(" ") ? paragraph.split("\n").map((l) => indent + l) : wrapParagraph(paragraph, width, indent)]);
29351
+ /** `term` and `description` side by side, the description wrapped into the column after `column`; a term too wide for it gets a line of its own. */
29352
+ const definition = (term, description, column) => {
29353
+ const cell = ` ${term}`;
29354
+ const body = wrap(description, HELP_WIDTH, " ".repeat(column));
29355
+ if (cell.length + 1 >= column) return [cell, ...body];
29356
+ const [first = "", ...rest] = body;
29357
+ return [cell.padEnd(column) + first.trimStart(), ...rest];
29358
+ };
29359
+ /** Optimal-string-alignment distance: Levenshtein plus adjacent transpositions, so `nxet` is one edit from `next`. */
29360
+ const distance = (a, b) => {
29361
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
29362
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
29363
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
29364
+ let best = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
29365
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, d[i - 2][j - 2] + 1);
29366
+ d[i][j] = best;
29367
+ }
29368
+ return d[a.length][b.length];
29369
+ };
29370
+ /** The candidate closest to `word`, when it is a plausible typo of it: at most one edit per three characters, and at least one. */
29371
+ const closest = (word, candidates) => {
29372
+ const limit = Math.max(1, Math.floor(word.length / 3));
29373
+ let best;
29374
+ for (const name of candidates) {
29375
+ const d = distance(word, name);
29376
+ if (d <= limit && (best === void 0 || d < best.d)) best = {
29377
+ name,
29378
+ d
29379
+ };
29380
+ }
29381
+ return best?.name;
29382
+ };
29383
+ //#endregion
29278
29384
  //#region src/cli/Cli.ts
29385
+ /** `<name>=<value>` occurrences of `flag` → one record, each name a setting name given at most once. */
29386
+ const decodeAssignments = (flag, raws) => {
29387
+ const vars = {};
29388
+ for (const raw of raws) {
29389
+ const eq = raw.indexOf("=");
29390
+ if (eq <= 0) return left(`gtd: ${flag} must be <name>=<value> with a non-empty name — got "${raw}"`);
29391
+ const name = raw.slice(0, eq);
29392
+ const value = raw.slice(eq + 1);
29393
+ if (!isSettingName(name)) return left(`gtd: ${flag} ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
29394
+ if (/[\r\n]/.test(value)) return left(`gtd: ${flag} ${name} must be a single-line value`);
29395
+ if (Object.hasOwn(vars, name)) return left(`gtd: ${flag} ${name} specified more than once`);
29396
+ if (!name.startsWith("flag_")) {
29397
+ vars[name] = value;
29398
+ continue;
29399
+ }
29400
+ const pinned = flagSettingValue(value);
29401
+ if (pinned === void 0) return left(`gtd: ${flag} ${name} must be 1, 0, true or false`);
29402
+ vars[name] = pinned;
29403
+ }
29404
+ return right(vars);
29405
+ };
29279
29406
  const nonNegativeNumber = (raw, flag) => {
29280
29407
  const n = Number(raw);
29281
29408
  if (raw.trim() === "" || !Number.isFinite(n) || n < 0) return left(`gtd: ${flag} must be a non-negative number — got "${raw}"`);
29282
29409
  return right(n);
29283
29410
  };
29411
+ /** The commands whose only output is a script: `--json` prints `{ schema, script }`. */
29412
+ const SCRIPT_KINDS = /* @__PURE__ */ new Set([
29413
+ "start",
29414
+ "abandon",
29415
+ "restore",
29416
+ "validate",
29417
+ "historyFetch"
29418
+ ]);
29419
+ const SCRIPT_JSON_HELP = "Print { schema, script }, or with --json=script the script alone";
29284
29420
  const JSON_KINDS = /* @__PURE__ */ new Set([
29421
+ ...SCRIPT_KINDS,
29285
29422
  "next",
29286
29423
  "land",
29287
29424
  "judge",
29288
29425
  "judgeAnswer",
29289
- "doors",
29290
29426
  "historyList",
29291
29427
  "historyShow",
29292
29428
  "historyMetrics",
@@ -29300,22 +29436,14 @@ const FLAGS = [
29300
29436
  repeatable: false,
29301
29437
  scope: (kind) => JSON_KINDS.has(kind),
29302
29438
  decode: ([raw]) => right(raw ?? ""),
29303
- scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`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",
29304
29440
  valueHint: "<path>",
29305
- help: [
29306
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
29307
- "list/gtd history show/gtd history metrics/gtd history judges/gtd strays only) output structured JSON. Bare --json prints the whole document;",
29308
- "--json=<path> (a dotted key path into that document, e.g.",
29309
- "kind, content, session.id) prints just that value: a",
29310
- "scalar raw and unquoted, a boolean as true/false, a list",
29311
- "one JSON entry per line — a numeric segment (e.g.",
29312
- "skills.0) indexes into one entry of a list field instead.",
29313
- "An absent optional field prints nothing and exits 0 —",
29314
- "including when an earlier segment of <path> is itself",
29315
- "absent/null (e.g. session.id at a non-prompt rest), which",
29316
- "never counts as unknown; an unknown path is a usage error",
29317
- "(exit 2)."
29318
- ]
29441
+ help: "Print the JSON document, or with --json=<path> one field of it (e.g. --json=kind): a scalar bare, a list one entry per line, nothing for an absent field",
29442
+ helpFor: {
29443
+ strays: "Print one entry of the array (--json=0), nothing past its end",
29444
+ ...Object.fromEntries([...SCRIPT_KINDS].map((kind) => [kind, SCRIPT_JSON_HELP])),
29445
+ start: `${SCRIPT_JSON_HELP}. Bare, print the list as a JSON array of { kind, name } (a door adds workflow and args)`
29446
+ }
29319
29447
  },
29320
29448
  {
29321
29449
  name: "--port",
@@ -29328,11 +29456,7 @@ const FLAGS = [
29328
29456
  },
29329
29457
  scopeError: "gtd: --port is only valid for `gtd ui`",
29330
29458
  valueHint: "<n>",
29331
- help: [
29332
- "(gtd ui only) the tailscale serve port (default: the first",
29333
- "free of 8443, 10000, 443), or the bind port when --host",
29334
- "opts out of serve (default: a free port)"
29335
- ]
29459
+ help: "The tailscale serve port (default: the first free of 8443, 10000, 443), or with --host the port to bind (default: any free one)"
29336
29460
  },
29337
29461
  {
29338
29462
  name: "--host",
@@ -29342,11 +29466,7 @@ const FLAGS = [
29342
29466
  decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --host must be a non-empty, single-line value") : right(raw),
29343
29467
  scopeError: "gtd: --host is only valid for `gtd ui`",
29344
29468
  valueHint: "<addr>",
29345
- help: [
29346
- "(gtd ui only) opt out of the default tailscale serve front",
29347
- "door and bind this address directly instead, showing it in",
29348
- "the printed URL"
29349
- ]
29469
+ help: "Bind this address directly instead of publishing through tailscale serve"
29350
29470
  },
29351
29471
  {
29352
29472
  name: "--self-signed",
@@ -29356,7 +29476,7 @@ const FLAGS = [
29356
29476
  decode: () => right(true),
29357
29477
  scopeError: "gtd: --self-signed is only valid for `gtd ui`",
29358
29478
  valueHint: "",
29359
- help: ["(gtd ui only) generate a throwaway self-signed TLS", "certificate instead of the configured ui.cert/ui.key"]
29479
+ help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key"
29360
29480
  },
29361
29481
  {
29362
29482
  name: "--dev",
@@ -29366,7 +29486,7 @@ const FLAGS = [
29366
29486
  decode: () => right(true),
29367
29487
  scopeError: "gtd: --dev is only valid for `gtd ui`",
29368
29488
  valueHint: "",
29369
- help: ["(gtd ui only) run against local development sources", "instead of the packaged build"]
29489
+ help: "Serve local development sources instead of the packaged build"
29370
29490
  },
29371
29491
  {
29372
29492
  name: "--provider",
@@ -29376,11 +29496,7 @@ const FLAGS = [
29376
29496
  decode: ([raw]) => raw === "fixed" || raw === "jev" || raw === "llm" ? right(raw) : left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
29377
29497
  scopeError: "gtd: --provider is only valid for `gtd judge run`",
29378
29498
  valueHint: "<name>",
29379
- help: [
29380
- "(gtd judge run only) the answerer: fixed | jev | llm;",
29381
- "omitted: GTD_JUDGE_PROVIDER, then judge.json, then jev",
29382
- "when TYPESAFE_API_KEY is set, else llm"
29383
- ]
29499
+ help: "The answerer: fixed, jev or llm (default: $GTD_JUDGE_PROVIDER, then ~/.config/gtd/judge.json, then jev when $TYPESAFE_API_KEY is set, else llm)"
29384
29500
  },
29385
29501
  {
29386
29502
  name: "--answers",
@@ -29390,11 +29506,7 @@ const FLAGS = [
29390
29506
  decode: ([raw]) => raw === void 0 || raw === "" ? left("gtd: --answers must be a non-empty path") : right(raw),
29391
29507
  scopeError: "gtd: --answers is only valid for `gtd judge run`",
29392
29508
  valueHint: "<path>",
29393
- help: [
29394
- "(gtd judge run --provider fixed only) a JSON verdict array",
29395
- "[{ id, answer, p }]; else the GTD_JUDGE_ANSWERS env var",
29396
- "(inline JSON) is read"
29397
- ]
29509
+ help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)"
29398
29510
  },
29399
29511
  {
29400
29512
  name: "--cost",
@@ -29404,7 +29516,7 @@ const FLAGS = [
29404
29516
  decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
29405
29517
  scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
29406
29518
  valueHint: "<n>",
29407
- help: ["(gtd land only) record the invocation's token cost"]
29519
+ help: "Record the token cost of the invocation that just finished on the commit"
29408
29520
  },
29409
29521
  {
29410
29522
  name: "--model",
@@ -29412,61 +29524,33 @@ const FLAGS = [
29412
29524
  repeatable: false,
29413
29525
  scope: (kind) => kind === "land" || kind === "judgeRun",
29414
29526
  decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --model must be a non-empty, single-line value") : right(raw),
29415
- scopeError: "gtd: --model is only valid for `gtd land` (with --cost) or `gtd judge run`",
29527
+ scopeError: "gtd: --model is only valid for `gtd land` (with --cost: the model to record the cost against) or `gtd judge run` (the model that answers)",
29416
29528
  valueHint: "<name>",
29417
- help: [
29418
- "(gtd land, with --cost) tag that cost's model",
29419
- "(gtd judge run, llm answerer) the claude model; omitted:",
29420
- "GTD_JUDGE_MODEL, then judge.json, then sonnet"
29421
- ]
29529
+ help: "",
29530
+ helpFor: {
29531
+ land: "With --cost: the model that cost was spent on — recorded on the commit, nothing runs on it",
29532
+ judgeRun: "The claude model the llm answerer uses (default: $GTD_JUDGE_MODEL, then judge.json, then sonnet)"
29533
+ }
29422
29534
  },
29423
29535
  {
29424
- name: "--workflow",
29536
+ name: "--threshold",
29425
29537
  arity: 1,
29426
- repeatable: false,
29427
- scope: (kind) => kind === "start",
29428
- decode: ([raw]) => right(raw ?? ""),
29429
- scopeError: "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` form; landing and starting are different verbs",
29430
- valueHint: "<name>",
29431
- help: ["(with no command at all) start a brand new process on", "the named workflow — authenticated as human"]
29538
+ repeatable: true,
29539
+ scope: (kind) => kind === "historyJudges",
29540
+ decode: (raws) => decodeAssignments("--threshold", raws),
29541
+ scopeError: "gtd: --threshold is only valid for `gtd history judges`",
29542
+ valueHint: "<name>=<value>",
29543
+ help: "Re-decide every past verdict under this threshold: judgeIdenticalMinP or qualityTriageMinP (repeatable)"
29432
29544
  },
29433
29545
  {
29434
29546
  name: "--var",
29435
29547
  arity: 1,
29436
29548
  repeatable: true,
29437
- scope: (kind) => kind === "start" || kind === "historyJudges",
29438
- decode: (raws) => {
29439
- const vars = {};
29440
- const seen = /* @__PURE__ */ new Set();
29441
- for (const raw of raws) {
29442
- const eq = raw.indexOf("=");
29443
- if (eq <= 0) return left(`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`);
29444
- const name = raw.slice(0, eq);
29445
- const value = raw.slice(eq + 1);
29446
- if (!isSettingName(name)) return left(`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
29447
- if (/[\r\n]/.test(value)) return left(`gtd: --var ${name} must be a single-line value`);
29448
- if (seen.has(name)) return left(`gtd: --var ${name} specified more than once`);
29449
- seen.add(name);
29450
- if (name.startsWith("flag_")) {
29451
- const pinned = flagSettingValue(value);
29452
- if (pinned === void 0) return left(`gtd: --var ${name} must be 1, 0, true or false`);
29453
- vars[name] = pinned;
29454
- continue;
29455
- }
29456
- vars[name] = value;
29457
- }
29458
- return right(vars);
29459
- },
29460
- scopeError: "gtd: --var requires --workflow (or `gtd history judges`)",
29549
+ scope: (kind) => kind === "start",
29550
+ decode: (raws) => decodeAssignments("--var", raws),
29551
+ scopeError: "gtd: --var is only valid for `gtd start <name>`",
29461
29552
  valueHint: "<name>=<value>",
29462
- help: [
29463
- "(with --workflow; repeatable) pin a process setting for the",
29464
- "new process; the name must already be declared by the",
29465
- "workflow's defaults (environment settings are not",
29466
- "pinnable). (gtd history judges) re-decide past verdicts",
29467
- "under this threshold: judgeIdenticalMinP or",
29468
- "qualityTriageMinP"
29469
- ]
29553
+ help: "Pin one of the workflow's declared settings for the new process (repeatable); through a door, on top of the settings its args set"
29470
29554
  },
29471
29555
  {
29472
29556
  name: "--jsonl",
@@ -29476,8 +29560,8 @@ const FLAGS = [
29476
29560
  decode: () => right(true),
29477
29561
  scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
29478
29562
  valueHint: "",
29479
- help: ["(gtd history judges only) print one JSON line per verdict", "instead of the report"],
29480
- conflicts: ["--json", "--var"]
29563
+ help: "Print one JSON line per verdict instead of the report",
29564
+ conflicts: ["--json", "--threshold"]
29481
29565
  },
29482
29566
  {
29483
29567
  name: "--open-questions",
@@ -29487,12 +29571,7 @@ const FLAGS = [
29487
29571
  decode: () => right(true),
29488
29572
  scopeError: "gtd: --open-questions is only valid for `gtd check`",
29489
29573
  valueHint: "",
29490
- help: [
29491
- "(gtd check only) ignore <mode>'s structural findings and",
29492
- "instead run the qa open-questions predicate over <file>,",
29493
- "printing each unanswered question one per line and exiting",
29494
- "non-zero when any remain"
29495
- ]
29574
+ help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain"
29496
29575
  },
29497
29576
  {
29498
29577
  name: "--open-threads",
@@ -29502,15 +29581,7 @@ const FLAGS = [
29502
29581
  decode: () => right(true),
29503
29582
  scopeError: "gtd: --open-threads is only valid for `gtd check`",
29504
29583
  valueHint: "",
29505
- help: [
29506
- "(gtd check only) ignore <mode>'s structural findings and",
29507
- "instead list every open thread (a footnote whose last",
29508
- "entry is the agent's) over <file>, one per line with its",
29509
- "first H entry, exiting non-zero when any are open. With no",
29510
- "<mode>/<file>, lists code-comment threads in the files the",
29511
- "current process changed (read from its diff base, unlike",
29512
- "the rest of `gtd check`)"
29513
- ]
29584
+ help: "Instead of <mode>'s findings, list the open threads in <file>; exits 1 when any are open. Alone, lists the open code-comment threads in the files the current process changed"
29514
29585
  },
29515
29586
  {
29516
29587
  name: "--verbose",
@@ -29520,11 +29591,7 @@ const FLAGS = [
29520
29591
  decode: () => right(true),
29521
29592
  scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
29522
29593
  valueHint: "",
29523
- help: [
29524
- "enable stderr narration for this invocation: which rest",
29525
- "resolved and how config resolved across layers. Aliased",
29526
- "to -v"
29527
- ]
29594
+ help: "Narrate on stderr what gtd resolved and where its config came from"
29528
29595
  }
29529
29596
  ];
29530
29597
  /**
@@ -29544,277 +29611,92 @@ const flagByToken = (token) => {
29544
29611
  };
29545
29612
  const COMMAND_ROWS = [
29546
29613
  {
29547
- token: "land",
29548
- kind: "land",
29549
- arity: "none",
29550
- details: [
29551
- "Land whatever the tree now shows at the currently resolved",
29552
- "rest — a human capture, an agent/check turn, or an empty",
29553
- "attempt (a fruitless prompt turn). Pass",
29554
- "--cost=<n> (optionally --model=<name>) to record the",
29555
- "just-finished invocation's token cost and model on the",
29556
- "turn commit (summed into it.processCost/",
29557
- "processCostByModel). Plain (the default) prints one",
29558
- "human-readable sentence naming the commit subject (or a",
29559
- "no-op note) plus a pointer to `gtd land --json=script | sh`",
29560
- "to get the landing script — never the script itself.",
29561
- "--json emits script (the actual POSIX sh",
29562
- "a driver runs) alongside settled, idle, state (the",
29563
- "post-land target), subject, cost and model. Exits 0 on",
29564
- "success, 1 on any refusal — see the Exit codes section",
29565
- "below"
29566
- ]
29567
- },
29568
- {
29569
- token: "abandon",
29570
- kind: "abandon",
29571
- arity: "none",
29572
- details: [
29573
- "End the process currently underway without completing it:",
29574
- "rewind HEAD to the commit the process started from,",
29575
- "keeping everything it produced as uncommitted changes. A",
29576
- "no-op when no process is underway"
29577
- ]
29578
- },
29579
- {
29580
- token: "restore",
29581
- kind: "restore",
29614
+ token: "next",
29615
+ kind: "next",
29582
29616
  arity: "none",
29583
- details: [
29584
- "Hard-reset HEAD back to the tip retained by the last",
29585
- "abandon (refs/worktree/gtd/history), bringing back an",
29586
- "abandoned process's turns. Refuses on a dirty working",
29587
- "tree, when there is no retained history, or when HEAD has",
29588
- "advanced past the retained tip with commits that would be",
29589
- "lost"
29590
- ]
29617
+ group: "Core loop",
29618
+ summary: "Print what should happen next",
29619
+ details: `Print what should happen next in the process, changing nothing — safe to poll.
29620
+
29621
+ Plain output is for a person: a status header (the step, who acts, the pending changes, where landing them would go), then the step's content. At a prompt the header goes to stderr, so stdout is exactly the agent's input.
29622
+
29623
+ --json prints one JSON document for a driver: its kind (capture, message, script, prompt or stalled) says what to do, its content what to run or show. --json=<path> prints one field of it; an unknown path is a usage error (exit 2).`
29591
29624
  },
29592
29625
  {
29593
- token: "next",
29594
- kind: "next",
29626
+ token: "land",
29627
+ kind: "land",
29595
29628
  arity: "none",
29596
- details: [
29597
- "Print the resolved rest's beat (no mutation, safe to",
29598
- "poll), in one of three encodings. Plain (the default): a",
29599
- "status summary, a blank line, then the step verbatim — at a",
29600
- "script/capture rest an instruction line ('Run this",
29601
- "script:' / 'The edit is already made — run `gtd land` to",
29602
- "land it.') precedes the status summary; at a prompt rest",
29603
- "it's the bare step (plus the self-validation instruction",
29604
- "when applicable) with no header at all, since those bytes",
29605
- "are the agent's own input. --json emits the one structured",
29606
- "surface gtd has: kind (capture|message|script|prompt|",
29607
- "stalled) selects what a driver does, content is what it",
29608
- "runs or shows, idle marks the workflow's initial state with",
29609
- "a clean tree, plus the prompt session, model, validate",
29610
- "script, log path, changes, next and the resting state's own",
29611
- "fields. --json=<path> (a dotted key path into that same",
29612
- "document, e.g. kind, content, session.id) prints just that",
29613
- "value instead of the whole document — see --json's own help",
29614
- "above. Exits 0 — see the Exit codes section below"
29615
- ]
29629
+ group: "Core loop",
29630
+ summary: "Record what the tree now shows",
29631
+ details: `Record what the working tree now shows at the current step: a person's edit, an agent's or a check's turn, or an empty attempt when an agent changed nothing.
29632
+
29633
+ gtd writes nothing itself. Plain output names the commit it would make; --json=script prints the script that makes it:
29634
+
29635
+ s=$(gtd land --json=script) && sh -c "$s"
29636
+
29637
+ --json prints the whole document: the script, the step the process lands at, the commit subject, and the recorded cost and model. A refusal exits 1.`
29616
29638
  },
29617
29639
  {
29618
29640
  token: "validate",
29619
29641
  kind: "validate",
29620
29642
  arity: "none",
29621
- details: [
29622
- "Print the script that formats (when declared) then",
29623
- "validates the resolved rest's steering file, using its",
29624
- "mode's commands (its file:/mode:), instead of running it —",
29625
- "a driver runs the script and reads the findings from its",
29626
- "own exit code/output. Always exits 0; prints \"nothing to",
29627
- "validate\" when the resolved rest declares nothing to run.",
29628
- "On a non-zero validate exit the emitted script prints a",
29629
- "ready-to-send fix prompt (instruction + findings) and",
29630
- "exits with the validator's own code"
29631
- ]
29643
+ group: "Core loop",
29644
+ summary: "Print the script that checks the steering file",
29645
+ details: `Print the script that formats, then validates, the current step's steering file with its mode's commands. gtd runs nothing: the script's exit code is the verdict, and on a failure it prints a ready-to-send fix prompt. Prints "nothing to validate" when the step declares no file to check; --json=script then prints an empty script.`
29632
29646
  },
29633
29647
  {
29634
- token: "lsp",
29635
- kind: "lsp",
29636
- arity: "none",
29637
- details: ["Start the LSP server for .gtd/ steering files (stdio)"]
29648
+ token: "start",
29649
+ kind: "start",
29650
+ arity: {
29651
+ name: "name",
29652
+ variadic: "args",
29653
+ bare: true
29654
+ },
29655
+ group: "Start and stop",
29656
+ summary: "Start a workflow or door; bare, list them",
29657
+ details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
29658
+
29659
+ <name> is a door — a named shortcut whose positional args set the workflow's settings — or else a workflow the config exports, which takes no args. A door shadows a workflow of the same name. --var pins a setting either way.
29660
+
29661
+ s=$(gtd start <name> --json=script) && sh -c "$s"
29662
+
29663
+ With no <name>, list every workflow and door it can start. An unknown <name>, the wrong number of args, or a --var the workflow does not declare is a usage error (exit 2); a process already underway is a refusal (exit 1).`
29638
29664
  },
29639
29665
  {
29640
- token: "ui",
29641
- kind: "ui",
29666
+ token: "abandon",
29667
+ kind: "abandon",
29642
29668
  arity: "none",
29643
- details: [
29644
- "Expose gtd's web/phone client for THIS worktree — the",
29645
- "invoking directory, never a configured list of roots — and",
29646
- "refuses outside a repository like every other state command.",
29647
- "By default, publishes through `tailscale serve` (reachable",
29648
- "from anywhere on the tailnet, including over a DERP relay)",
29649
- "with tailscaled terminating TLS, walking 8443, then 10000,",
29650
- "then 443 until one publishes; falls back to binding a local",
29651
- "HTTPS server directly, never refusing, when none of the",
29652
- "three works. --host <addr> opts out of serve and binds that",
29653
- "address directly instead; --port <n> overrides the serve",
29654
- "port (default: the first free of 8443, 10000, 443), or the",
29655
- "bind port when --host is given (default: a free port);",
29656
- "--self-signed also opts out of serve, generating a",
29657
- "throwaway TLS certificate instead of the configured",
29658
- "ui.cert/ui.key; --dev runs against local development sources",
29659
- "instead of the packaged build"
29660
- ]
29661
- },
29662
- {
29663
- token: "check",
29664
- kind: "check",
29665
- arity: { names: ["mode", "file"] },
29666
- details: [
29667
- "Read <file> and run the built-in steering format named",
29668
- "<mode> (see `gtd validate`'s modes: qa, review, findings)",
29669
- "over its contents, printing each finding one per line and",
29670
- "exiting non-zero when there are any. Resolves no workflow",
29671
- "state and reads no config — standalone, runnable from any",
29672
- "directory with <mode>/<file> given explicitly. This is what a",
29673
- "workflow's emitted validation script invokes as a leaf step.",
29674
- "--open-questions runs the qa unanswered-questions predicate",
29675
- "instead (see --help); --open-threads lists open threads.",
29676
- "`gtd check --open-threads` alone lists open code-comment",
29677
- "threads (`// H: ...`) in the files the process changed;",
29678
- "it prints nothing when no process is underway."
29679
- ]
29680
- },
29681
- {
29682
- token: "uncheck",
29683
- kind: "uncheck",
29684
- arity: { name: "file" },
29685
- details: [
29686
- "Read <file> and reset every review-mode `- [x]`/`- [X]`",
29687
- "file-pointer box back to `- [ ]`, writing the result back",
29688
- "only when the bytes actually changed. Resolves no workflow",
29689
- "state and reads no config — standalone, runnable from any",
29690
- "directory with <file> given explicitly. This is what the",
29691
- "landing script runs ahead of the commit at the human review",
29692
- "gate, so no tick ever reaches a commit. Takes no <mode> —",
29693
- "it means review-mode file pointers and nothing else, never",
29694
- "qa-mode's answered-question boxes. A missing file writes",
29695
- "nothing and exits 0"
29696
- ]
29669
+ group: "Start and stop",
29670
+ summary: "Stop the process, keeping its changes",
29671
+ details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from and keeps everything it produced as uncommitted changes; \`gtd restore\` undoes it. With no process underway the script does nothing.
29672
+
29673
+ s=$(gtd abandon --json=script) && sh -c "$s"`
29697
29674
  },
29698
29675
  {
29699
- token: "install",
29700
- kind: "install",
29676
+ token: "restore",
29677
+ kind: "restore",
29701
29678
  arity: "none",
29702
- details: [
29703
- "Print a complete, self-contained briefing that teaches an",
29704
- "agent (or a human) to build a gtd driver in any shell or",
29705
- "runtime — the self-serve version of",
29706
- "https://github.com/pmelab/gtd/blob/main/docs/driver.md's",
29707
- "'Writing your own driver', and how to get a workflow: gtd",
29708
- "ships none. It writes nothing."
29709
- ]
29679
+ group: "Start and stop",
29680
+ summary: "Bring back the last abandoned process",
29681
+ details: `Bring back the process the last \`gtd abandon\` stopped: the printed script resets HEAD to that process's last commit. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`
29710
29682
  },
29711
29683
  {
29712
29684
  token: "summary",
29713
29685
  kind: "summary",
29714
29686
  arity: "none",
29715
- details: [
29716
- "Print the prompt for an agent to write the process HEAD",
29717
- "closes or sits inside its own closing message — the entry",
29718
- "commit, each human-authored commit (a review round, an",
29719
- "answered question gate), the diff range to inspect, and",
29720
- "it.processCost/processCostByModel. Writes nothing: no git,",
29721
- "no state transition, no file, no session identity — the",
29722
- "driver pipes the output to a cold agent and does what it",
29723
- "wants with the result (a squash, an amend, a PR body).",
29724
- "Refuses (exit 1) when the workflow declares no summary:",
29725
- "template, or when the resolved run has no commits to name —",
29726
- "it is runnable any time before the next thing lands on the",
29727
- "branch"
29728
- ]
29687
+ group: "Inspect",
29688
+ summary: "Print the prompt for the closing message",
29689
+ details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside: its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
29690
+
29691
+ Refuses when the workflow declares no summary template, or when the process has no commits.`
29729
29692
  },
29730
29693
  {
29731
29694
  token: "base",
29732
29695
  kind: "base",
29733
29696
  arity: "none",
29734
- details: [
29735
- "Print the review anchor hash — the diff base an external",
29736
- "tool (a diff, a PR tool, another agent) should point at —",
29737
- "bare, newline-terminated, and nothing else. Writes nothing:",
29738
- "no git, no state transition, no session identity. Before",
29739
- "the first review round it's the process's diff base;",
29740
- "afterward it's the most-recent review round's boundary.",
29741
- "Refuses (exit 1) when no process is underway."
29742
- ]
29743
- },
29744
- {
29745
- token: "door",
29746
- kind: "door",
29747
- arity: {
29748
- name: "name",
29749
- variadic: "args"
29750
- },
29751
- details: [
29752
- "Start a process through a named shortcut, mapping the",
29753
- "positional args to that workflow's process settings — the",
29754
- "same start `gtd --workflow` makes, printed the same way.",
29755
- "Each workflow the config exports brings its own doors (gtd's",
29756
- "`fix`, `review [base]`, `rollout <flag>` and",
29757
- "`cleanup <flag>`), plus the config's `doors` export. An",
29758
- "unknown door, or too few or too many args, is a usage error",
29759
- "(exit 2) naming the doors or the door's synopsis"
29760
- ]
29761
- },
29762
- {
29763
- token: "doors",
29764
- kind: "doors",
29765
- arity: "none",
29766
- details: [
29767
- "List every door, sorted: one `name [args] → workflow` line",
29768
- "each. --json prints one JSON array of { name, workflow,",
29769
- "args: [{ name, optional }] } instead; --json=<path> reads one",
29770
- "value off it. Writes nothing"
29771
- ]
29772
- },
29773
- {
29774
- token: "judge",
29775
- kind: "judge",
29776
- arity: "none",
29777
- details: [
29778
- "Print the resolved rest's pending judgment — the prepared",
29779
- "state, its typed questions, and their criteria — the same",
29780
- "judge field `gtd next --json` already carries. Read-only:",
29781
- "resolves no session, writes nothing. Refuses (exit 1) when",
29782
- "the resolved rest declares no judge:"
29783
- ]
29784
- },
29785
- {
29786
- token: "judge answer",
29787
- kind: "judgeAnswer",
29788
- arity: "none",
29789
- details: [
29790
- "Read a verdict off stdin — one { id, answer, p } entry per",
29791
- "question the pending judgment declared — and decode it",
29792
- "against an Effect Schema built from those same question",
29793
- "ids. Refuses (exit 1) when the resolved rest declares no",
29794
- "judge:. Exits 2 (a usage error, like an unknown --json",
29795
- "selector) when stdin isn't valid JSON or the verdict",
29796
- "doesn't decode against the pending question ids."
29797
- ]
29798
- },
29799
- {
29800
- token: "judge run",
29801
- kind: "judgeRun",
29802
- arity: "none",
29803
- details: [
29804
- "Answer a judgment: read the `gtd judge --json` document off",
29805
- "stdin, write a verdict [{ id, answer, p }] on stdout — what",
29806
- "`gtd judge answer` decodes. --provider fixed answers from",
29807
- "--answers <path> or GTD_JUDGE_ANSWERS; questions the file",
29808
- "does not cover are left out. --provider jev asks TypeSafe",
29809
- "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
29810
- "on PATH (Claude Code only), model sonnet unless --model.",
29811
- "No --provider: GTD_JUDGE_PROVIDER, then ~/.config/gtd/",
29812
- "judge.json, else jev when TYPESAFE_API_KEY is set and no",
29813
- "model is given, else llm; --model likewise falls back to",
29814
- "GTD_JUDGE_MODEL, then the file's model. Needs no",
29815
- "repository. jev and llm exit 1 when they cannot answer",
29816
- "every question, with nothing on stdout"
29817
- ]
29697
+ group: "Inspect",
29698
+ summary: "Print the commit a review diff starts from",
29699
+ details: `Print the commit a review diff should start from, bare: the process's diff base before the first review round, the latest round's boundary after it. Writes nothing; refuses when no process is underway.`
29818
29700
  },
29819
29701
  {
29820
29702
  token: "strays",
@@ -29823,45 +29705,27 @@ const COMMAND_ROWS = [
29823
29705
  name: "range",
29824
29706
  variadic: "glob"
29825
29707
  },
29826
- details: [
29827
- "List the files <range> (<base>..<tip>, an empty side is HEAD)",
29828
- "touched in its committed net diff that match none of the",
29829
- "globs. Anchored at the repo root: * stays within one path",
29830
- "segment, ** crosses them, everything else is literal — quote",
29831
- "globs in the shell. stdout: a sorted JSON array, [] when",
29832
- "clean; stderr: a summary, plus one stray: line per path with",
29833
- "--verbose. --json=<n> picks one entry, nothing past the end;",
29834
- "a symlink is a path like any other, never followed; a",
29835
- "submodule counts as its path, whatever diff.ignoreSubmodules",
29836
- "or .gitmodules ignore say. Runs in any git repository, no",
29837
- "process needed. Exits 10 when",
29838
- "strays were found, 1 on an unknown revision, 2 on a bad range"
29839
- ]
29708
+ group: "Inspect",
29709
+ summary: "List changed files outside the globs",
29710
+ details: `List the files changed in <range> (<base>..<tip>, an empty side is HEAD) that match none of the globs. Globs are anchored at the repository root: * stays within one path segment, ** crosses them, everything else is literal — quote them in the shell.
29711
+
29712
+ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose adds one stray: line per path. Runs in any git repository, no process needed. Exits 10 when strays were found, 1 on an unknown revision, 2 on a bad range.`
29840
29713
  },
29841
29714
  {
29842
29715
  token: "history list",
29843
29716
  kind: "historyList",
29844
29717
  arity: "none",
29845
- details: [
29846
- "List the finished processes kept behind refs/gtd/history/*,",
29847
- "newest first: entry, branch, tip, date and subject per line.",
29848
- "--json prints one array of { ref, entry, branch, tip, date,",
29849
- "subject }. Writes nothing and never replays; with no refs it",
29850
- "hints at `gtd history fetch | sh` and still exits 0"
29851
- ]
29718
+ group: "Inspect",
29719
+ summary: "List finished processes",
29720
+ details: `List the finished processes this clone keeps, newest first: entry, branch, tip, date and subject. --json prints an array of { ref, entry, branch, tip, date, subject }. Writes nothing; with none kept it suggests \`gtd history fetch\`.`
29852
29721
  },
29853
29722
  {
29854
29723
  token: "history show",
29855
29724
  kind: "historyShow",
29856
29725
  arity: { name: "ref" },
29857
- details: [
29858
- "Replay one kept process under today's config and print its",
29859
- "steps, judge verdicts and approvals. <ref> is a full ref, an",
29860
- "entry hash (or unique prefix) or a branch name. Refuses",
29861
- "(exit 1) when a config file differs from its content at the",
29862
- "ref's tip. A version mismatch only warns on stderr. --json",
29863
- "prints the trail as one document; writes nothing"
29864
- ]
29726
+ group: "Inspect",
29727
+ summary: "Replay one finished process",
29728
+ details: `Replay one finished process under today's config and print its steps, judge verdicts and approvals. <ref> is a full ref, an entry hash or a unique prefix of one, or a branch name. Refuses when a config file changed since; a different gtd version only warns. --json prints the trail as one document. Writes nothing.`
29865
29729
  },
29866
29730
  {
29867
29731
  token: "history metrics",
@@ -29870,18 +29734,9 @@ const COMMAND_ROWS = [
29870
29734
  name: "ref",
29871
29735
  many: true
29872
29736
  },
29873
- details: [
29874
- "Report how the kept processes went: builds whose naive pass",
29875
- "landed or was rebuilt (the refactor-first rate), Gtd-Cost by",
29876
- "model and phase with the double work of a rebuild, quality-lap",
29877
- "findings per lens, and red checks that passed on their",
29878
- "re-check — per process and in total. Each <ref>",
29879
- "is read as by history show, but a branch name means every",
29880
- "process kept from it; none means every ref.",
29881
- "Reads commit subjects and trailers, never replays, so a",
29882
- "changed config is no refusal. --json prints { processes, total",
29883
- "} as one document; writes nothing"
29884
- ]
29737
+ group: "Inspect",
29738
+ summary: "Report cost and rework of finished processes",
29739
+ details: `Report how finished processes went: builds that landed first time or were rebuilt, cost by model and phase, review findings per lens, and red checks that passed when re-run — per process and in total. <ref> is read as by \`gtd history show\`, but a branch name means every process kept from it; no <ref> means all of them. Never replays, so a changed config is no reason to refuse. --json prints { processes, total }. Writes nothing.`
29885
29740
  },
29886
29741
  {
29887
29742
  token: "history judges",
@@ -29890,18 +29745,9 @@ const COMMAND_ROWS = [
29890
29745
  name: "ref",
29891
29746
  many: true
29892
29747
  },
29893
- details: [
29894
- "Calibrate the judges of the kept processes from their",
29895
- "recorded Gtd-Judge verdicts: per judge (retry, triage, clear)",
29896
- "and p bucket, how many verdicts later events overturned.",
29897
- "--var <name>=<value> re-decides every past verdict under",
29898
- "another threshold and prints what would have changed;",
29899
- "--jsonl prints one { process, judge, commit, step, question,",
29900
- "state, answer, p, outcome } line per verdict instead. <ref>",
29901
- "is read as by history metrics. Never replays and asks no",
29902
- "model. --json prints { processes, total, whatIf }; writes",
29903
- "nothing"
29904
- ]
29748
+ group: "Inspect",
29749
+ summary: "Calibrate the judges of finished processes",
29750
+ details: `Calibrate the judges of finished processes from their recorded verdicts: per judge and confidence bucket, how many verdicts later events overturned. <ref> is read as by \`gtd history metrics\`. Never replays and asks no model. --json prints { processes, total, whatIf }. Writes nothing.`
29905
29751
  },
29906
29752
  {
29907
29753
  token: "history fetch",
@@ -29910,69 +29756,201 @@ const COMMAND_ROWS = [
29910
29756
  name: "remote",
29911
29757
  optional: true
29912
29758
  },
29913
- details: [
29914
- "Print a script that fetches every refs/gtd/history/* from",
29915
- "<remote> (default origin) and reports how many arrived — run",
29916
- "it with `gtd history fetch | sh`. Never runs git itself"
29917
- ]
29759
+ group: "Inspect",
29760
+ summary: "Print a script that fetches finished processes",
29761
+ details: `Print a script that fetches every finished process kept on <remote> (default: origin) and reports how many arrived. gtd runs no git itself:
29762
+
29763
+ s=$(gtd history fetch --json=script) && sh -c "$s"`
29764
+ },
29765
+ {
29766
+ token: "ui",
29767
+ kind: "ui",
29768
+ arity: "none",
29769
+ group: "Inspect",
29770
+ summary: "Serve the web and phone client",
29771
+ details: `Serve gtd's web and phone client for this worktree. By default it publishes through \`tailscale serve\`, reachable from anywhere on the tailnet, on the first of ports 8443, 10000 and 443 that works; when none does it serves HTTPS locally instead of refusing.`
29772
+ },
29773
+ {
29774
+ token: "guide",
29775
+ kind: "guide",
29776
+ arity: "none",
29777
+ group: "Driver plumbing",
29778
+ summary: "Print a briefing for writing a driver",
29779
+ details: `Print a self-contained briefing that teaches an agent or a person to build a gtd driver in any shell or runtime, and where to get a workflow — gtd ships none. Writes nothing.`
29780
+ },
29781
+ {
29782
+ token: "judge",
29783
+ kind: "judge",
29784
+ arity: "none",
29785
+ group: "Driver plumbing",
29786
+ summary: "Print the pending judgment",
29787
+ details: `Print the current step's pending judgment: the prepared state, its questions and their criteria — the judge field \`gtd next --json\` carries. Writes nothing; refuses when the step declares no judge.`
29788
+ },
29789
+ {
29790
+ token: "judge answer",
29791
+ kind: "judgeAnswer",
29792
+ arity: "none",
29793
+ group: "Driver plumbing",
29794
+ summary: "Land a verdict read from stdin",
29795
+ details: `Read a verdict from stdin — one { id, answer, p } entry per pending question — and land it the way \`gtd land\` does: plain output names the commit, --json=script prints the script. Refuses when the step declares no judge; a verdict that is not JSON, or names a question that was not asked, is a usage error (exit 2).`
29796
+ },
29797
+ {
29798
+ token: "judge run",
29799
+ kind: "judgeRun",
29800
+ arity: "none",
29801
+ group: "Driver plumbing",
29802
+ summary: "Answer a judgment with a model",
29803
+ details: `Answer a judgment: read the \`gtd judge --json\` document from stdin and print the verdict [{ id, answer, p }] \`gtd judge answer\` reads. Needs no repository.
29804
+
29805
+ The fixed answerer reads --answers and leaves out the questions it does not cover; jev asks TypeSafe ($TYPESAFE_API_KEY); llm asks the \`claude\` CLI on PATH. jev and llm exit 1, printing nothing, when they cannot answer every question. A stdin that is not a judgment document is a usage error (exit 2).`
29806
+ },
29807
+ {
29808
+ token: "check",
29809
+ kind: "check",
29810
+ arity: { names: ["mode", "file"] },
29811
+ group: "Driver plumbing",
29812
+ summary: "Check a steering file's format",
29813
+ details: `Run the steering format <mode> (qa, review or findings) over <file> and print each finding on a line of its own; exits 1 when there are any, or when <file> does not exist. An unknown <mode> is a usage error (exit 2). Reads no workflow and runs from any directory — emitted validation scripts call it.`
29814
+ },
29815
+ {
29816
+ token: "uncheck",
29817
+ kind: "uncheck",
29818
+ arity: { name: "file" },
29819
+ group: "Driver plumbing",
29820
+ summary: "Untick a review file's boxes",
29821
+ details: `Reset every ticked review box (- [x]) in <file> to - [ ], writing the file only when it changes; a missing file writes nothing. Reads no workflow and runs from any directory — the landing script at a review gate calls it, so no tick is ever committed.`
29822
+ },
29823
+ {
29824
+ token: "lsp",
29825
+ kind: "lsp",
29826
+ arity: "none",
29827
+ group: "Driver plumbing",
29828
+ summary: "Start the steering-file language server",
29829
+ details: `Start the language server for .gtd/ steering files on stdio.`
29918
29830
  }
29919
29831
  ];
29920
29832
  const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
29921
- const COLUMN = 19;
29922
- const renderBlock = (header, lines) => {
29923
- const [first, ...rest] = lines;
29924
- const headerCell = ` ${header}`;
29925
- return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
29926
- };
29927
- const START_SHORT_FORM = renderBlock("(no command) --workflow <name>", ["Starts a new process authenticated as human, e.g.", "'gtd --workflow <name>'"]);
29928
- const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
29929
- const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
29930
- const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--workflow", "--var"]);
29833
+ /** Every word that may follow `gtd`, for "did you mean". */
29834
+ const COMMAND_WORDS = [.../* @__PURE__ */ new Set([
29835
+ ...COMMAND_ROWS.map((r) => r.token.split(" ")[0]),
29836
+ "help",
29837
+ "version"
29838
+ ])];
29839
+ const TOP_COLUMN = 28;
29840
+ const OPTION_COLUMN = 22;
29841
+ const synopsis = (row) => {
29842
+ const { token, arity } = row;
29843
+ if (arity === "none") return token;
29844
+ if ("names" in arity) return `${token} <${arity.names[0]}> <${arity.names[1]}>`;
29845
+ if ("bare" in arity) return `${token} [${arity.name}] [${arity.variadic}...]`;
29846
+ if ("variadic" in arity) return `${token} <${arity.name}> [${arity.variadic}...]`;
29847
+ if ("optional" in arity) return `${token} [${arity.name}]`;
29848
+ if ("many" in arity) return `${token} [${arity.name}...]`;
29849
+ return `${token} <${arity.name}>`;
29850
+ };
29851
+ const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--var", "--threshold"]);
29931
29852
  const flagHeader = (row) => {
29932
29853
  if (row.valueHint === "") return row.name;
29854
+ if (row.arity === "optional") return `${row.name}[=${row.valueHint}]`;
29933
29855
  return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
29934
29856
  };
29857
+ const GLOBAL_OPTIONS = [
29858
+ ["-v, --verbose", "Narrate on stderr what gtd resolved"],
29859
+ ["-h, --help", "Print help; gtd <command> --help for one command"],
29860
+ ["-V, --version", "Print the version"],
29861
+ ["--", "End the options: every word after it is an argument"]
29862
+ ];
29863
+ const EXTRA_TOPICS = [[
29864
+ "help [command]",
29865
+ "Print help for gtd or one command",
29866
+ "Print gtd's overview, or with <command> that command's own help."
29867
+ ], [
29868
+ "version",
29869
+ "Print the version",
29870
+ "Print gtd's version."
29871
+ ]];
29872
+ const EXAMPLES = ` $ gtd start list what can start
29873
+ $ gtd start <name> start a process
29874
+ $ gtd next see what happens next
29875
+ $ s=$(gtd land --json=script) && sh -c "$s" land the turn
29876
+ $ gtd help land one command's details`;
29877
+ const lines = (block) => block.map((l) => `${l}\n`).join("");
29878
+ const GROUPS = [
29879
+ "Core loop",
29880
+ "Start and stop",
29881
+ "Inspect",
29882
+ "Driver plumbing"
29883
+ ];
29935
29884
  const renderHelp = () => {
29936
- const commandBlocks = COMMAND_ROWS.map((row) => {
29937
- const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : "variadic" in row.arity ? `${row.token} <${row.arity.name}> [${row.arity.variadic}...]` : "optional" in row.arity ? `${row.token} [${row.arity.name}]` : "many" in row.arity ? `${row.token} [${row.arity.name}...]` : `${row.token} <${row.arity.name}>`;
29938
- return renderBlock(header, row.details);
29885
+ const groups = GROUPS.map((group) => {
29886
+ const rows = COMMAND_ROWS.filter((r) => r.group === group).flatMap((r) => definition(synopsis(r), r.summary, TOP_COLUMN));
29887
+ const extra = group === "Driver plumbing" ? EXTRA_TOPICS.flatMap(([term, summary]) => definition(term, summary, TOP_COLUMN)) : [];
29888
+ return `${group}:\n${lines([...rows, ...extra])}`;
29939
29889
  });
29940
- return `Usage: gtd [command] [options]\n\nCommands:\n${[
29941
- commandBlocks[0],
29942
- START_SHORT_FORM,
29943
- ...commandBlocks.slice(1),
29944
- VERSION_BLOCK,
29945
- HELP_BLOCK
29946
- ].join("")}\nOptions:\n${FLAGS.map((row) => renderBlock(flagHeader(row), row.help)).join("")}${renderBlock("--version, -V", ["Print version and exit"]) + renderBlock("--help, -h", ["Print this help and exit"])}`;
29890
+ return "Usage: gtd <command> [options]\n\n" + lines(wrap("gtd reads a repository's git state and prints what should happen next in its workflow. It never acts on it: a driver, or you, runs what it prints.")) + "\n" + groups.join("\n") + "\nOptions:\n" + lines(GLOBAL_OPTIONS.flatMap(([term, text]) => definition(term, text, TOP_COLUMN))) + `\nExamples:\n${EXAMPLES}\n\n` + lines(wrap("Run `gtd help <command>` for a command's details and options, and `gtd guide` to learn how to drive gtd."));
29891
+ };
29892
+ /** Shown on stderr above a bare `gtd`'s usage error — the overview stays one `gtd help` away. */
29893
+ const SHORT_USAGE = `Usage: gtd <command> [options]
29894
+
29895
+ gtd start <name> start a process
29896
+ gtd next print what should happen next
29897
+ gtd land record what the tree now shows
29898
+
29899
+ `;
29900
+ const renderCommandHelp = (row) => {
29901
+ const options = FLAGS.filter((f) => f.name !== "--verbose" && f.scope(row.kind)).flatMap((f) => definition(flagHeader(f), f.helpFor?.[row.kind] ?? f.help, OPTION_COLUMN));
29902
+ const global = definition("-v, --verbose", "Narrate on stderr what gtd resolved", OPTION_COLUMN);
29903
+ return `Usage: gtd ${synopsis(row)}${options.length > 0 ? " [options]" : ""}\n\n` + lines(wrap(row.details)) + `\nOptions:\n${lines([...options, ...global])}`;
29904
+ };
29905
+ /** `gtd help <topic…>`'s text: one command's help, every `history` subcommand's for `history`, or undefined for an unknown topic. */
29906
+ const renderTopicHelp = (topic) => {
29907
+ const two = commandByToken(topic.slice(0, 2).join(" "));
29908
+ if (two !== void 0 && topic.length >= 2) return renderCommandHelp(two);
29909
+ const [first = ""] = topic;
29910
+ const one = commandByToken(first);
29911
+ if (one !== void 0) return renderCommandHelp(one);
29912
+ const extra = EXTRA_TOPICS.find(([term]) => term.split(" ")[0] === first);
29913
+ if (extra !== void 0) return `Usage: gtd ${extra[0]}\n\n${lines(wrap(extra[2]))}`;
29914
+ const family = COMMAND_ROWS.filter((r) => r.token.startsWith(`${first} `));
29915
+ if (first !== "" && family.length > 0) return family.map(renderCommandHelp).join("\n");
29916
+ };
29917
+ /** A usage error's tail: the nearest candidate when `word` looks like a typo of one, and where the full list is. */
29918
+ const didYouMean = (word, candidates, prefix = "", pointer = "`gtd help` for every command") => {
29919
+ const guess = closest(word, candidates);
29920
+ return guess === void 0 ? ` — run ${pointer}` : ` — did you mean \`${prefix}${guess}\`? Run ${pointer}`;
29921
+ };
29922
+ const unknownOption = (tok, jsonSeen) => {
29923
+ const candidates = tok.startsWith("--") ? FLAGS.map((f) => f.name) : [];
29924
+ return {
29925
+ kind: "error",
29926
+ message: `gtd: unknown option '${tok}'${didYouMean(tok.split("=")[0], candidates, "", "`gtd help <command>` for its options")}`,
29927
+ jsonSeen
29928
+ };
29947
29929
  };
29948
29930
  const tokenize = (tail) => {
29949
29931
  const positionals = [];
29950
29932
  const byFlag = /* @__PURE__ */ new Map();
29951
29933
  const present = /* @__PURE__ */ new Set();
29952
29934
  let jsonSeen = false;
29935
+ let afterBareJson;
29936
+ const end = tail.indexOf("--");
29953
29937
  for (let i = 0; i < tail.length; i++) {
29954
29938
  const original = tail[i];
29939
+ if (i === end) continue;
29955
29940
  const tok = SINGLE_DASH_ALIASES.get(original) ?? original;
29956
- if (!tok.startsWith("--")) {
29941
+ if (end !== -1 && i > end || !tok.startsWith("-") || tok === "-") {
29942
+ if (tail[i - 1] === "--json") afterBareJson ??= original;
29957
29943
  positionals.push(original);
29958
29944
  continue;
29959
29945
  }
29960
- const row = flagByToken(tok);
29961
- if (row === void 0) return {
29962
- kind: "error",
29963
- message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
29964
- jsonSeen
29965
- };
29946
+ const row = tok.startsWith("--") ? flagByToken(tok) : void 0;
29947
+ if (row === void 0) return unknownOption(tok, jsonSeen);
29966
29948
  const eq = tok.indexOf("=");
29967
29949
  const alreadyPresent = present.has(row.name);
29968
29950
  present.add(row.name);
29969
29951
  if (row.name === "--json") jsonSeen = true;
29970
29952
  if (row.arity === 0) {
29971
- if (eq !== -1) return {
29972
- kind: "error",
29973
- message: `gtd: unknown option '${tok}' — see \`gtd --help\``,
29974
- jsonSeen
29975
- };
29953
+ if (eq !== -1) return unknownOption(tok, jsonSeen);
29976
29954
  if (!byFlag.has(row.name)) byFlag.set(row.name, []);
29977
29955
  continue;
29978
29956
  }
@@ -30023,7 +30001,8 @@ const tokenize = (tail) => {
30023
30001
  positionals,
30024
30002
  byFlag,
30025
30003
  present,
30026
- jsonSeen
30004
+ jsonSeen,
30005
+ ...afterBareJson !== void 0 ? { afterBareJson } : {}
30027
30006
  };
30028
30007
  };
30029
30008
  const usagePlan = (message, json, stdout = "") => ({
@@ -30046,7 +30025,7 @@ const tooMany = (cmd, rest, expected, max) => rest.length > max ? `gtd ${cmd}: t
30046
30025
  const arityError = (cmd, rest, arity) => {
30047
30026
  if (arity === "none") return tooMany(cmd, rest, "none", 0);
30048
30027
  if ("names" in arity) return pairArityError(cmd, rest, arity.names);
30049
- if ("many" in arity) return void 0;
30028
+ if ("many" in arity || "bare" in arity) return void 0;
30050
30029
  if ("optional" in arity) return tooMany(cmd, rest, `at most one ${arity.name}`, 1);
30051
30030
  if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
30052
30031
  return "variadic" in arity ? void 0 : tooMany(cmd, rest, `one ${arity.name}`, 1);
@@ -30069,54 +30048,85 @@ const decodeFlags = (byFlag) => {
30069
30048
  }
30070
30049
  return right(bag);
30071
30050
  };
30051
+ /** `gtd history judges`' thresholds: each a known judge threshold with a numeric value. */
30052
+ const judgeThresholds = (thresholds) => {
30053
+ const unknown = Object.keys(thresholds).find((name) => !Object.hasOwn(THRESHOLDS, name));
30054
+ if (unknown !== void 0) return left(`gtd history judges: --threshold ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`);
30055
+ const nan = Object.entries(thresholds).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
30056
+ if (nan !== void 0) return left(`gtd history judges: --threshold ${nan[0]} must be a number — got "${nan[1]}"`);
30057
+ return right(thresholds);
30058
+ };
30072
30059
  /** Assembles a `land` `Command` from its already-scope-checked flag bag — split out of `parseArgv` so its two independent, omit-when-absent optional fields don't inflate that function's own branching. */
30073
30060
  const buildLandCommand = (bag) => ({
30074
30061
  kind: "land",
30075
30062
  ...bag["--cost"] !== void 0 ? { cost: bag["--cost"] } : {},
30076
30063
  ...bag["--model"] !== void 0 ? { model: bag["--model"] } : {}
30077
30064
  });
30078
- const parseArgv = (argv) => {
30079
- if (argv.includes("--help") || argv.includes("-h")) return {
30065
+ const HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h"]);
30066
+ /** The options part of argv: everything before a `--`, which ends the options. */
30067
+ const optionsOf = (argv) => {
30068
+ const end = argv.indexOf("--", 2);
30069
+ return end === -1 ? argv : argv.slice(0, end);
30070
+ };
30071
+ /** `--help`/`-h` anywhere before `--`: the named command's own help, else the overview — never a usage error, so a typo'd command still gets help. */
30072
+ const helpPlan = (argv) => {
30073
+ const tokenized = tokenize(optionsOf(argv).slice(2).filter((tok) => !HELP_FLAGS.has(tok)));
30074
+ const topic = tokenized.kind === "ok" ? tokenized.positionals : [];
30075
+ return {
30076
+ kind: "output",
30077
+ stdout: renderTopicHelp(topic) ?? renderHelp()
30078
+ };
30079
+ };
30080
+ /** `gtd help [topic…]`: an unknown topic is a usage error naming the nearest command. */
30081
+ const helpCommandPlan = (topic, json) => {
30082
+ if (topic.length === 0) return {
30080
30083
  kind: "output",
30081
30084
  stdout: renderHelp()
30082
30085
  };
30083
- if (argv.includes("--version") || argv.includes("-V")) return {
30086
+ const text = renderTopicHelp(topic);
30087
+ if (text !== void 0) return {
30088
+ kind: "output",
30089
+ stdout: text
30090
+ };
30091
+ return usagePlan(`gtd: help: no command '${topic.join(" ")}'${didYouMean(topic[0], COMMAND_WORDS, "gtd help ")}`, json);
30092
+ };
30093
+ const parseArgv = (argv) => {
30094
+ const options = optionsOf(argv);
30095
+ if (options.some((tok) => HELP_FLAGS.has(tok))) return helpPlan(argv);
30096
+ if (options.includes("--version") || options.includes("-V")) return {
30084
30097
  kind: "output",
30085
30098
  stdout: `${GTD_VERSION}\n`
30086
30099
  };
30087
30100
  const tokenized = tokenize(argv.slice(2));
30088
30101
  if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
30089
- const { positionals, byFlag, present, jsonSeen } = tokenized;
30102
+ const { positionals, byFlag, present, jsonSeen, afterBareJson } = tokenized;
30090
30103
  const first = positionals[0];
30091
- const workflowRaw = byFlag.get("--workflow")?.[0];
30092
- const workflowPresent = workflowRaw !== void 0;
30093
30104
  if (first === "version") return {
30094
30105
  kind: "output",
30095
30106
  stdout: `${GTD_VERSION}\n`
30096
30107
  };
30097
- if (first === "help") return {
30098
- kind: "output",
30099
- stdout: renderHelp()
30100
- };
30108
+ if (first === "help") return helpCommandPlan(positionals.slice(1), jsonSeen);
30101
30109
  const isHistory = first === "history";
30102
30110
  const row = first === void 0 ? void 0 : commandByToken(isHistory ? `history ${positionals[1] ?? ""}` : first);
30103
- if (isHistory && row === void 0) return usagePlan("gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]", jsonSeen);
30111
+ if (isHistory && row === void 0) {
30112
+ const subcommands = COMMAND_ROWS.filter((r) => r.token.startsWith("history ")).map((r) => r.token.split(" ")[1]);
30113
+ const typo = closest(positionals[1] ?? "", subcommands);
30114
+ return usagePlan(typo !== void 0 ? `gtd history: unknown subcommand '${positionals[1]}' — did you mean \`gtd history ${typo}\`?` : "gtd history: expected a subcommand — list, show <ref>, metrics [ref...], judges [ref...] or fetch [remote]", jsonSeen);
30115
+ }
30104
30116
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
30105
- const selectsStart = workflowPresent && first === void 0;
30106
30117
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
30107
- const kind = selectsStart ? "start" : judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row?.kind;
30108
- if (row === void 0 && !selectsStart) {
30118
+ if (row === void 0 || first === void 0) {
30109
30119
  const violation = scopeViolation(void 0, present);
30110
30120
  if (violation !== void 0) return usagePlan(violation, jsonSeen);
30111
- if (first === void 0) return usagePlan("gtd: missing command — gtd decides and prints, a driver executes; run `gtd install`, or copy one from https://github.com/pmelab/gtd/blob/main/docs/driver.md's \"A complete minimal driver\" section and run that, or see usage above (`gtd --help`)", jsonSeen, renderHelp());
30112
- return usagePlan(`unknown command '${first}'`, jsonSeen);
30121
+ if (first === void 0) return usagePlan("gtd: missing command — run `gtd help` for every command, or `gtd guide` to learn how to drive gtd", jsonSeen, SHORT_USAGE);
30122
+ return usagePlan(`gtd: unknown command '${first}'${didYouMean(first, COMMAND_WORDS, "gtd ")}`, jsonSeen);
30113
30123
  }
30114
- const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
30115
- if (kind === "start" && first === void 0) {
30116
- if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
30117
- } else {
30118
- const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
30119
- if (arityMsg !== void 0) return usagePlan(arityMsg, jsonSeen);
30124
+ const kind = judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row.kind;
30125
+ const restPositionals = positionals.slice(judgeAnswer || judgeRun || isHistory ? 2 : 1);
30126
+ const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(isHistory ? row.token : first, restPositionals, row.arity);
30127
+ if (arityMsg !== void 0) {
30128
+ const hint = afterBareJson !== void 0 && restPositionals.includes(afterBareJson) ? ` — to print one field, write --json=${afterBareJson}` : "";
30129
+ return usagePlan(arityMsg + hint, jsonSeen);
30120
30130
  }
30121
30131
  const violation = scopeViolation(kind, present);
30122
30132
  if (violation !== void 0) return usagePlan(violation, jsonSeen);
@@ -30140,30 +30150,20 @@ const parseArgv = (argv) => {
30140
30150
  };
30141
30151
  }
30142
30152
  if (kind === "start") {
30143
- const 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);
30144
30155
  return {
30145
30156
  kind: "command",
30146
30157
  command: {
30147
30158
  kind: "start",
30148
- actor: "human",
30149
- workflow: workflowRaw,
30150
- vars: bag["--var"] ?? {},
30151
- label
30159
+ ...name !== void 0 ? { name } : {},
30160
+ args,
30161
+ vars: bag["--var"] ?? {}
30152
30162
  },
30153
30163
  json,
30154
30164
  verbose
30155
30165
  };
30156
30166
  }
30157
- if (kind === "door") return {
30158
- kind: "command",
30159
- command: {
30160
- kind: "door",
30161
- name: restPositionals[0],
30162
- args: restPositionals.slice(1)
30163
- },
30164
- json,
30165
- verbose
30166
- };
30167
30167
  if (kind === "ui") return {
30168
30168
  kind: "command",
30169
30169
  command: {
@@ -30229,17 +30229,14 @@ const parseArgv = (argv) => {
30229
30229
  verbose
30230
30230
  };
30231
30231
  if (kind === "historyJudges") {
30232
- const vars = bag["--var"] ?? {};
30233
- const unknown = Object.keys(vars).find((name) => !Object.hasOwn(THRESHOLDS, name));
30234
- if (unknown !== void 0) return usagePlan(`gtd history judges: --var ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`, jsonSeen);
30235
- const nan = Object.entries(vars).find(([, value]) => value.trim() === "" || !Number.isFinite(Number(value)));
30236
- if (nan !== void 0) return usagePlan(`gtd history judges: --var ${nan[0]} must be a number — got "${nan[1]}"`, jsonSeen);
30232
+ const thresholds = judgeThresholds(bag["--threshold"] ?? {});
30233
+ if (isLeft(thresholds)) return usagePlan(thresholds.left, jsonSeen);
30237
30234
  return {
30238
30235
  kind: "command",
30239
30236
  command: {
30240
30237
  kind: "historyJudges",
30241
30238
  refs: restPositionals,
30242
- vars,
30239
+ vars: thresholds.right,
30243
30240
  jsonl: bag["--jsonl"] ?? false
30244
30241
  },
30245
30242
  json,
@@ -31128,7 +31125,7 @@ const recognizeWrite = (repo, script) => {
31128
31125
  const [, content, to] = words$1(lines[3]);
31129
31126
  const remove = words$1(lines.find((l) => l.startsWith("rm -rf -- ")) ?? "");
31130
31127
  if (path === void 0 || content === void 0 || to !== path) return void 0;
31131
- if (writeScript(path, content, remove) !== script) return void 0;
31128
+ if (writeScript$1(path, content, remove) !== script) return void 0;
31132
31129
  repo.writeFile(path, content);
31133
31130
  removeAll(repo, remove);
31134
31131
  return ok();
@@ -31765,7 +31762,10 @@ const recognizeGtdCheck = (repo, block) => {
31765
31762
  error: `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`
31766
31763
  };
31767
31764
  const content = repo.readFile(file);
31768
- if (content === void 0) return { kind: "noop" };
31765
+ if (content === void 0) return {
31766
+ kind: "failed",
31767
+ error: `gtd check: no such file: ${file}`
31768
+ };
31769
31769
  const findings = format.validate(content).map((finding) => formatFinding(file, finding));
31770
31770
  if (findings.length === 0) return { kind: "noop" };
31771
31771
  return {
@@ -31788,12 +31788,10 @@ const recognizeGtdUncheck = (repo, block) => {
31788
31788
  return { kind: "noop" };
31789
31789
  };
31790
31790
  /**
31791
- * `Emit.ts`'s `combinedScript` leading comment — `gtd land`/`gtd --workflow`'s
31792
- * whole plain-text artifact opens with this line ahead of the required
31793
- * script, so recognizing it as a no-op (like the retry-helper function
31794
- * definition) keeps the `@inmem` tier green.
31791
+ * A block of nothing but comments is a no-op in `sh` too — `combinedScript`'s
31792
+ * leading `DID_NOT_RUN_COMMENT`, and the list of paths a start commits.
31795
31793
  */
31796
- const recognizeDidNotRunComment = (block) => block === "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn" ? { kind: "noop" } : void 0;
31794
+ const recognizeCommentBlock = (block) => block.split("\n").every((line) => line.startsWith("#")) ? { kind: "noop" } : void 0;
31797
31795
  /** `combinedScript`'s optional half. Reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`. Exception: under `git: false` any unrecognized inner block fails the outer script (git or not), so the in-memory twin knowingly diverges from `sh` rather than run git unseen. */
31798
31796
  const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0].replace(/\\n$/, "")}\n`;
31799
31797
  const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`;
@@ -31907,7 +31905,7 @@ const recognizeWriteLines = (repo, block) => {
31907
31905
  /** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
31908
31906
  const recognizersFor = (repo, commands, git) => [
31909
31907
  (block) => recognizeSetFlags(block),
31910
- (block) => recognizeDidNotRunComment(block),
31908
+ (block) => recognizeCommentBlock(block),
31911
31909
  (block) => recognizePrecondition(repo, block),
31912
31910
  (block) => recognizeFileExistsGuard(repo, block),
31913
31911
  (block) => recognizeBinaryGuard(commands, block),
@@ -31985,6 +31983,52 @@ const unrecognized = (block, output = "") => ({
31985
31983
  });
31986
31984
  Narrator.layer(() => {}, false);
31987
31985
  //#endregion
31986
+ //#region src/testkit/imports.ts
31987
+ const IDENTIFIER$1 = /^[A-Za-z_$][\w$]*$/;
31988
+ const STATEMENT = /^[ \t]*(import|export)([ \t]+type)?[ \t]+([\w$* \t,]*(?:\{[^}]*\})?[\w$* \t,]*?)\s*\bfrom\s*(["'])([^"'\n]+)\4[ \t]*;?|^[ \t]*import\s*(["'])([^"'\n]+)\6[ \t]*;?/gm;
31989
+ const bindings = (list) => list.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "" && !entry.startsWith("type ")).map((entry) => {
31990
+ const [name, as] = entry.split(/\s+as\s+/);
31991
+ return {
31992
+ name,
31993
+ as: as ?? name
31994
+ };
31995
+ });
31996
+ const exportLine = (as, value) => as === "default" ? `export default ${value}` : `export const ${as} = ${value}`;
31997
+ const exportLines = (module, m, read, head, braces) => {
31998
+ if (braces !== void 0) return bindings(braces).map((b) => exportLine(b.as, read(b.name)));
31999
+ if (/^\*\s+as\s+/.test(head)) return [exportLine(head.replace(/^\*\s+as\s+/, ""), m)];
32000
+ return Object.keys(module).filter((name) => name !== "default" && IDENTIFIER$1.test(name)).map((name) => exportLine(name, read(name)));
32001
+ };
32002
+ const importLines = (m, read, head, braces) => [...head.split(",").map((part) => part.trim()).filter((part) => part !== "").map((part) => part.startsWith("*") ? `const ${part.replace(/^\*\s+as\s+/, "")} = ${m}` : `const ${part} = ${read("default")}`), ...braces === void 0 ? [] : bindings(braces).map((b) => `const ${b.as} = ${read(b.name)}`)];
32003
+ /**
32004
+ * `source` with each import or re-export of a specifier in `modules` reading
32005
+ * that module off `lookup(specifier)`, an expression evaluated when the
32006
+ * config runs. A string config is evaluated in memory, where no resolver
32007
+ * knows the specifier, so the binding is made by hand.
32008
+ */
32009
+ const importsFrom = (source, modules, lookup) => {
32010
+ let n = 0;
32011
+ const rewrite = (kind, clause, specifier, module) => {
32012
+ const m = `__testkit_module_${++n}`;
32013
+ const read = (name) => {
32014
+ if (!(name in module)) throw new Error(`testkit: module "${specifier}" has no export "${name}"`);
32015
+ return `${m}[${JSON.stringify(name)}]`;
32016
+ };
32017
+ const text = clause.trim();
32018
+ const braces = /\{([^}]*)\}/.exec(text)?.[1];
32019
+ const head = text.replace(/\{[^}]*\}/, "").replace(/,\s*$/, "").trim();
32020
+ const lines = kind === "export" ? exportLines(module, m, read, head, braces) : importLines(m, read, head, braces);
32021
+ return [`const ${m} = ${lookup(specifier)}`, ...lines].join("\n");
32022
+ };
32023
+ return source.replace(STATEMENT, (statement, kind, typeOnly, clause, _q, from, _bq, bare) => {
32024
+ const specifier = from ?? bare;
32025
+ const module = modules[specifier];
32026
+ if (module === void 0) return statement;
32027
+ if (typeOnly !== void 0 || bare !== void 0) return "";
32028
+ return rewrite(kind, clause, specifier, module);
32029
+ });
32030
+ };
32031
+ //#endregion
31988
32032
  //#region src/testkit/TestRepo.ts
31989
32033
  const REGISTRY = Symbol.for("@pmelab/gtd/testkit:modules");
31990
32034
  const registry = () => {
@@ -31993,9 +32037,10 @@ const registry = () => {
31993
32037
  };
31994
32038
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
31995
32039
  let registered = 0;
32040
+ const lookup = (key) => `globalThis[Symbol.for(${JSON.stringify(REGISTRY.description)})].get(${JSON.stringify(key)})`;
31996
32041
  /** A config whose exports are module `id`'s, read off the registry when the engine loads it. */
31997
32042
  const moduleSource = (id, module) => {
31998
- const lines = [`const m = globalThis[Symbol.for(${JSON.stringify(REGISTRY.description)})].get(${JSON.stringify(id)})`];
32043
+ const lines = [`const m = ${lookup(id)}`];
31999
32044
  for (const name of Object.keys(module)) if (name === "default") lines.push(`export default m.default`);
32000
32045
  else if (IDENTIFIER.test(name)) lines.push(`export const ${name} = m[${JSON.stringify(name)}]`);
32001
32046
  else throw new Error(`testkit: export name "${name}" is not an identifier`);
@@ -32003,6 +32048,48 @@ const moduleSource = (id, module) => {
32003
32048
  };
32004
32049
  /** An actor that runs the step's own script, for steps whose script `runScript` understands. */
32005
32050
  const runsItsScript = (beat, repo) => repo.runScript(beat.content);
32051
+ /**
32052
+ * A judge's verdict, keyed by question id: `verdict({ ready: true })`, or with
32053
+ * a confidence, `verdict({ ready: { answer: true, p: 0.6 } })`. `p` defaults to 0.9.
32054
+ */
32055
+ const verdict = (answers) => ({ answers: Object.entries(answers).map(([id, entry]) => typeof entry === "object" ? {
32056
+ id,
32057
+ answer: entry.answer,
32058
+ p: entry.p ?? .9
32059
+ } : {
32060
+ id,
32061
+ answer: entry,
32062
+ p: .9
32063
+ }) });
32064
+ /** `actor`, at a visit whose beat content contains `text`; any other visit changes nothing. */
32065
+ const whenPrompted = (text, actor) => (beat, repo) => beat.content.includes(text) ? actor(beat, repo) : void 0;
32066
+ /** Each actor in order, at every visit; the first verdict any of them returns is the result. */
32067
+ const inTurn = (actors) => async (beat, repo) => {
32068
+ let result;
32069
+ for (const actor of actors) {
32070
+ const returned = await actor(beat, repo);
32071
+ if (result === void 0 && returned !== void 0) result = returned;
32072
+ }
32073
+ return result;
32074
+ };
32075
+ /** At a judge step, `answer` when the pending questions (as `gtd judge` prints them) contain `text`. */
32076
+ const verdictWhen = (text, answer) => async (_, repo) => {
32077
+ const { stdout } = await repo.gtd("judge");
32078
+ return stdout.includes(text) ? answer : void 0;
32079
+ };
32080
+ const isVerdict = (value) => typeof value === "object" && value !== null && Array.isArray(value.answers);
32081
+ const TRAILER = /^([A-Za-z0-9][A-Za-z0-9-]*): ?(.*)$/;
32082
+ const trailersOf = (message) => {
32083
+ const paragraphs = message.trimEnd().split(/\n\s*\n/);
32084
+ const last = paragraphs.length > 1 ? paragraphs.at(-1).split("\n") : [];
32085
+ const trailers = {};
32086
+ if (last.length === 0 || !last.every((line) => TRAILER.test(line))) return trailers;
32087
+ for (const line of last) {
32088
+ const [, key, value] = TRAILER.exec(line);
32089
+ (trailers[key] ??= []).push(value);
32090
+ }
32091
+ return trailers;
32092
+ };
32006
32093
  const words = (text) => [...text.matchAll(/'((?:[^']|'\\'')*)'/g)].map((m) => m[1].replaceAll("'\\''", "'"));
32007
32094
  const fileOf = (repo, path, what) => {
32008
32095
  const content = repo.readFile(path);
@@ -32047,17 +32134,61 @@ const runLines = (repo, commands, script) => {
32047
32134
  if (!applied.ok) throw cannotRun(applied.error ?? "");
32048
32135
  };
32049
32136
  const failed = (what, result) => /* @__PURE__ */ new Error(`${what} exited ${result.exitCode}\n${result.stderr}${result.stdout}`);
32137
+ const ANSWER = {
32138
+ noul: "true",
32139
+ choice: "\"<option>\"",
32140
+ score: "0.5"
32141
+ };
32142
+ /** The verdict a judge beat's questions take, as a template to copy. */
32143
+ const expectedVerdict = (doc) => {
32144
+ let questions = [];
32145
+ try {
32146
+ questions = JSON.parse(doc.judge ?? "").questions;
32147
+ } catch {}
32148
+ return ["expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:", `[${questions.map(({ id, primitive }) => `{ "id": ${JSON.stringify(id)}, "answer": ${ANSWER[primitive] ?? "\"<answer>\""}, "p": 0.9 }`).join(", ")}]`].join("\n");
32149
+ };
32150
+ const quoted = (names) => names.map((name) => `"${name}"`).join(", ");
32151
+ /** Who acts at `doc`: its scripted actor, or a check's own script under `checks: "run"`. A stall has none. */
32152
+ const actorAt = (doc, actors, checks) => {
32153
+ if (doc.kind === "stalled") return {
32154
+ scripted: false,
32155
+ actor: void 0
32156
+ };
32157
+ if (Object.hasOwn(actors, doc.state)) return {
32158
+ scripted: true,
32159
+ actor: actors[doc.state]
32160
+ };
32161
+ return {
32162
+ scripted: false,
32163
+ actor: checks === "run" && doc.actor === "check" ? runsItsScript : void 0
32164
+ };
32165
+ };
32166
+ /** A strict drive's rest: refused at an unscripted check step, or with an actor no drive reached. */
32167
+ const strictRest = (doc, actors, reached) => {
32168
+ if (doc.kind !== "stalled" && doc.actor === "check") throw new Error(`strict drive: check step "${doc.state}" has no actor — script it, or drive with checks: "run"`);
32169
+ const never = reached === void 0 ? [] : Object.keys(actors).filter((step) => !reached.has(step));
32170
+ if (never.length > 0) throw new Error(`strict drive: the process rests at "${doc.state}", and never reached the actors for ${quoted(never)} — script a step only for the drive that reaches it`);
32171
+ return doc;
32172
+ };
32173
+ const refuseVerdict = (doc, returned) => {
32174
+ if (doc.actor !== "judge" && returned !== void 0) throw new Error(`strict drive: the actor for ${doc.actor} step "${doc.state}" returned a verdict; only a judge step takes one`);
32175
+ };
32176
+ const refuseNothingLanded = (doc, nothing) => {
32177
+ if (doc.actor === "human" && nothing) throw new Error(`strict drive: the actor for human step "${doc.state}" changed nothing, so nothing landed and the drive would repeat the step`);
32178
+ };
32050
32179
  const testRepo = (options) => {
32051
32180
  const repo = new InMemRepo();
32052
32181
  const env = { ...options.env };
32053
32182
  const { workflow } = options;
32054
32183
  const id = `module-${++registered}`;
32055
- 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))));
32056
32187
  for (const [path, content] of Object.entries(options.files ?? {})) repo.writeFile(path, content);
32057
32188
  repo.seedCommit("chore: initial commit");
32058
32189
  const canned = [...options.commands ?? []];
32059
- const commands = { get: (command) => {
32060
- const hit = canned.find(({ run }) => typeof run === "string" ? run === command : run.test(command));
32190
+ const table = (first = []) => ({ get: (command) => {
32191
+ const hit = [...first, ...canned].find(({ run }) => typeof run === "string" ? run === command : run.test(command));
32061
32192
  if (hit === void 0) return void 0;
32062
32193
  const { exit = 0, output = "", effect } = hit;
32063
32194
  return {
@@ -32066,10 +32197,16 @@ const testRepo = (options) => {
32066
32197
  output,
32067
32198
  effect: () => effect?.(self)
32068
32199
  };
32069
- } };
32200
+ } });
32201
+ const commands = table();
32202
+ const seenEnv = () => options.ambientFlags === false ? {
32203
+ ...Object.fromEntries(Object.keys(process.env).filter((name) => name.startsWith("GTD_FLAG_")).map((name) => [name, void 0])),
32204
+ ...env
32205
+ } : env;
32070
32206
  const gtd = async (...args) => {
32071
- const { io, result } = makeCapturingCliIo(repo, env);
32207
+ const { io, result } = makeCapturingCliIo(repo, seenEnv());
32072
32208
  if (typeof workflow !== "string") registry().set(id, workflow);
32209
+ for (const [specifier, module] of modules) registry().set(moduleKey(specifier), module);
32073
32210
  try {
32074
32211
  await runPromise(runCli([
32075
32212
  "node",
@@ -32078,6 +32215,7 @@ const testRepo = (options) => {
32078
32215
  ], io));
32079
32216
  } finally {
32080
32217
  registry().delete(id);
32218
+ for (const [specifier] of modules) registry().delete(moduleKey(specifier));
32081
32219
  }
32082
32220
  return result();
32083
32221
  };
@@ -32101,10 +32239,10 @@ const testRepo = (options) => {
32101
32239
  if (!applied.ok) throw new Error(`${what}: its script failed — ${applied.error ?? ""}`);
32102
32240
  };
32103
32241
  /** Runs a write command, then the script its `--json=script` twin prints. */
32104
- const runWrite = async (args, stdin) => {
32242
+ const runWrite = async (args, stdin, refused = (error) => error) => {
32105
32243
  const call = (...extra) => stdin === void 0 ? gtd(...args, ...extra) : withStdin(stdin, () => gtd(...args, ...extra));
32106
32244
  const reported = await call();
32107
- if (reported.exitCode !== 0) throw failed(`gtd ${args.join(" ")}`, reported);
32245
+ if (reported.exitCode !== 0) throw refused(failed(`gtd ${args.join(" ")}`, reported));
32108
32246
  const script = await call("--json=script");
32109
32247
  apply(`gtd ${args.join(" ")}`, script.stdout);
32110
32248
  return reported;
@@ -32121,19 +32259,42 @@ const testRepo = (options) => {
32121
32259
  return JSON.parse(result.stdout);
32122
32260
  };
32123
32261
  /** Lands what an actor did: a judge's verdict answered, anything else validated and landed. */
32124
- const act = async (doc, verdict) => {
32262
+ const act = async (doc, returned) => {
32125
32263
  if (doc.actor !== "judge") {
32126
32264
  if (doc.validate !== void 0) apply(`validating ${doc.file ?? doc.state}`, doc.validate);
32127
32265
  await runWrite(["land"]);
32128
32266
  return;
32129
32267
  }
32130
- if (typeof verdict !== "string") throw new Error(`the actor for judge step "${doc.state}" returned no verdict`);
32131
- await runWrite(["judge", "answer"], verdict);
32268
+ const actor = `the actor for judge step "${doc.state}"`;
32269
+ if (returned === void 0) throw new Error(`${actor} returned no verdict\n${expectedVerdict(doc)}`);
32270
+ const sent = isVerdict(returned) ? JSON.stringify(returned.answers) : returned;
32271
+ if (typeof sent !== "string") throw new Error(`${actor} returned ${Array.isArray(sent) ? "an array" : typeof sent === "object" ? "an object" : `a ${typeof sent}`}, not a JSON string — JSON.stringify it\n${expectedVerdict(doc)}`);
32272
+ await runWrite(["judge", "answer"], sent, (error) => /* @__PURE__ */ new Error(`${actor} sent a verdict gtd refused:\n${sent}\n${expectedVerdict(doc)}\n${error.message}`));
32273
+ };
32274
+ const reached = /* @__PURE__ */ new Set();
32275
+ /** One beat of a drive: the beat it rests at, or `undefined` to read the next. */
32276
+ const beat = async (doc, actors, { strict = true, checks, unreachedActors = "fail" }) => {
32277
+ if (doc.kind === "capture") {
32278
+ await runWrite(["land"]);
32279
+ return;
32280
+ }
32281
+ const { scripted, actor } = actorAt(doc, actors, checks);
32282
+ if (actor === void 0) return strict ? strictRest(doc, actors, unreachedActors === "fail" ? reached : void 0) : doc;
32283
+ if (scripted) reached.add(doc.state);
32284
+ const returned = await actor(doc, self);
32285
+ if (strict) refuseVerdict(doc, returned);
32286
+ const before = repo.commitHistory().length;
32287
+ await act(doc, returned);
32288
+ if (strict) refuseNothingLanded(doc, repo.commitHistory().length === before);
32132
32289
  };
32133
32290
  const self = {
32134
32291
  command: (command) => {
32135
32292
  canned.push(command);
32136
32293
  },
32294
+ setEnv: (name, value) => {
32295
+ if (value === void 0) delete env[name];
32296
+ else env[name] = value;
32297
+ },
32137
32298
  branch: (name, ref = "HEAD") => repo.createBranch(name, ref),
32138
32299
  checkout: (name) => repo.checkoutBranch(name),
32139
32300
  ref: (name, ref = "HEAD") => {
@@ -32148,43 +32309,47 @@ const testRepo = (options) => {
32148
32309
  remove: (path) => repo.deleteFile(path),
32149
32310
  commit: (message) => repo.seedCommit(message),
32150
32311
  show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
32151
- runScript: (script) => {
32152
- const flow = applyFlowScript(repo, commands, script);
32153
- 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);
32154
32316
  else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`);
32155
32317
  },
32156
32318
  gtd,
32157
- start: (workflow, ...args) => begin([
32158
- "--workflow",
32159
- workflow,
32319
+ start: (name, ...args) => begin([
32320
+ "start",
32321
+ name,
32160
32322
  ...args
32161
32323
  ]),
32162
32324
  door: (name, ...args) => begin([
32163
- "door",
32325
+ "start",
32164
32326
  name,
32165
32327
  ...args
32166
32328
  ]),
32167
32329
  land: () => runWrite(["land"]),
32168
32330
  next,
32169
- drive: async (actors = {}, { maxBeats = 50 } = {}) => {
32170
- for (let beat = 0; beat < maxBeats; beat++) {
32331
+ drive: async (actors = {}, options = {}) => {
32332
+ const { maxBeats = 50 } = options;
32333
+ const passed = [];
32334
+ for (let n = 0; n < maxBeats; n++) {
32171
32335
  const doc = await next();
32172
- if (doc.kind === "stalled") return doc;
32173
- if (doc.kind === "capture") {
32174
- await runWrite(["land"]);
32175
- continue;
32176
- }
32177
- const actor = actors[doc.state];
32178
- if (actor === void 0) return doc;
32179
- await act(doc, await actor(doc, self));
32336
+ passed.push(doc.state);
32337
+ const rest = await beat(doc, actors, options);
32338
+ if (rest !== void 0) return rest;
32180
32339
  }
32181
- throw new Error(`the process did not rest within ${maxBeats} beats`);
32340
+ throw new Error(`the process did not rest within ${maxBeats} beats; it passed through:\n${passed.join(" → ")}`);
32182
32341
  },
32183
32342
  subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
32184
32343
  messages: () => repo.commitHistory().map(({ message }) => message),
32185
- hashes: () => repo.commitHistory().map(({ hash }) => hash)
32344
+ hashes: () => repo.commitHistory().map(({ hash }) => hash),
32345
+ commits: () => repo.commitHistory().map(({ hash, message }) => ({
32346
+ hash,
32347
+ subject: message.split("\n")[0] ?? "",
32348
+ message,
32349
+ trailers: trailersOf(message)
32350
+ }))
32186
32351
  };
32187
32352
  return self;
32188
32353
  };
32189
32354
  //#endregion
32190
- export { 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 };