@pmelab/gtd 19.0.0 → 20.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30964,7 +30964,7 @@ const changesSince = (hash, pattern) => {
30964
30964
  };
30965
30965
  /** The commit the process stands on at this point of the flow. */
30966
30966
  const head = () => ctx().head();
30967
- /** The process's diff base: the commit before it began, or the base `gtd --entry` fixed. */
30967
+ /** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
30968
30968
  const start = () => ctx().start();
30969
30969
  /** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
30970
30970
  const skillsFor = (localName) => ctx().skillsFor(localName);
@@ -47622,6 +47622,9 @@ const TRAILER_READERS = {
47622
47622
  "Gtd-Step": (value, into) => {
47623
47623
  into.step ??= parseStepId(value);
47624
47624
  },
47625
+ "Gtd-Workflow": (value, into) => {
47626
+ if (value !== "") into.workflow ??= value;
47627
+ },
47625
47628
  "Gtd-Judge": (value, into) => {
47626
47629
  const parsed = parseJson(value);
47627
47630
  if (isVerdict(parsed)) into.judge.push(parsed);
@@ -47646,6 +47649,7 @@ const parseCommitMessage = (message) => {
47646
47649
  const subject = (newline === -1 ? message : message.slice(0, newline)).trim();
47647
47650
  const trailers = {
47648
47651
  step: void 0,
47652
+ workflow: void 0,
47649
47653
  reviewBase: void 0,
47650
47654
  judge: [],
47651
47655
  vars: {},
@@ -47662,6 +47666,7 @@ const parseCommitMessage = (message) => {
47662
47666
  const formatCommitMessage = (spec) => {
47663
47667
  const lines = [];
47664
47668
  if (spec.step !== void 0) lines.push(`Gtd-Step: ${formatStepId(spec.step)}`);
47669
+ if (spec.workflow !== void 0) lines.push(`Gtd-Workflow: ${spec.workflow}`);
47665
47670
  if (spec.reviewBase !== void 0) lines.push(`Gtd-Review-Base: ${spec.reviewBase}`);
47666
47671
  for (const [name, value] of Object.entries(spec.vars ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) lines.push(`Gtd-Var: ${name}=${value}`);
47667
47672
  if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
@@ -47769,11 +47774,6 @@ const unknownOptions = (step) => {
47769
47774
  return `gtd: step "${step.name}": unknown key(s) ${unknown.join(", ")} in ${step.kind}() options`;
47770
47775
  };
47771
47776
  const replay = async (input) => {
47772
- let entryRead = false;
47773
- const args = { get entry() {
47774
- entryRead = true;
47775
- return input.episode.entry;
47776
- } };
47777
47777
  const commits = input.episode.commits.map((c) => ({
47778
47778
  ...c,
47779
47779
  parsed: parseCommitMessage(c.message)
@@ -47964,8 +47964,7 @@ const replay = async (input) => {
47964
47964
  kind: "rest",
47965
47965
  rest: step,
47966
47966
  trace,
47967
- landed,
47968
- entryRead
47967
+ landed
47969
47968
  });
47970
47969
  pendingUsed = true;
47971
47970
  landed = step;
@@ -48072,7 +48071,7 @@ const replay = async (input) => {
48072
48071
  };
48073
48072
  installContext(context);
48074
48073
  try {
48075
- const flowDone = input.flow(args).then(() => {
48074
+ const flowDone = input.flow({}).then(() => {
48076
48075
  if (outcome === void 0 && trace.length === 0) outcome = {
48077
48076
  kind: "failed",
48078
48077
  message: "gtd: the flow returned without reaching any step"
@@ -48381,7 +48380,7 @@ const startGateBlockedMessage = () => `The test baseline is red — gtd will not
48381
48380
  \`.gtd/FEEDBACK.md\` holds the failing output.
48382
48381
 
48383
48382
  What each change does next (then run \`gtd land\`):
48384
- - **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --entry fix-precheck\` from a clean \`idle\`.
48383
+ - **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --workflow fix\` from a clean \`idle\`.
48385
48384
  `;
48386
48385
  const reviewGateBlockedMessage = () => `The test baseline is red — gtd will not start a review on a broken suite.
48387
48386
  \`.gtd/FEEDBACK.md\` holds the failing output.
@@ -49141,7 +49140,7 @@ const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuiteP
49141
49140
  model: coder(),
49142
49141
  system: builderSystem()
49143
49142
  });
49144
- const fix = (built) => agentWithSkills("fix", buildFixPrompt(built), {
49143
+ const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
49145
49144
  label: "Fixing the check",
49146
49145
  file: FEEDBACK,
49147
49146
  model: coder(),
@@ -49687,7 +49686,7 @@ const routeNotes = async (notes, r) => {
49687
49686
  }
49688
49687
  if (nits.length > 0) {
49689
49688
  await guarded(r.frozen, () => fixNits(nits), "review")();
49690
- await healthy(guarded(r.frozen, fix, "review"), { escalations: r.escalations });
49689
+ await healthy(guarded(r.frozen, fixCheck, "review"), { escalations: r.escalations });
49691
49690
  }
49692
49691
  if (edits.length > 0) {
49693
49692
  await r.close();
@@ -49750,7 +49749,7 @@ const reviewOnce = async (base, carry, escalations, frozen) => {
49750
49749
  const risks = reviewRisks(read(".gtd/REVIEW.md") ?? "");
49751
49750
  if (risks.length === 0) return;
49752
49751
  await guarded(frozen, () => fixRisks(risks), "review")();
49753
- await healthy(guarded(frozen, fix, "review"), { escalations });
49752
+ await healthy(guarded(frozen, fixCheck, "review"), { escalations });
49754
49753
  await reviewing(base, carry);
49755
49754
  };
49756
49755
  /**
@@ -49759,7 +49758,7 @@ const reviewOnce = async (base, carry, escalations, frozen) => {
49759
49758
  * another planning lap, questions are answered in place, nits fixed in one
49760
49759
  * batch, praise dropped.
49761
49760
  */
49762
- const review = async (base, escalations = { rounds: 0 }, frozen) => {
49761
+ const review$1 = async (base, escalations = { rounds: 0 }, frozen) => {
49763
49762
  let carry;
49764
49763
  for (;;) {
49765
49764
  await reviewOnce(base, carry, escalations, frozen);
@@ -49783,9 +49782,9 @@ const review = async (base, escalations = { rounds: 0 }, frozen) => {
49783
49782
  const buildTail = (fixFirst, base, built) => scope("build", async () => {
49784
49783
  const frozen = built?.frozen;
49785
49784
  const escalations = { rounds: 0 };
49786
- const guardedFix = guarded(frozen, () => fix());
49785
+ const guardedFix = guarded(frozen, () => fixCheck());
49787
49786
  let redFirst = fixFirst;
49788
- if (!fixFirst) await healthy(guarded(frozen, () => fix(built)), {
49787
+ if (!fixFirst) await healthy(guarded(frozen, () => fixCheck(built)), {
49789
49788
  escalations,
49790
49789
  sweepOnGreen: [ARCHITECTURE]
49791
49790
  });
@@ -49801,7 +49800,7 @@ const buildTail = (fixFirst, base, built) => scope("build", async () => {
49801
49800
  }
49802
49801
  const lap = lapped ? "clean" : await qualityLap();
49803
49802
  lapped = true;
49804
- if (lap === "clean") return review(base, escalations, frozen);
49803
+ if (lap === "clean") return review$1(base, escalations, frozen);
49805
49804
  if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
49806
49805
  else redFirst = true;
49807
49806
  }
@@ -49856,8 +49855,8 @@ const access = (vars) => ({
49856
49855
  ...Object.fromEntries(lensesOf(vars.qualityReviews ?? "").map((lens) => [`build.quality.${lens}`, { write: [] }]))
49857
49856
  });
49858
49857
  //#endregion
49859
- //#region src/workflows/unified.ts
49860
- var unified_exports = /* @__PURE__ */ __exportAll({
49858
+ //#region src/workflows/bundled.ts
49859
+ var bundled_exports = /* @__PURE__ */ __exportAll({
49861
49860
  ARCHITECTURE: () => ARCHITECTURE,
49862
49861
  ESCALATION: () => ESCALATION,
49863
49862
  FEEDBACK: () => FEEDBACK,
@@ -49884,17 +49883,20 @@ var unified_exports = /* @__PURE__ */ __exportAll({
49884
49883
  collecting: () => collecting,
49885
49884
  declaredTests: () => declaredTests,
49886
49885
  decompose: () => decompose,
49887
- default: () => unified,
49886
+ default: () => feature,
49888
49887
  defaults: () => defaults,
49889
49888
  describeEscalation: () => describeEscalation,
49890
49889
  design: () => design,
49890
+ doors: () => doors,
49891
49891
  driftedScenarios: () => driftedScenarios,
49892
49892
  envDefaults: () => envDefaults,
49893
49893
  escalate: () => escalate,
49894
49894
  escalation: () => escalation,
49895
49895
  escalationExhausted: () => escalationExhausted,
49896
49896
  fastSuiteGuard: () => fastSuiteGuard,
49897
+ feature: () => feature,
49897
49898
  fix: () => fix,
49899
+ fixCheck: () => fixCheck,
49898
49900
  fixNits: () => fixNits,
49899
49901
  fixQuality: () => fixQuality,
49900
49902
  fixQualityFindings: () => fixQualityFindings,
@@ -49986,34 +49988,35 @@ const ordinaryStart = async () => {
49986
49988
  await gate("start-gate", startGateBlockedMessage());
49987
49989
  await planAndBuild(start());
49988
49990
  };
49989
- const ENTRIES = [
49990
- "fix-precheck",
49991
- "review-gate.check",
49992
- "start-gate.check"
49993
- ];
49994
- async function unified({ entry }) {
49995
- if (entry === void 0) return ordinaryStart();
49996
- if (entry === "fix-precheck") {
49997
- if (await baseline("fix-precheck")) return;
49998
- return afterTail(await buildTail(true, start()));
49999
- }
50000
- if (entry === "review-gate.check") {
50001
- await gate("review-gate", reviewGateBlockedMessage());
50002
- return afterTail(await buildTail(false, start()));
50003
- }
50004
- if (entry === "start-gate.check") {
50005
- await gate("start-gate", startGateBlockedMessage());
50006
- return planAndBuild(start());
50007
- }
50008
- refuse(`"${entry}" is not an enterable state — enterable states:\n${ENTRIES.map((name) => ` ${name}`).join("\n")}`);
50009
- }
49991
+ /** Repair a red baseline through the build tail, as its own reviewed commit. */
49992
+ const fix = async () => {
49993
+ if (await baseline("fix-precheck")) return;
49994
+ return afterTail(await buildTail(true, start()));
49995
+ };
49996
+ /** Pure review of everything since `reviewBase`. */
49997
+ const review = async () => {
49998
+ await gate("review-gate", reviewGateBlockedMessage());
49999
+ return afterTail(await buildTail(false, start()));
50000
+ };
50001
+ const feature = ordinaryStart;
50010
50002
  const summary = summaryPrompt;
50011
50003
  const steering = {
50012
50004
  [REQUIREMENTS]: "qa",
50013
50005
  [ARCHITECTURE]: "qa",
50014
50006
  [REVIEW]: "review"
50015
50007
  };
50016
- const base = (entry, vars) => entry === "review-gate.check" ? vars.reviewBase ?? "" : void 0;
50008
+ const base = (workflow, vars) => workflow === "review" ? vars.reviewBase ?? "" : void 0;
50009
+ const doors = {
50010
+ fix: { workflow: "fix" },
50011
+ review: {
50012
+ workflow: "review",
50013
+ args: [{
50014
+ name: "base",
50015
+ optional: true
50016
+ }],
50017
+ vars: ({ base }) => base === void 0 ? {} : { reviewBase: base }
50018
+ }
50019
+ };
50017
50020
  //#endregion
50018
50021
  //#region src/platform/Host.ts
50019
50022
  const toError$1 = (e) => e instanceof Error ? e : new Error(String(e));
@@ -50206,6 +50209,8 @@ const makeGitImpl = (executor, root) => {
50206
50209
  resolveRef: (ref) => exec("git", "rev-parse", "--verify", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
50207
50210
  readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
50208
50211
  isAncestor: (a, b) => run$2(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
50212
+ mergeBase: (a, b) => exec("git", "merge-base", a, b).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
50213
+ defaultBranch: () => exec("git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD").pipe(map$4((s) => s.trim()), catchAll(() => succeed$3("")), map$4((name) => name === "" ? "main" : name)),
50209
50214
  topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
50210
50215
  gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
50211
50216
  gitCommonDir: () => exec("git", "rev-parse", "--path-format=absolute", "--git-common-dir").pipe(map$4((s) => s.trim())),
@@ -58112,45 +58117,71 @@ const load = gen(function* () {
58112
58117
  const { levels, compiled, decodeDiagnostics } = yield* readRc;
58113
58118
  const module = yield* discovery.workflowModule(host.root, host.home);
58114
58119
  const loaded = yield* try_({
58115
- try: () => loadWorkflow(module),
58120
+ try: () => loadWorkflows(module),
58116
58121
  catch: (e) => new GtdError(`gtd config:\n - ${module?.filepath ?? "(built-in default)"}: ${e instanceof Error ? e.message : String(e)}`)
58117
58122
  });
58118
58123
  const layerOrder = [...levels.map((level) => level.filepath), ...module ? [module.filepath] : []];
58119
58124
  const diagnostics = dedupeDiagnostics(sortDiagnostics([
58120
58125
  ...decodeDiagnostics,
58121
58126
  ...compiled.diagnostics,
58122
- ...wrongKindDiagnostics(loaded, compiled)
58127
+ ...wrongKindDiagnostics(loaded.files, compiled)
58123
58128
  ], layerOrder));
58124
58129
  yield* failOnErrors(diagnostics);
58125
- const initial = yield* firstStep(loaded, headTree(yield* Workspace));
58130
+ const initial = yield* firstStep(loaded.default, headTree(yield* Workspace));
58131
+ const steering = Object.assign({}, ...loaded.files.map((f) => f.steering));
58132
+ const scopesOf = (file) => {
58133
+ const vars = resolveVars(file.defaults, compiled.rcVars, {}, host.env);
58134
+ try {
58135
+ return [...Object.keys(file.skills(vars)), ...Object.keys(file.access(vars))];
58136
+ } catch {
58137
+ return [];
58138
+ }
58139
+ };
58140
+ const define = (entry) => ({
58141
+ flow: entry.flow,
58142
+ summary: entry.file.summary,
58143
+ base: entry.file.base,
58144
+ steering,
58145
+ modes: compiled.modes,
58146
+ skills: entry.file.skills,
58147
+ configuredSkills: compiled.rcSkills,
58148
+ skillsKeys: compiled.skillsKeys,
58149
+ access: entry.file.access,
58150
+ configuredAccess: compiled.access,
58151
+ accessKeys: compiled.accessKeys,
58152
+ knownScopes: () => loaded.files.filter((f) => f !== entry.file).flatMap(scopesOf),
58153
+ skillsOrigin: entry.file.origin,
58154
+ initial
58155
+ });
58156
+ const names = [...loaded.catalogue.keys()].sort();
58157
+ const resolve = (name, entry) => ({
58158
+ name,
58159
+ def: define(entry),
58160
+ vars: { ...entry.file.defaults },
58161
+ env: { ...entry.file.envDefaults }
58162
+ });
58163
+ const defaultWorkflow = resolve(loaded.default.name, loaded.default);
58126
58164
  return {
58127
- workflow: {
58128
- flow: loaded.flow,
58129
- summary: loaded.summary,
58130
- base: loaded.base,
58131
- steering: loaded.steering,
58132
- modes: compiled.modes,
58133
- skills: loaded.skills,
58134
- configuredSkills: compiled.rcSkills,
58135
- skillsKeys: compiled.skillsKeys,
58136
- access: loaded.access,
58137
- configuredAccess: compiled.access,
58138
- accessKeys: compiled.accessKeys,
58139
- skillsOrigin: module?.filepath ?? "(built-in default)",
58140
- initial
58165
+ workflow: defaultWorkflow.def,
58166
+ defaultWorkflow,
58167
+ workflowNames: names,
58168
+ doors: loaded.doors,
58169
+ workflowNamed: (name) => {
58170
+ const entry = loaded.catalogue.get(name);
58171
+ return entry === void 0 ? void 0 : resolve(name, entry);
58141
58172
  },
58142
- workflowVars: { ...loaded.defaults },
58143
58173
  rcVars: compiled.rcVars,
58144
- workflowEnv: { ...loaded.envDefaults },
58145
58174
  rcEnv: compiled.rcEnv,
58146
58175
  ...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
58147
58176
  warnings: diagnostics.filter((d) => d.severity === "warning"),
58148
58177
  configFiles: levels.map((level) => level.filepath),
58149
- workflowFiles: loaded.dependencyFiles
58178
+ workflowFiles: loaded.files.flatMap((f) => f.dependencyFiles)
58150
58179
  };
58151
58180
  });
58152
58181
  /** A name under the wrong key (or under both) in a `.gtdrc` layer: each is an error at its own origin. */
58153
- const wrongKindDiagnostics = (loaded, compiled) => {
58182
+ const wrongKindDiagnostics = (files, compiled) => {
58183
+ const isEnvOnly = (key) => files.some((f) => Object.hasOwn(f.envDefaults, key)) && !files.some((f) => Object.hasOwn(f.defaults, key));
58184
+ const isVarOnly = (key) => files.some((f) => Object.hasOwn(f.defaults, key)) && !files.some((f) => Object.hasOwn(f.envDefaults, key));
58154
58185
  const error = (path, message, origin) => ({
58155
58186
  severity: "error",
58156
58187
  path,
@@ -58158,9 +58189,9 @@ const wrongKindDiagnostics = (loaded, compiled) => {
58158
58189
  origin
58159
58190
  });
58160
58191
  const diagnostics = [];
58161
- for (const { key, origin } of compiled.varsKeys) if (Object.hasOwn(loaded.envDefaults, key)) diagnostics.push(error(["vars", key], `"vars.${key}" is an environment setting — move it under "env:"`, origin));
58192
+ for (const { key, origin } of compiled.varsKeys) if (isEnvOnly(key)) diagnostics.push(error(["vars", key], `"vars.${key}" is an environment setting — move it under "env:"`, origin));
58162
58193
  for (const { key, origin } of compiled.envKeys) {
58163
- if (Object.hasOwn(loaded.defaults, key)) diagnostics.push(error(["env", key], `"env.${key}" is a process setting — move it under "vars:"`, origin));
58194
+ if (isVarOnly(key)) diagnostics.push(error(["env", key], `"env.${key}" is a process setting — move it under "vars:"`, origin));
58164
58195
  if (compiled.varsKeys.some((v) => v.key === key)) diagnostics.push(error(["env", key], `"${key}" is declared under both "vars:" and "env:" — a setting is one kind or the other`, origin));
58165
58196
  }
58166
58197
  return diagnostics;
@@ -58188,7 +58219,7 @@ const jiti = () => {
58188
58219
  return jitiModule.createJiti(import.meta.url, {
58189
58220
  virtualModules: {
58190
58221
  "@pmelab/gtd/flows": flows_exports,
58191
- "@pmelab/gtd/workflow": unified_exports
58222
+ "@pmelab/gtd/workflow": bundled_exports
58192
58223
  },
58193
58224
  fsCache: false,
58194
58225
  moduleCache: false,
@@ -58225,14 +58256,19 @@ const optionalFunction = (exports, name) => {
58225
58256
  if (typeof value !== "function") throw new Error(`the "${name}" export is not a function`);
58226
58257
  return value;
58227
58258
  };
58228
- /**
58229
- * Read a workflow module: the default export is the flow; `defaults`,
58230
- * `envDefaults`, `summary`, `base` and `steering` are optional; any other export is ignored.
58231
- */
58232
- const fromModule = (exported, origin, dependencyFiles) => {
58233
- const exports = typeof exported === "object" && exported !== null ? exported : {};
58234
- const flow = exports.default;
58235
- if (typeof flow !== "function") throw new Error("the default export is not a flow — export default an async function");
58259
+ const RESERVED_EXPORTS = /* @__PURE__ */ new Set([
58260
+ "default",
58261
+ "summary",
58262
+ "base",
58263
+ "defaults",
58264
+ "envDefaults",
58265
+ "steering",
58266
+ "skills",
58267
+ "access",
58268
+ "doors"
58269
+ ]);
58270
+ /** A module's per-file exports: `defaults`, `envDefaults`, `steering`, `skills`, `access`, `summary`, `base`. */
58271
+ const fileFrom = (exports, origin, dependencyFiles) => {
58236
58272
  const defaults = stringRecord(exports, "defaults");
58237
58273
  const envDefaults = stringRecord(exports, "envDefaults");
58238
58274
  for (const [exportName, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) {
@@ -58242,7 +58278,6 @@ const fromModule = (exported, origin, dependencyFiles) => {
58242
58278
  const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
58243
58279
  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`);
58244
58280
  return {
58245
- flow,
58246
58281
  defaults,
58247
58282
  envDefaults,
58248
58283
  steering: stringRecord(exports, "steering"),
@@ -58254,23 +58289,120 @@ const fromModule = (exported, origin, dependencyFiles) => {
58254
58289
  dependencyFiles
58255
58290
  };
58256
58291
  };
58292
+ const DOOR_NAME_RULE = /^[a-z][a-z0-9-]*$/;
58293
+ /** Why one door's `args` are malformed, or `undefined` when they are well-formed. */
58294
+ const argsProblem = (args) => {
58295
+ if (args === void 0) return void 0;
58296
+ if (!Array.isArray(args)) return `has "args" that is not an array`;
58297
+ const seen = /* @__PURE__ */ new Set();
58298
+ let optional = false;
58299
+ for (const arg of args) {
58300
+ if (typeof arg?.name !== "string") return `has an arg without a "name"`;
58301
+ if (seen.has(arg.name)) return `declares duplicate arg "${arg.name}"`;
58302
+ seen.add(arg.name);
58303
+ if (arg.optional === true) optional = true;
58304
+ else if (optional) return `declares required arg "${arg.name}" after an optional one`;
58305
+ }
58306
+ };
58307
+ /** Why one door is malformed, or `undefined` when it is well-formed. */
58308
+ const doorProblem = (name, door) => {
58309
+ if (!DOOR_NAME_RULE.test(name)) return `is not a valid door name — it must match ${DOOR_NAME_RULE}`;
58310
+ const { workflow, args, vars } = door ?? {};
58311
+ if (typeof workflow !== "string") return `must be an object with a "workflow" name`;
58312
+ const varsOk = vars === void 0 || typeof vars === "function";
58313
+ return argsProblem(args) ?? (varsOk ? void 0 : `has "vars" that is not a function`);
58314
+ };
58315
+ /** A module's `doors` export, shape-checked; the workflow names are checked later, against the merged catalogue. */
58316
+ const doorsFrom = (exports) => {
58317
+ const value = exports["doors"];
58318
+ if (value === void 0) return /* @__PURE__ */ new Map();
58319
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`the "doors" export is not a record of doors`);
58320
+ for (const [name, door] of Object.entries(value)) {
58321
+ const problem = doorProblem(name, door);
58322
+ if (problem !== void 0) throw new Error(`door "${name}" ${problem}`);
58323
+ }
58324
+ return new Map(Object.entries(value));
58325
+ };
58326
+ /** Merge bundled and repo doors (repo wins), then check each names a workflow the merged catalogue holds. */
58327
+ const mergeDoors = (bundled, repo, catalogue) => {
58328
+ const doors = new Map([...bundled, ...repo].sort(([a], [b]) => a.localeCompare(b)));
58329
+ 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(", ")}`);
58330
+ return doors;
58331
+ };
58332
+ const namespaceOf = (exported) => typeof exported === "object" && exported !== null ? exported : {};
58333
+ /**
58334
+ * The bundled module is never scanned: its helper exports stay helpers, and
58335
+ * exactly these three are startable.
58336
+ */
58337
+ const loadBundled = () => {
58338
+ const file = fileFrom(namespaceOf(bundled_exports), BUILT_IN_ORIGIN, []);
58339
+ const { feature, review, fix } = bundled_exports;
58340
+ return {
58341
+ file,
58342
+ doors: doorsFrom(namespaceOf(bundled_exports)),
58343
+ catalogue: /* @__PURE__ */ new Map([
58344
+ ["feature", {
58345
+ flow: feature,
58346
+ file
58347
+ }],
58348
+ ["review", {
58349
+ flow: review,
58350
+ file
58351
+ }],
58352
+ ["fix", {
58353
+ flow: fix,
58354
+ file
58355
+ }]
58356
+ ])
58357
+ };
58358
+ };
58257
58359
  /**
58258
- * Evaluate `gtd.config.ts`, or take the bundled default. `cache` is jiti's
58259
- * OWN per-call module cache (unrelated to the instance's `moduleCache:
58260
- * false` — that governs reuse ACROSS calls, this one is populated fresh
58261
- * DURING this one) — passing an empty object in has the side effect of
58262
- * collecting every real file jiti resolved evaluating this module, module
58263
- * AND every relative import it pulled in, transitively. Virtual modules
58264
- * (`@pmelab/gtd/flows`) never touch it — no real file backs them.
58360
+ * Evaluate `gtd.config.ts` and overlay it on the bundled workflows. Every
58361
+ * exported function of the repo file but the reserved names is a workflow
58362
+ * named by its export. `cache` is jiti's OWN per-call module cache (unrelated
58363
+ * to the instance's `moduleCache: false`) — passing an empty object in has
58364
+ * the side effect of collecting every real file jiti resolved evaluating this
58365
+ * module, module AND every relative import it pulled in, transitively.
58265
58366
  */
58266
- const loadWorkflow = (module) => {
58267
- if (module === void 0) return fromModule(unified_exports, BUILT_IN_ORIGIN, []);
58367
+ const loadWorkflows = (module) => {
58368
+ const bundled = loadBundled();
58369
+ const bundledDefault = {
58370
+ flow: feature,
58371
+ file: bundled.file,
58372
+ name: "feature"
58373
+ };
58374
+ if (module === void 0) return {
58375
+ files: [bundled.file],
58376
+ catalogue: bundled.catalogue,
58377
+ doors: mergeDoors(bundled.doors, /* @__PURE__ */ new Map(), bundled.catalogue),
58378
+ default: bundledDefault
58379
+ };
58268
58380
  const cache = {};
58269
- const exported = jiti().evalModule(module.source, {
58381
+ const exports = namespaceOf(jiti().evalModule(module.source, {
58270
58382
  filename: module.filepath,
58271
58383
  cache
58272
- });
58273
- return fromModule(exported, module.filepath, Object.keys(cache));
58384
+ }));
58385
+ const file = fileFrom(exports, module.filepath, Object.keys(cache));
58386
+ const catalogue = new Map(bundled.catalogue);
58387
+ for (const [name, value] of Object.entries(exports)) {
58388
+ if (RESERVED_EXPORTS.has(name) || typeof value !== "function") continue;
58389
+ catalogue.set(name, {
58390
+ flow: value,
58391
+ file
58392
+ });
58393
+ }
58394
+ const flow = exports.default;
58395
+ if (flow !== void 0 && typeof flow !== "function") throw new Error("the default export is not a function");
58396
+ return {
58397
+ files: [bundled.file, file],
58398
+ catalogue,
58399
+ doors: mergeDoors(bundled.doors, doorsFrom(exports), catalogue),
58400
+ default: flow === void 0 ? bundledDefault : {
58401
+ flow,
58402
+ file,
58403
+ name: "default"
58404
+ }
58405
+ };
58274
58406
  };
58275
58407
  const headTree = (workspace) => {
58276
58408
  let entries;
@@ -58326,7 +58458,6 @@ const settingReads = (kind, watch) => {
58326
58458
  const replayToFirstStep = (loaded, vars, env, tree) => flatMap$4(promise(() => replay({
58327
58459
  flow: loaded.flow,
58328
58460
  episode: {
58329
- entry: void 0,
58330
58461
  base: {
58331
58462
  hash: "",
58332
58463
  tree
@@ -58340,7 +58471,7 @@ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$4(promise(() => r
58340
58471
  })), (outcome) => {
58341
58472
  if (outcome.kind === "rest") return succeed$3(outcome.rest.name);
58342
58473
  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";
58343
- return fail$7(new GtdError(`gtd config:\n - ${loaded.origin}: ${problem}`));
58474
+ return fail$7(new GtdError(`gtd config:\n - ${loaded.file.origin}: ${problem}`));
58344
58475
  });
58345
58476
  /**
58346
58477
  * The default entry's first step — where a finished process waits — found the
@@ -58356,13 +58487,13 @@ const firstStep = (loaded, head) => gen(function* () {
58356
58487
  const env = watchedEmptySettings();
58357
58488
  const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
58358
58489
  const reads = [...settingReads("process", vars), ...settingReads("environment", env)];
58359
- if (reads.length > 0) return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.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`));
58490
+ if (reads.length > 0) return yield* fail$7(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`));
58360
58491
  if (outcome._tag === "Left") return yield* fail$7(outcome.left);
58361
58492
  const name = outcome.right;
58362
58493
  if (!empty.read()) return name;
58363
58494
  const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
58364
58495
  if (atHead === name) return name;
58365
- return yield* fail$7(new GtdError(`gtd config:\n - ${loaded.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`));
58496
+ return yield* fail$7(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`));
58366
58497
  });
58367
58498
  var ConfigService = class ConfigService extends Tag("ConfigService")() {
58368
58499
  static Live = succeed$2(ConfigService, { load });
@@ -58401,6 +58532,7 @@ const checkKeys = (keys, known, message, section) => {
58401
58532
  origin
58402
58533
  })), [...new Set(keys.map((k) => k.origin))])));
58403
58534
  };
58535
+ const knownTo = (def, own) => [.../* @__PURE__ */ new Set([...own, ...def.knownScopes?.() ?? []])];
58404
58536
  /**
58405
58537
  * Skill lists for the settings actually in use. `.gtdrc` `skills:` keys are
58406
58538
  * judged here, not at config load: only now are the pinned/entry vars known.
@@ -58410,7 +58542,8 @@ const resolveScopeSkills = (def, vars) => gen(function* () {
58410
58542
  try: () => checkShape(def.skills(vars)),
58411
58543
  catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
58412
58544
  });
58413
- yield* checkKeys(def.skillsKeys, Object.keys(skills), unknownSkillsKeyMessage, "skills");
58545
+ const known = knownTo(def, Object.keys(skills));
58546
+ yield* checkKeys(def.skillsKeys, known, unknownSkillsKeyMessage, "skills");
58414
58547
  return skills;
58415
58548
  });
58416
58549
  const checkAccessShape = (value) => {
@@ -58430,7 +58563,8 @@ const resolveScopeAccess = (def, skills, vars) => gen(function* () {
58430
58563
  try: () => checkAccessShape(def.access(vars)),
58431
58564
  catch: (e) => fail$4(def.skillsOrigin, e instanceof Error ? e.message : String(e))
58432
58565
  });
58433
- yield* checkKeys(def.accessKeys, [.../* @__PURE__ */ new Set([...Object.keys(skills), ...Object.keys(access)])], unknownAccessKeyMessage, "access");
58566
+ const known = knownTo(def, [...Object.keys(skills), ...Object.keys(access)]);
58567
+ yield* checkKeys(def.accessKeys, known, unknownAccessKeyMessage, "access");
58434
58568
  return access;
58435
58569
  });
58436
58570
  //#endregion
@@ -63819,14 +63953,16 @@ const demandOf = (input) => {
63819
63953
  * instead of failing loudly.
63820
63954
  */
63821
63955
  const statusOf = (input) => {
63822
- const { rendered, idle, log, changes, next, cost, costByModel } = input;
63956
+ const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
63823
63957
  return {
63824
63958
  idle,
63959
+ initial,
63825
63960
  model: rendered.model,
63826
63961
  system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
63827
63962
  log,
63828
63963
  state: rendered.state,
63829
63964
  actor: rendered.actor,
63965
+ workflow,
63830
63966
  label: rendered.label,
63831
63967
  memory: rendered.memory,
63832
63968
  file: rendered.file,
@@ -63875,7 +64011,7 @@ const dispatchFieldsOf = (demand) => {
63875
64011
  }
63876
64012
  };
63877
64013
  /**
63878
- * Flatten a `Demand` plus its `BeatStatus` into the single 21-key document
64014
+ * Flatten a `Demand` plus its `BeatStatus` into the single 23-key document
63879
64015
  * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
63880
64016
  * `costByModel` are omitted together, exactly when no cost was recorded
63881
64017
  * (`cost <= 0`).
@@ -63887,6 +64023,7 @@ const beatDocument = (demand, status) => {
63887
64023
  kind: demand.kind,
63888
64024
  content: demand.content,
63889
64025
  idle: status.idle,
64026
+ initial: status.initial,
63890
64027
  session,
63891
64028
  model: status.model,
63892
64029
  system: status.system,
@@ -63894,6 +64031,7 @@ const beatDocument = (demand, status) => {
63894
64031
  log: status.log,
63895
64032
  state: status.state,
63896
64033
  actor: status.actor,
64034
+ workflow: status.workflow,
63897
64035
  label: status.label,
63898
64036
  memory: status.memory,
63899
64037
  file: status.file,
@@ -63932,7 +64070,7 @@ const definedFields = (entries) => {
63932
64070
  return result;
63933
64071
  };
63934
64072
  /**
63935
- * The header block's own lines — `State:`/`Awaits:`/optional `Label:`/
64073
+ * The header block's own lines — `State:`/`Awaits:`/`Workflow:`/optional `Label:`/
63936
64074
  * `Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`(+breakdown)/`Pending:`/`Next:`,
63937
64075
  * in that order. Used by `renderBeatPlain`'s header-showing kinds below.
63938
64076
  */
@@ -63947,6 +64085,7 @@ const beatHeaderLines = (document) => {
63947
64085
  return [
63948
64086
  `State: ${document.state}`,
63949
64087
  `Awaits: ${document.actor}`,
64088
+ `Workflow: ${document.workflow}`,
63950
64089
  ...Object.entries(optional).map(([key, value]) => `${key}: ${value}`),
63951
64090
  ...costStatusLines(document.cost ?? 0, document.costByModel ?? []),
63952
64091
  ...pendingStatusLines(document.changes),
@@ -64148,7 +64287,7 @@ const accessRefusal = (changes, write) => {
64148
64287
  ].join("\n");
64149
64288
  };
64150
64289
  //#endregion
64151
- //#region src/step/planEntry.ts
64290
+ //#region src/step/planStart.ts
64152
64291
  const refusal = (message) => ({
64153
64292
  kind: "refusal",
64154
64293
  message
@@ -64160,43 +64299,42 @@ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) =>
64160
64299
  if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
64161
64300
  });
64162
64301
  /**
64163
- * `gtd --entry <name>`: start a brand NEW process at a workflow entry, writing
64164
- * an opening commit that carries every process setting as a `Gtd-Var:` trailer, plus a
64165
- * `Gtd-Review-Base:` trailer when the entry fixes the process's diff base.
64302
+ * `gtd --workflow <name>`: start a brand NEW process on a workflow, writing an
64303
+ * opening commit that names the workflow's first step and carries a
64304
+ * `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
64305
+ * a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base.
64166
64306
  * All validation is a REFUSAL, not an Effect failure.
64167
64307
  */
64168
- const planEntry = (current, actor, entry) => gen(function* () {
64169
- const { state: name, commandLabel, vars: varOverrides } = entry;
64170
- if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before entering`);
64171
- if (current.entryRefusal !== void 0) return refusal(`${commandLabel}: ${current.entryRefusal}`);
64308
+ const planStart = (current, actor, start) => gen(function* () {
64309
+ const { workflow, first, commandLabel, vars: varOverrides } = start;
64310
+ const name = workflow.name;
64311
+ if (!current.idle) return refusal(`${commandLabel}: a process is already underway (resting at "${current.state}") — finish it, or run \`gtd abandon\`, before starting another`);
64312
+ if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`);
64313
+ const { firstStep } = first;
64172
64314
  const config = yield* (yield* ConfigService).load;
64173
64315
  const declaredNames = Object.keys({
64174
- ...config.workflowVars,
64316
+ ...workflow.vars,
64175
64317
  ...config.rcVars
64176
64318
  });
64177
64319
  const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
64178
- ...config.workflowEnv,
64320
+ ...workflow.env,
64179
64321
  ...config.rcEnv
64180
64322
  }));
64181
64323
  if (overrideRefusal !== void 0) return overrideRefusal;
64182
- let base;
64183
- const baseOf = current.def.base;
64184
- const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
64324
+ const baseOf = workflow.def.base;
64325
+ const vars = resolveVars(workflow.vars, config.rcVars, varOverrides, (yield* Host).env);
64185
64326
  const multiline = multilineSetting(vars);
64186
64327
  if (multiline !== void 0) return refusal(`${commandLabel}: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`);
64187
64328
  const template = yield* try_({
64188
64329
  try: () => baseOf?.(name, vars),
64189
64330
  catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
64190
64331
  });
64191
- if (baseOf !== void 0 && template !== void 0) {
64192
- const resolved = yield* resolveBase(name, commandLabel, baseOf, template);
64193
- if (typeof resolved !== "string") return resolved;
64194
- base = resolved;
64195
- }
64196
- const subject = formatSubject(actor, name);
64332
+ const base = template === void 0 ? void 0 : yield* resolveBase(commandLabel, template);
64333
+ if (typeof base === "object") return base;
64334
+ const subject = formatSubject(actor, firstStep);
64197
64335
  return {
64198
- kind: "entry",
64199
- state: name,
64336
+ kind: "start",
64337
+ workflow: name,
64200
64338
  subject,
64201
64339
  steps: [{
64202
64340
  kind: "gitWrite",
@@ -64204,7 +64342,8 @@ const planEntry = (current, actor, entry) => gen(function* () {
64204
64342
  kind: "commitAll",
64205
64343
  message: formatCommitMessage({
64206
64344
  actor,
64207
- to: name,
64345
+ to: firstStep,
64346
+ workflow: name,
64208
64347
  ...base !== void 0 ? { reviewBase: base } : {},
64209
64348
  vars
64210
64349
  })
@@ -64218,16 +64357,21 @@ const planEntry = (current, actor, entry) => gen(function* () {
64218
64357
  }]
64219
64358
  };
64220
64359
  });
64221
- /** The entry's diff base: a commitish that resolves, is an ancestor of HEAD, and is not HEAD. */
64222
- const resolveBase = (name, commandLabel, base, value) => gen(function* () {
64223
- const rendered = value.trim();
64224
- if (rendered === "") return refusal(`${commandLabel}: "${name}"'s reviewBase template rendered blank — template: ${base.toString()}; pass the base with --var`);
64360
+ /**
64361
+ * The workflow's diff base: `git merge-base <base> HEAD`, where a blank base
64362
+ * means the default branch. The merge-base is what gets pinned, so a base on
64363
+ * another branch reviews exactly this branch's own commits.
64364
+ */
64365
+ const resolveBase = (commandLabel, value) => gen(function* () {
64225
64366
  const git = yield* GitService;
64226
- const resolvedBase = yield* either(git.resolveRef(rendered));
64227
- if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${rendered}" does not resolve to a commit`);
64228
- if (!(yield* git.isAncestor(resolvedBase.right, "HEAD"))) return refusal(`${commandLabel}: "${rendered}" is not an ancestor of HEAD`);
64229
- if (resolvedBase.right === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: "${rendered}" is HEAD — nothing to review`);
64230
- return resolvedBase.right;
64367
+ const rendered = value.trim();
64368
+ const target = rendered === "" ? yield* git.defaultBranch() : rendered;
64369
+ const resolvedBase = yield* either(git.resolveRef(target));
64370
+ if (resolvedBase._tag === "Left") return refusal(`${commandLabel}: "${target}" does not resolve to a commit`);
64371
+ const mergeBase = yield* git.mergeBase(resolvedBase.right, "HEAD");
64372
+ if (isNone(mergeBase)) return refusal(`${commandLabel}: "${target}" shares no common ancestor with HEAD`);
64373
+ if (mergeBase.value === (yield* git.resolveRef("HEAD"))) return refusal(`${commandLabel}: nothing to review: HEAD has no commits beyond ${target}`);
64374
+ return mergeBase.value;
64231
64375
  });
64232
64376
  //#endregion
64233
64377
  //#region src/Edge.ts
@@ -64240,9 +64384,10 @@ const ACTORS = /* @__PURE__ */ new Set([
64240
64384
  ]);
64241
64385
  /**
64242
64386
  * Where the episode HEAD belongs to begins, read off subjects alone. Walking
64243
- * back from HEAD: a trailer-less bare `gtd(human): <entry>` commit is what
64244
- * `gtd --entry` writes — no landing ever produces one — and opens an entered
64245
- * episode as its first commit; a commit that is not a gtd step commit, or one
64387
+ * back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
64388
+ * `gtd --workflow` writes — no landing ever produces one — and opens an
64389
+ * episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
64390
+ * a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
64246
64391
  * entering the flow's first step (a finished episode), bounds the episode
64247
64392
  * from below.
64248
64393
  */
@@ -64251,18 +64396,21 @@ const locateEpisode = (def, history) => {
64251
64396
  const message = parseCommitMessage(history[i].message);
64252
64397
  const subject = message.parsed;
64253
64398
  if (subject !== void 0 && message.step === void 0 && subject.from === void 0 && subject.actor === "human") return {
64254
- entry: subject.to,
64399
+ workflow: message.workflow,
64400
+ legacyOpening: message.workflow === void 0,
64255
64401
  baseIndex: i,
64256
64402
  processStart: i
64257
64403
  };
64258
64404
  if (subject === void 0 || !ACTORS.has(subject.actor) || subject.to === def.initial) return {
64259
- entry: void 0,
64405
+ workflow: void 0,
64406
+ legacyOpening: false,
64260
64407
  baseIndex: i,
64261
64408
  processStart: i + 1
64262
64409
  };
64263
64410
  }
64264
64411
  return {
64265
- entry: void 0,
64412
+ workflow: void 0,
64413
+ legacyOpening: false,
64266
64414
  baseIndex: -1,
64267
64415
  processStart: 0
64268
64416
  };
@@ -64296,7 +64444,8 @@ const runOf = (history, location, closingHash = void 0) => {
64296
64444
  const startParentHash = hashAt(history, location.processStart - 1) ?? EMPTY_TREE$1;
64297
64445
  const head = history[history.length - 1];
64298
64446
  return {
64299
- entry: location.entry,
64447
+ workflow: location.workflow,
64448
+ legacyOpening: location.legacyOpening,
64300
64449
  startHash: hashAt(history, location.processStart) ?? head?.hash ?? EMPTY_TREE$1,
64301
64450
  startParentHash,
64302
64451
  diffBase: first?.reviewBase ?? startParentHash,
@@ -64323,10 +64472,10 @@ const PAGE_TIERS = [
64323
64472
  * gtd step commit, or one landing the flow's initial state, bounds an
64324
64473
  * episode from below — EXCEPT a bare `gtd(human): <x>` subject (no `→`),
64325
64474
  * which is NEVER a boundary here, whatever `<x>` is. That bare shape is
64326
- * `locateEpisode`'s own `opening` branch (an entered episode's first commit),
64475
+ * `locateEpisode`'s own `opening` branch (a started episode's first commit),
64327
64476
  * which takes priority there over the initial-state check no matter what the
64328
- * entry is named — including an entry named after `def.initial` itself,
64329
- * where the two branches would otherwise disagree. A bare human commit can
64477
+ * workflow's first step is named — including one named after `def.initial`
64478
+ * itself, where the two branches would otherwise disagree. A bare human commit can
64330
64479
  * also be a self-loop step landing rather than a real opening (this
64331
64480
  * subject-only pass can't read the `Gtd-Step` trailer that would tell them
64332
64481
  * apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
@@ -64462,16 +64611,24 @@ const pendingTree = (workspace, entries, rewrite) => {
64462
64611
  * The one resolver of both setting kinds. Process settings: the recorded
64463
64612
  * (pinned) value wins per name, anything unrecorded resolves live — the
64464
64613
  * fallback for a process started before pinning existed. Environment settings
64465
- * are always live. `entryOverrides` are `--var` values, passed only while a
64466
- * process is being entered.
64614
+ * are always live. `startOverrides` are `--var` values, passed only while a
64615
+ * process is being started.
64467
64616
  */
64468
- const settingsFor = (config, pinnedVars, hostEnv, entryOverrides = {}) => ({
64617
+ const settingsFor = (config, workflow, pinnedVars, hostEnv, startOverrides = {}) => ({
64469
64618
  vars: {
64470
- ...resolveVars(config.workflowVars, config.rcVars, entryOverrides, hostEnv),
64619
+ ...resolveVars(workflow.vars, config.rcVars, startOverrides, hostEnv),
64471
64620
  ...pinnedVars
64472
64621
  },
64473
- env: resolveVars(config.workflowEnv, config.rcEnv, {}, hostEnv)
64622
+ env: resolveVars(workflow.env, config.rcEnv, {}, hostEnv)
64474
64623
  });
64624
+ /** The workflow a run replays on: its pinned one, else the default. */
64625
+ const workflowOf = (config, run) => {
64626
+ if (run.legacyOpening) return fail$7(/* @__PURE__ */ new Error("gtd: this process was opened by `gtd --entry`, which no longer exists — run `gtd abandon` to start over"));
64627
+ if (run.workflow === void 0) return succeed$3(config.defaultWorkflow);
64628
+ const named = config.workflowNamed(run.workflow);
64629
+ if (named !== void 0) return succeed$3(named);
64630
+ return fail$7(/* @__PURE__ */ new Error(`gtd: this process runs workflow "${run.workflow}", which gtd.config.ts no longer defines — run \`gtd abandon\` to start over`));
64631
+ };
64475
64632
  /**
64476
64633
  * `judgeBudgetBytes` is the one var that refuses rather than disabling its
64477
64634
  * mechanism when blanked: without a bound the judge payload is rejected
@@ -64488,7 +64645,6 @@ const judgeBudgetBytes = (vars) => {
64488
64645
  const replayFor = (setup, pending) => replay({
64489
64646
  flow: setup.def.flow,
64490
64647
  episode: {
64491
- entry: setup.run.entry,
64492
64648
  base: setup.base,
64493
64649
  commits: setup.episode
64494
64650
  },
@@ -64506,7 +64662,7 @@ const replayFor = (setup, pending) => replay({
64506
64662
  const replayError = (outcome) => {
64507
64663
  if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
64508
64664
  if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
64509
- if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default entry's first step — the workflow changed under this process; run `gtd abandon` to start over");
64665
+ if (outcome.kind === "ended") return /* @__PURE__ */ new Error("gtd: history ends the episode, but HEAD does not enter the default workflow's first step — the workflow changed under this process; run `gtd abandon` to start over");
64510
64666
  };
64511
64667
  const judgeDocument = (step) => step.request.kind === "judge" ? JSON.stringify({
64512
64668
  state: step.request.evidence,
@@ -64633,14 +64789,15 @@ const costByModel = (entries) => {
64633
64789
  })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
64634
64790
  };
64635
64791
  /** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
64636
- const restAt = (ref, entryOverrides = {}) => gen(function* () {
64792
+ const restAt = (ref, startOverrides = {}) => gen(function* () {
64637
64793
  const git = yield* GitService;
64638
64794
  const config = yield* (yield* ConfigService).load;
64639
64795
  const workspace = yield* Workspace;
64640
64796
  const host = yield* Host;
64641
- const def = config.workflow;
64642
- const run = yield* computeProcessRun(git, def, ref);
64643
- const { vars, env } = settingsFor(config, run.pinnedVars, host.env, entryOverrides);
64797
+ const run = yield* computeProcessRun(git, config.workflow, ref);
64798
+ const resolved = yield* workflowOf(config, run);
64799
+ const def = resolved.def;
64800
+ const { vars, env } = settingsFor(config, resolved, run.pinnedVars, host.env, startOverrides);
64644
64801
  const skills = yield* resolveScopeSkills(def, vars);
64645
64802
  const access = yield* resolveScopeAccess(def, skills, vars);
64646
64803
  const budget = yield* try_({
@@ -64688,6 +64845,7 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
64688
64845
  const memory = memoryOf(outcome.trace, run);
64689
64846
  return {
64690
64847
  def,
64848
+ workflowName: resolved.name,
64691
64849
  state: step.name,
64692
64850
  stepDef,
64693
64851
  actor: step.actor,
@@ -64709,21 +64867,20 @@ const restAt = (ref, entryOverrides = {}) => gen(function* () {
64709
64867
  });
64710
64868
  const currentRest = restAt(void 0);
64711
64869
  /**
64712
- * Why `gtd --entry <name>` cannot open a process here, or `undefined` when it
64713
- * can: the flow, handed `name`, must read it and reach a step from the tree
64714
- * the opening commit would capture.
64870
+ * Whether `gtd --workflow <name>` can start a process here: the workflow must
64871
+ * reach a step from the tree the opening commit would capture. That step is
64872
+ * what the opening commit's subject names.
64715
64873
  */
64716
- const entryRefusal = (rest, name, entryVars) => gen(function* () {
64874
+ const startRefusal = (rest, resolved, startVars) => gen(function* () {
64717
64875
  const config = yield* (yield* ConfigService).load;
64718
64876
  const host = yield* Host;
64719
- const { vars, env } = settingsFor(config, {}, host.env, entryVars);
64720
- const skills = yield* resolveScopeSkills(rest.def, vars);
64721
- const access = yield* resolveScopeAccess(rest.def, skills, vars);
64877
+ const { vars, env } = settingsFor(config, resolved, {}, host.env, startVars);
64878
+ const skills = yield* resolveScopeSkills(resolved.def, vars);
64879
+ const access = yield* resolveScopeAccess(resolved.def, skills, vars);
64722
64880
  const workspace = rest.setup.workspace;
64723
64881
  const outcome = yield* promise(() => replay({
64724
- flow: rest.def.flow,
64882
+ flow: resolved.def.flow,
64725
64883
  episode: {
64726
- entry: name,
64727
64884
  base: {
64728
64885
  hash: "",
64729
64886
  tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
@@ -64735,13 +64892,14 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
64735
64892
  start: "",
64736
64893
  budgetBytes: rest.setup.budget,
64737
64894
  skills,
64738
- configuredSkills: rest.def.configuredSkills,
64895
+ configuredSkills: resolved.def.configuredSkills,
64739
64896
  access,
64740
- configuredAccess: rest.def.configuredAccess
64897
+ configuredAccess: resolved.def.configuredAccess
64741
64898
  }));
64742
- if (outcome.kind === "refused") return outcome.message;
64743
- if (outcome.kind === "rest") return outcome.entryRead ? void 0 : `"${name}" is not an enterable state — this workflow reads no entry`;
64744
- return `"${name}" is not an enterable state — ${outcome.kind === "ended" ? "the flow reaches no step for it" : outcome.message}`;
64899
+ if (outcome.kind === "rest") return { firstStep: outcome.rest.name };
64900
+ if (outcome.kind === "refused") return { refusal: outcome.message };
64901
+ const why = outcome.kind === "ended" ? "the flow reaches no step" : outcome.message;
64902
+ return { refusal: `workflow "${resolved.name}" cannot start — ${why}` };
64745
64903
  });
64746
64904
  /** The review window's diff base at the rest. */
64747
64905
  const reviewBaseFor = (rest) => rest.step.request.options.base ?? rest.run.diffBase;
@@ -64759,8 +64917,8 @@ const renderRest = (rest) => succeed$3({
64759
64917
  * attempt at this very agent rest, and another dispatch would repeat it.
64760
64918
  */
64761
64919
  const stalledAt = (rest) => rest.changes.length === 0 && rest.stepDef.kind === "prompt" && rest.stepDef.allowEmpty !== true && rest.run.headTurn?.state === rest.state && rest.run.headTurn.actor === rest.actor && !rest.run.headTurn.step && rest.run.headTurn.empty;
64762
- /** No process is underway: the default entry's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
64763
- const noProcessUnderway = (rest) => rest.run.entry === void 0 && rest.state === rest.def.initial && rest.step.id.occurrence === 1;
64920
+ /** No process is underway: the default workflow's first step, first visit — a dirty tree there is a turn not yet landed, not a process. */
64921
+ const noProcessUnderway = (rest) => rest.run.workflow === void 0 && rest.state === rest.def.initial && rest.step.id.occurrence === 1;
64764
64922
  /** `idle` means exactly one thing: no process underway, clean tree. */
64765
64923
  const restIsIdle = (rest) => noProcessUnderway(rest) && rest.changes.length === 0;
64766
64924
  const isHumanReviewGate = (def) => def.actor === "human" && def.mode === "review";
@@ -64799,8 +64957,19 @@ const landingTarget = (outcome, rest) => {
64799
64957
  message: outcome.message
64800
64958
  });
64801
64959
  if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$7(new Error(outcome.message));
64802
- return succeed$3(outcome.kind === "rest" ? outcome.rest.name : rest.def.initial);
64960
+ if (outcome.kind !== "rest") return succeed$3(rest.def.initial);
64961
+ const reserved = initialStepRefusal(rest.run.workflow, outcome.rest.name, rest.def.initial);
64962
+ return succeed$3(reserved === void 0 ? outcome.rest.name : reserved);
64803
64963
  };
64964
+ /**
64965
+ * A step commit entering the default's first step is what ends an episode, so
64966
+ * a named workflow landing there would read back as finished. Its opening
64967
+ * commit may name that step: a bare opening is never read as a boundary.
64968
+ */
64969
+ const initialStepRefusal = (workflow, step, initial) => workflow !== void 0 && step === initial ? {
64970
+ kind: "refusal",
64971
+ message: `gtd: workflow "${workflow}" reaches step "${step}", the default workflow's first step — that is where a finished process waits, so a started process must never land there; rename the step`
64972
+ } : void 0;
64804
64973
  const landingTo = (rest, to) => {
64805
64974
  if (rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state) return {
64806
64975
  kind: "noop",
@@ -64862,7 +65031,8 @@ const snapshotFromRest = (rest, verdicts) => gen(function* () {
64862
65031
  const summaryFor = (run) => gen(function* () {
64863
65032
  const config = yield* (yield* ConfigService).load;
64864
65033
  const host = yield* Host;
64865
- const summary = config.workflow.summary;
65034
+ const resolved = yield* workflowOf(config, run);
65035
+ const summary = resolved.def.summary;
64866
65036
  if (summary === void 0 || run.trace.length === 0) return void 0;
64867
65037
  const entryCommit = run.trace[0].hash;
64868
65038
  return summary({
@@ -64875,7 +65045,7 @@ const summaryFor = (run) => gen(function* () {
64875
65045
  })),
64876
65046
  processCost: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
64877
65047
  processCostByModel: costByModel(run.costEntries),
64878
- ...settingsFor(config, run.pinnedVars, host.env)
65048
+ ...settingsFor(config, resolved, run.pinnedVars, host.env)
64879
65049
  });
64880
65050
  });
64881
65051
  //#endregion
@@ -76754,22 +76924,15 @@ f="$(gtd next --json=file)"
76754
76924
  f="\${f:-.gtd/TODO.md}"
76755
76925
  mkdir -p "$(dirname "$f")"
76756
76926
  exec "\${EDITOR:-vi}" "$f"`;
76757
- const REVIEW_COMMAND = `#!/usr/bin/env sh
76927
+ const DOOR_COMMAND = `#!/usr/bin/env sh
76758
76928
  set -eu
76759
76929
  GTD_BUILD=~/.local/bin/gtd-build
76760
76930
  if [ -z "\${1:-}" ]; then
76761
- echo "usage: gtd-review <commitish>" >&2
76931
+ echo "usage: gtd-door <name> [args...]" >&2
76762
76932
  exit 2
76763
76933
  fi
76764
76934
  cd "$(git rev-parse --show-toplevel)"
76765
- script="$(gtd --entry review-gate.check --var reviewBase="$1")"
76766
- sh -c "$script"
76767
- exec "$GTD_BUILD"`;
76768
- const FIX_COMMAND = `#!/usr/bin/env sh
76769
- set -eu
76770
- GTD_BUILD=~/.local/bin/gtd-build
76771
- cd "$(git rev-parse --show-toplevel)"
76772
- script="$(gtd --entry fix-precheck)"
76935
+ script="$(gtd door "$@")"
76773
76936
  sh -c "$script"
76774
76937
  exec "$GTD_BUILD"`;
76775
76938
  const HEADER = () => `gtd ${GTD_VERSION$1} — 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`;
@@ -76850,7 +77013,8 @@ exits 0, so read it in a shell variable and guard on emptiness
76850
77013
  adds the self-validation instruction at a validatable \`prompt\` rest, and a
76851
77014
  status-summary header at every OTHER kind; \`content\` itself never carries
76852
77015
  either), \`idle\` (\`true\` iff the resolved rest is the workflow's initial
76853
- state with a clean tree), \`log\`, \`state\`, \`actor\`, \`changes\` (which
77016
+ state with a clean tree), \`initial\` (\`true\` iff no process is underway —
77017
+ the default workflow's first step, first visit — whatever the tree holds), \`log\`, \`state\`, \`actor\`, \`changes\` (which
76854
77018
  declared \`on\` pattern, if any, each pending change matches), \`next\` (the
76855
77019
  first declared \`on\` edge that would fire right now — \`{action?, pattern,
76856
77020
  target}\` — or \`null\` on no match). Only at \`kind: "prompt"\` (the DISPATCH
@@ -76944,7 +77108,7 @@ const DRIVER_OBLIGATIONS = `
76944
77108
  \`--json=judge\`, pipes that rendered \`{ state, questions }\` document to a
76945
77109
  judge model, and pipes ITS verdict (one \`{ id, answer, p }\` per pending
76946
77110
  question, as JSON on stdin) to \`gtd judge answer\` — the FIFTH command
76947
- that changes anything, alongside \`gtd land\`/\`gtd --entry\`/\`gtd
77111
+ that changes anything, alongside \`gtd land\`/\`gtd --workflow\`/\`gtd
76948
77112
  abandon\`/\`gtd restore\`. It follows the exact same required-half /
76949
77113
  optional-half script contract as \`gtd land\` in obligation 8 above:
76950
77114
  read \`--json=script\` (and, if you want them, \`--json=settled\`/
@@ -76986,10 +77150,10 @@ logic beyond "ask gtd again".
76986
77150
  const commandSuite = () => `
76987
77151
  ## Building the user's command suite: interview first, then adapt
76988
77152
 
76989
- The obligations above are the contract; the four command bodies below are one
77153
+ The obligations above are the contract; the three command bodies below are one
76990
77154
  WORKED EXAMPLE of it. Do not copy them blindly, and do not guess the user's
76991
77155
  setup — INVESTIGATE first, then INTERVIEW, then build a suite shaped by both.
76992
- Hold ONE conversation, not four: the setup steps below come first, because a
77156
+ Hold ONE conversation, not three: the setup steps below come first, because a
76993
77157
  repo that is not set up yet cannot be driven, and the suite-shape questions
76994
77158
  follow in the same numbered list.
76995
77159
 
@@ -77045,8 +77209,8 @@ follow in the same numbered list.
77045
77209
  wrapped in \`# gtd-install: model exports\` / \`# gtd-install: end\`
77046
77210
  markers — NOT into \`.gtdrc\`. They stay per machine and are never
77047
77211
  committed, so one person's model choice binds nobody else on the repo.
77048
- Two consequences follow, both worth stating to the user: \`gtd-review\`
77049
- and \`gtd-fix\` inherit these exports only because they \`exec\`
77212
+ Two consequences follow, both worth stating to the user: \`gtd-door\`
77213
+ inherits these exports only because it \`exec\`s
77050
77214
  \`gtd-build\` — anything that drives beats without going through
77051
77215
  \`gtd-build\` sees the raw \`smart\`/\`base\` hints and fails; and \`GTD_*\` is
77052
77216
  the highest-precedence config layer, so these exports silently win over
@@ -77061,11 +77225,11 @@ follow in the same numbered list.
77061
77225
  obligations directly. Pick the runtime to match: bash, their language of
77062
77226
  choice, anything that reads \`--json=<path>\` and spawns subprocesses.
77063
77227
  9. **Where should the suite live, which editor should \`gtd-edit\` spawn, and
77064
- does anything need renaming?** Suggest \`~/.local/bin\` for all four —
77065
- \`gtd-build\`, \`gtd-edit\`, \`gtd-review\`, \`gtd-fix\` — and \`$EDITOR\` (never
77228
+ does anything need renaming?** Suggest \`~/.local/bin\` for all three —
77229
+ \`gtd-build\`, \`gtd-edit\`, \`gtd-door\` — and \`$EDITOR\` (never
77066
77230
  a hardcoded editor) for the one that opens files. The interview can
77067
- rename any of the four; whatever name is chosen for \`gtd-build\` is the
77068
- RESOLVED path baked into \`GTD_BUILD\` in the other two bodies below —
77231
+ rename any of the three; whatever name is chosen for \`gtd-build\` is the
77232
+ RESOLVED path baked into \`GTD_BUILD\` in the \`gtd-door\` body below —
77069
77233
  never the literal string \`gtd-build\`.
77070
77234
 
77071
77235
  Then build it, and verify safely before the first real drive:
@@ -77074,9 +77238,9 @@ check your kind dispatch against the table above, call them as often as you
77074
77238
  like. Nothing happens until you run an emitted script.
77075
77239
 
77076
77240
  The reference rendering in sh (no \`jq\`, no JSON parser at all — each
77077
- \`--json=<path>\` call prints its one value directly). Two of the four bodies
77078
- below share one convention: \`GTD_BUILD\` is set once, at the top, to the
77079
- suite's resolved \`gtd-build\` path.
77241
+ \`--json=<path>\` call prints its one value directly). \`gtd-door\` takes the
77242
+ \`gtd-build\` path from one convention: \`GTD_BUILD\` is set once, at the top, to
77243
+ the suite's resolved \`gtd-build\` path.
77080
77244
 
77081
77245
  ### \`gtd-build\` — the driver loop
77082
77246
 
@@ -77114,41 +77278,33 @@ are ready. Default path: \`~/.local/bin/gtd-edit\`.
77114
77278
  ${EDIT_COMMAND}
77115
77279
  \`\`\`
77116
77280
 
77117
- ### \`gtd-review <commitish>\` — start a review round and drive it
77118
-
77119
- Runs \`gtd --entry review-gate.check --var reviewBase=<commitish>\`, captured
77120
- by command substitution — never a pipe: a pipeline reports only its LAST
77121
- command's exit status, so \`gtd --entry ... | sh\` under \`set -e\` would sail
77122
- past a refusal and \`exec\` a loop over a process that was never started. It
77123
- runs the captured script, then \`exec\`s the suite's RESOLVED \`gtd-build\`
77124
- path — never the literal string \`gtd-build\`. This is one invocation that
77125
- carries the review all the way to its next human gate, not just a starter:
77126
- \`gtd-review\` on a RED baseline hands off to \`gtd-build\`, which halts at the
77127
- blocked gate and prints it, rather than starting the review. Refuses with
77128
- \`usage: gtd-review <commitish>\` on stderr and exit \`2\` when the commitish is
77129
- missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-review\`.
77130
-
77131
- \`\`\`bash
77132
- ${REVIEW_COMMAND}
77133
- \`\`\`
77281
+ ### \`gtd-door <name> [args...]\` — start a process through a door and drive it
77134
77282
 
77135
- ### \`gtd-fix\` — enter the fix process and drive it
77136
-
77137
- Runs \`gtd --entry fix-precheck\`, captured the same way, then \`exec\`s the
77138
- suite's RESOLVED \`gtd-build\` path. \`gtd-fix\` on a GREEN suite is a no-op
77283
+ Runs \`gtd door <name> [args...]\`, captured by command substitution — never a
77284
+ pipe: a pipeline reports only its LAST command's exit status, so
77285
+ \`gtd door ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
77286
+ loop over a process that was never started. It runs the captured script, then
77287
+ \`exec\`s the suite's RESOLVED \`gtd-build\` path — never the literal string
77288
+ \`gtd-build\`. This is one invocation that carries the process all the way to
77289
+ its next human gate, not just a starter: \`gtd-door review\` on a RED baseline
77290
+ hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
77291
+ rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
77139
77292
  straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
77140
- Default path: \`~/.local/bin/gtd-fix\`.
77293
+ \`gtd doors\` lists the doors a repository offers (the bundled \`fix\` and
77294
+ \`review [base]\`, plus any its config declares). Refuses with
77295
+ \`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
77296
+ missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
77141
77297
 
77142
77298
  \`\`\`bash
77143
- ${FIX_COMMAND}
77299
+ ${DOOR_COMMAND}
77144
77300
  \`\`\`
77145
77301
  ${REINSTALL}`;
77146
77302
  const REINSTALL = `
77147
77303
  ## Re-installing: detect and adapt, don't blindly overwrite
77148
77304
 
77149
77305
  A second install on a machine that already has a suite is not a fresh
77150
- install — read each of the four suite paths before writing anything
77151
- (\`gtd-build\`, \`gtd-edit\`, \`gtd-review\`, \`gtd-fix\`), and branch per path:
77306
+ install — read each of the three suite paths before writing anything
77307
+ (\`gtd-build\`, \`gtd-edit\`, \`gtd-door\`), and branch per path:
77152
77308
 
77153
77309
  - Absent — install it, as in a fresh install.
77154
77310
  - Present and content-equal to what this gtd version would emit — say so,
@@ -77170,10 +77326,16 @@ markers is unchanged, and re-asks nothing — the resolved model names are
77170
77326
  per machine, so without this exemption every single re-install would
77171
77327
  report drift on the one command everybody has.
77172
77328
 
77173
- This check scopes to exactly the four suite paths above. \`gtd-loop\` is
77329
+ This check scopes to exactly the three suite paths above, plus the superseded
77330
+ pair below. \`gtd-loop\` is
77174
77331
  outside it entirely — never read, never diffed, never touched by it in any
77175
77332
  way — an existing \`gtd-loop\` survives untouched beside the new
77176
77333
  \`gtd-build\`, and cleaning it up is the human's own call.
77334
+
77335
+ \`gtd-fix\` and \`gtd-review\` are superseded by \`gtd-door\` (\`gtd-door fix\`,
77336
+ \`gtd-door review [base]\`). When either is still present beside the suite,
77337
+ name it to the user as superseded and ask before removing it — never remove
77338
+ one unasked.
77177
77339
  `;
77178
77340
  const EDITOR_INTEGRATION = `
77179
77341
  ## Editor integration: offer it, don't bury it
@@ -77221,7 +77383,7 @@ const PREREQUISITES = `
77221
77383
  ## Prerequisites and portability
77222
77384
 
77223
77385
  - A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
77224
- scripts (\`gtd land --json=script\`, \`gtd --entry <state>\`, \`gtd abandon\`,
77386
+ scripts (\`gtd land --json=script\`, \`gtd --workflow <name>\`, \`gtd abandon\`,
77225
77387
  \`gtd restore\`, \`gtd judge answer --json=script\`) are POSIX sh; captured,
77226
77388
  then piped into it (see obligation 8 above). Reading
77227
77389
  \`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own output needs
@@ -77359,7 +77521,7 @@ const normalizeScriptNewline = (script) => script.length === 0 ? script : `${scr
77359
77521
  /**
77360
77522
  * `combinedScript`, narrowed to accept only an already-guarded
77361
77523
  * `RunnableScript` for `required` — the one boundary `ScriptSurface`'s brand
77362
- * exists to protect: `gtd land`/`gtd --entry`'s emitted script. Every OTHER
77524
+ * exists to protect: `gtd land`/`gtd --workflow`'s emitted script. Every OTHER
77363
77525
  * `combinedScript` call (`abandon`/`restore`/steering validate, below) builds
77364
77526
  * from `emitScripts` directly and keeps the unbranded signature — those
77365
77527
  * paths land no guard-gated git write for a brand to protect.
@@ -77522,29 +77684,77 @@ const runLandCommand = (opts, json, out) => gen(function* () {
77522
77684
  else out.write(renderLandPlain(built));
77523
77685
  });
77524
77686
  /**
77525
- * Starts a brand new process at `<state>`, always as `"human"`. Captures
77526
- * whatever the working tree carries at entry, like an ordinary land, rather
77527
- * than demanding a clean tree.
77687
+ * Starts a brand new process on `<workflow>`, always as `"human"`. Captures
77688
+ * whatever the working tree carries at the start, like an ordinary land,
77689
+ * rather than demanding a clean tree.
77528
77690
  *
77529
- * Refused when a process is already underway, the flow opens none for
77530
- * `<state>`, a `--var` name is undeclared, or `reviewBase:` renders to
77531
- * something that is not an ancestor of (and differs from) HEAD.
77691
+ * An unknown name is a usage error. Refused when a process is already
77692
+ * underway, the workflow reaches no step, a `--var` name is undeclared, or
77693
+ * `base` renders to something that is not an ancestor of (and differs from)
77694
+ * HEAD.
77532
77695
  */
77533
- const runEntryCommand = (actor, entryState, varOverrides, out, commandLabel) => gen(function* () {
77696
+ const runStartCommand = (actor, workflow, varOverrides, out, commandLabel) => gen(function* () {
77697
+ const config = yield* (yield* ConfigService).load;
77698
+ const resolved = config.workflowNamed(workflow);
77699
+ if (resolved === void 0) return yield* fail$7(new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — startable: ${config.workflowNames.join(", ")}`));
77534
77700
  const rest = yield* restAt(void 0, varOverrides);
77535
- const plan = yield* planEntry({
77536
- def: rest.def,
77701
+ const started = yield* startRefusal(rest, resolved, varOverrides);
77702
+ const plan = yield* planStart({
77537
77703
  state: rest.state,
77538
- idle: noProcessUnderway(rest),
77539
- entryRefusal: yield* entryRefusal(rest, entryState, varOverrides)
77704
+ idle: noProcessUnderway(rest)
77540
77705
  }, actor, {
77541
- state: entryState,
77706
+ workflow: resolved,
77707
+ first: started,
77542
77708
  commandLabel,
77543
77709
  vars: varOverrides
77544
77710
  });
77545
77711
  if (plan.kind === "refusal") return yield* fail$7(new Error(plan.message));
77546
77712
  out.write(landingScript(ScriptSurface.render(plan.steps)));
77547
77713
  });
77714
+ const doorSynopsis = (name, door) => [name, ...(door.args ?? []).map((arg) => arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)].join(" ");
77715
+ /**
77716
+ * `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
77717
+ * process settings its `vars` derive from the positional args. A wrong name or
77718
+ * arity is a usage error naming what would have worked.
77719
+ */
77720
+ const runDoorCommand = (name, args, out) => gen(function* () {
77721
+ const config = yield* (yield* ConfigService).load;
77722
+ const door = config.doors.get(name);
77723
+ if (door === void 0) {
77724
+ const known = [...config.doors];
77725
+ return yield* fail$7(new GtdUsageError(`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`));
77726
+ }
77727
+ const declared = door.args ?? [];
77728
+ const required = declared.filter((arg) => arg.optional !== true).length;
77729
+ if (args.length < required || args.length > declared.length) return yield* fail$7(new GtdUsageError(`gtd door ${name}: ${args.length < required ? "missing" : "too many"} arguments — usage: gtd door ${doorSynopsis(name, door)}`));
77730
+ const named = {};
77731
+ declared.forEach((arg, i) => {
77732
+ named[arg.name] = args[i];
77733
+ });
77734
+ const vars = yield* try_({
77735
+ try: () => door.vars?.(named) ?? {},
77736
+ catch: (e) => /* @__PURE__ */ new Error(`gtd door ${name}: ${e instanceof Error ? e.message : String(e)}`)
77737
+ });
77738
+ yield* runStartCommand("human", door.workflow, { ...vars }, out, `gtd door ${name}`);
77739
+ });
77740
+ /** `gtd doors`: every door and its args, sorted by name — plain lines, or one JSON array. */
77741
+ const runDoorsCommand = (json, out) => gen(function* () {
77742
+ const doors = [...(yield* (yield* ConfigService).load).doors];
77743
+ if (json.kind === "off") {
77744
+ out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""));
77745
+ return;
77746
+ }
77747
+ const document = doors.map(([n, d]) => ({
77748
+ name: n,
77749
+ workflow: d.workflow,
77750
+ args: (d.args ?? []).map((arg) => ({
77751
+ name: arg.name,
77752
+ optional: arg.optional === true
77753
+ }))
77754
+ }));
77755
+ if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
77756
+ else yield* writeSelection(out, document, json.path);
77757
+ });
77548
77758
  /**
77549
77759
  * Ends the process underway without completing it. Nothing is discarded: a
77550
77760
  * `git reset --mixed` to the process's start commit leaves every turn
@@ -77801,6 +78011,8 @@ const gatherBeatDocument = (rest, rendered) => gen(function* () {
77801
78011
  }), statusOf({
77802
78012
  rendered,
77803
78013
  idle: restIsIdle(rest),
78014
+ initial: noProcessUnderway(rest),
78015
+ workflow: rest.workflowName,
77804
78016
  log,
77805
78017
  changes: computeStatusChanges(rest.changes),
77806
78018
  next: yield* computeNextMatch(rest),
@@ -77887,7 +78099,9 @@ const dispatchVoidCommand = (command, json, out) => {
77887
78099
  ...command.cost !== void 0 ? { cost: command.cost } : {},
77888
78100
  ...command.model !== void 0 ? { model: command.model } : {}
77889
78101
  }, json, out);
77890
- case "entry": return runEntryCommand(command.actor, command.state, command.vars, out, command.label);
78102
+ case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
78103
+ case "door": return runDoorCommand(command.name, command.args, out);
78104
+ case "doors": return runDoorsCommand(json, out);
77891
78105
  case "abandon": return runAbandonCommand(out);
77892
78106
  case "restore": return runRestoreCommand(out);
77893
78107
  case "next": return runNextCommand(json, out);
@@ -77951,12 +78165,12 @@ const FLAGS = [
77951
78165
  name: "--json",
77952
78166
  arity: "optional",
77953
78167
  repeatable: false,
77954
- scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer",
78168
+ scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors",
77955
78169
  decode: ([raw]) => right(raw ?? ""),
77956
- scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
78170
+ scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
77957
78171
  valueHint: "<path>",
77958
78172
  help: [
77959
- "(gtd next/gtd land/gtd judge/gtd judge answer only) output",
78173
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors only) output",
77960
78174
  "structured JSON. Bare --json prints the whole document;",
77961
78175
  "--json=<path> (a dotted key path into that document, e.g.",
77962
78176
  "kind, content, session.id) prints just that value: a",
@@ -78055,7 +78269,7 @@ const FLAGS = [
78055
78269
  repeatable: false,
78056
78270
  scope: (kind) => kind === "land",
78057
78271
  decode: ([raw]) => nonNegativeNumber(raw ?? "", "--cost"),
78058
- scopeError: "gtd: --cost is only valid for `gtd land` — an entry is not a metered agent turn",
78272
+ scopeError: "gtd: --cost is only valid for `gtd land` — a start is not a metered agent turn",
78059
78273
  valueHint: "<n>",
78060
78274
  help: ["(gtd land only) record the invocation's token cost"]
78061
78275
  },
@@ -78074,24 +78288,20 @@ const FLAGS = [
78074
78288
  ]
78075
78289
  },
78076
78290
  {
78077
- name: "--entry",
78291
+ name: "--workflow",
78078
78292
  arity: 1,
78079
78293
  repeatable: false,
78080
- scope: (kind) => kind === "entry",
78294
+ scope: (kind) => kind === "start",
78081
78295
  decode: ([raw]) => right(raw ?? ""),
78082
- scopeError: "gtd: --entry is only valid with no other command — use the bare `gtd --entry <state>` form; landing and entering are different verbs",
78083
- valueHint: "<state>",
78084
- help: [
78085
- "(with no command at all) start a brand new process,",
78086
- "handing <state> to the workflow as its entry —",
78087
- "authenticated as human"
78088
- ]
78296
+ scopeError: "gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` form; landing and starting are different verbs",
78297
+ valueHint: "<name>",
78298
+ help: ["(with no command at all) start a brand new process on", "the named workflow — authenticated as human"]
78089
78299
  },
78090
78300
  {
78091
78301
  name: "--var",
78092
78302
  arity: 1,
78093
78303
  repeatable: true,
78094
- scope: (kind) => kind === "entry",
78304
+ scope: (kind) => kind === "start",
78095
78305
  decode: (raws) => {
78096
78306
  const vars = {};
78097
78307
  const seen = /* @__PURE__ */ new Set();
@@ -78108,10 +78318,10 @@ const FLAGS = [
78108
78318
  }
78109
78319
  return right(vars);
78110
78320
  },
78111
- scopeError: "gtd: --var requires --entry",
78321
+ scopeError: "gtd: --var requires --workflow",
78112
78322
  valueHint: "<name>=<value>",
78113
78323
  help: [
78114
- "(with --entry; repeatable) pin a process setting for the",
78324
+ "(with --workflow; repeatable) pin a process setting for the",
78115
78325
  "new process; the name must already be declared by the",
78116
78326
  "workflow's defaults or the .gtdrc vars: (environment",
78117
78327
  "settings are not pinnable)"
@@ -78394,6 +78604,34 @@ const COMMAND_ROWS = [
78394
78604
  "Refuses (exit 1) when no process is underway."
78395
78605
  ]
78396
78606
  },
78607
+ {
78608
+ token: "door",
78609
+ kind: "door",
78610
+ arity: {
78611
+ name: "name",
78612
+ variadic: "args"
78613
+ },
78614
+ details: [
78615
+ "Start a process through a named shortcut, mapping the",
78616
+ "positional args to that workflow's process settings — the",
78617
+ "same start `gtd --workflow` makes, printed the same way.",
78618
+ "Doors come from the config's `doors` export; the bundled",
78619
+ "ones are `fix` and `review [base]`. An unknown door, or too",
78620
+ "few or too many args, is a usage error (exit 2) naming the",
78621
+ "doors or the door's synopsis"
78622
+ ]
78623
+ },
78624
+ {
78625
+ token: "doors",
78626
+ kind: "doors",
78627
+ arity: "none",
78628
+ details: [
78629
+ "List every door, sorted: one `name [args] → workflow` line",
78630
+ "each. --json prints one JSON array of { name, workflow,",
78631
+ "args: [{ name, optional }] } instead; --json=<path> reads one",
78632
+ "value off it. Writes nothing"
78633
+ ]
78634
+ },
78397
78635
  {
78398
78636
  token: "judge",
78399
78637
  kind: "judge",
@@ -78449,23 +78687,23 @@ const renderBlock = (header, lines) => {
78449
78687
  const headerCell = ` ${header}`;
78450
78688
  return (headerCell.length < COLUMN ? `${headerCell.padEnd(COLUMN)}${first ?? ""}\n` : `${headerCell}\n${" ".repeat(COLUMN)}${first ?? ""}\n`) + rest.map((l) => `${" ".repeat(COLUMN)}${l}\n`).join("");
78451
78689
  };
78452
- const ENTRY_SHORT_FORM = renderBlock("(no command) --entry <state>", ["Starts a new process authenticated as human, e.g.", "'gtd --entry <state>'"]);
78690
+ const START_SHORT_FORM = renderBlock("(no command) --workflow <name>", ["Starts a new process authenticated as human, e.g.", "'gtd --workflow <name>'"]);
78453
78691
  const VERSION_BLOCK = renderBlock("version", ["Print version and exit"]);
78454
78692
  const HELP_BLOCK = renderBlock("help", ["Print this help and exit"]);
78455
- const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--entry", "--var"]);
78693
+ const SPACE_FORM_FLAGS = /* @__PURE__ */ new Set(["--workflow", "--var"]);
78456
78694
  const flagHeader = (row) => {
78457
78695
  if (row.valueHint === "") return row.name;
78458
78696
  return SPACE_FORM_FLAGS.has(row.name) ? `${row.name} ${row.valueHint}` : `${row.name}=${row.valueHint}`;
78459
78697
  };
78460
78698
  const renderHelp = () => {
78461
78699
  const commandBlocks = COMMAND_ROWS.map((row) => {
78462
- const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : `${row.token} <${row.arity.name}>`;
78700
+ 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}...]` : `${row.token} <${row.arity.name}>`;
78463
78701
  return renderBlock(header, row.details);
78464
78702
  });
78465
78703
  return `Usage: gtd [command] [options]\n\nCommands:\n${[
78466
78704
  commandBlocks[0],
78467
78705
  commandBlocks[1],
78468
- ENTRY_SHORT_FORM,
78706
+ START_SHORT_FORM,
78469
78707
  ...commandBlocks.slice(2),
78470
78708
  VERSION_BLOCK,
78471
78709
  HELP_BLOCK
@@ -78570,14 +78808,14 @@ const conflictViolation = (present) => {
78570
78808
  };
78571
78809
  const arityError = (cmd, rest, arity) => {
78572
78810
  if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
78573
- if ("names" in arity) {
78574
- const [first, second] = arity.names;
78575
- if (rest.length < 2) return rest.length === 0 ? `gtd ${cmd}: missing ${first} and ${second} arguments` : `gtd ${cmd}: missing ${second} argument`;
78576
- if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
78577
- return;
78578
- }
78811
+ if ("names" in arity) return pairArityError(cmd, rest, arity.names);
78579
78812
  if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
78580
- if (rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
78813
+ if (!("variadic" in arity) && rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
78814
+ };
78815
+ const pairArityError = (cmd, rest, [first, second]) => {
78816
+ if (rest.length === 0) return `gtd ${cmd}: missing ${first} and ${second} arguments`;
78817
+ if (rest.length === 1) return `gtd ${cmd}: missing ${second} argument`;
78818
+ if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
78581
78819
  };
78582
78820
  /** Decode every present flag into a bag of values, or the first decode error encountered. */
78583
78821
  const decodeFlags = (byFlag) => {
@@ -78611,8 +78849,8 @@ const parseArgv = (argv) => {
78611
78849
  if (tokenized.kind === "error") return usagePlan(tokenized.message, tokenized.jsonSeen);
78612
78850
  const { positionals, byFlag, present, jsonSeen } = tokenized;
78613
78851
  const first = positionals[0];
78614
- const entryRaw = byFlag.get("--entry")?.[0];
78615
- const entryPresent = entryRaw !== void 0;
78852
+ const workflowRaw = byFlag.get("--workflow")?.[0];
78853
+ const workflowPresent = workflowRaw !== void 0;
78616
78854
  if (first === "version") return {
78617
78855
  kind: "output",
78618
78856
  stdout: `${GTD_VERSION}\n`
@@ -78623,17 +78861,17 @@ const parseArgv = (argv) => {
78623
78861
  };
78624
78862
  const row = first === void 0 ? void 0 : commandByToken(first);
78625
78863
  const judgeAnswer = row?.kind === "judge" && positionals[1] === "answer";
78626
- const selectsEntry = entryPresent && first === void 0;
78864
+ const selectsStart = workflowPresent && first === void 0;
78627
78865
  const judgeRun = row?.kind === "judge" && positionals[1] === "run";
78628
- const kind = selectsEntry ? "entry" : judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row?.kind;
78629
- if (row === void 0 && !selectsEntry) {
78866
+ const kind = selectsStart ? "start" : judgeAnswer ? "judgeAnswer" : judgeRun ? "judgeRun" : row?.kind;
78867
+ if (row === void 0 && !selectsStart) {
78630
78868
  const violation = scopeViolation(void 0, present);
78631
78869
  if (violation !== void 0) return usagePlan(violation, jsonSeen);
78632
78870
  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());
78633
78871
  return usagePlan(`unknown command '${first}'`, jsonSeen);
78634
78872
  }
78635
78873
  const restPositionals = first === void 0 ? positionals : positionals.slice(judgeAnswer || judgeRun ? 2 : 1);
78636
- if (kind === "entry" && first === void 0) {
78874
+ if (kind === "start" && first === void 0) {
78637
78875
  if (restPositionals.length > 0) return usagePlan(`gtd: too many arguments — expected none, got: ${restPositionals.join(", ")}`, jsonSeen);
78638
78876
  } else {
78639
78877
  const arityMsg = kind === "check" && restPositionals.length === 0 && present.has("--open-threads") ? void 0 : arityError(first, restPositionals, row.arity);
@@ -78660,14 +78898,14 @@ const parseArgv = (argv) => {
78660
78898
  verbose
78661
78899
  };
78662
78900
  }
78663
- if (kind === "entry") {
78664
- const label = `gtd --entry ${entryRaw}`;
78901
+ if (kind === "start") {
78902
+ const label = `gtd --workflow ${workflowRaw}`;
78665
78903
  return {
78666
78904
  kind: "command",
78667
78905
  command: {
78668
- kind: "entry",
78906
+ kind: "start",
78669
78907
  actor: "human",
78670
- state: entryRaw,
78908
+ workflow: workflowRaw,
78671
78909
  vars: bag["--var"] ?? {},
78672
78910
  label
78673
78911
  },
@@ -78675,6 +78913,16 @@ const parseArgv = (argv) => {
78675
78913
  verbose
78676
78914
  };
78677
78915
  }
78916
+ if (kind === "door") return {
78917
+ kind: "command",
78918
+ command: {
78919
+ kind: "door",
78920
+ name: restPositionals[0],
78921
+ args: restPositionals.slice(1)
78922
+ },
78923
+ json,
78924
+ verbose
78925
+ };
78678
78926
  if (kind === "ui") return {
78679
78927
  kind: "command",
78680
78928
  command: {