@pmelab/gtd 15.10.0 → 17.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.
@@ -30966,17 +30966,26 @@ const changesSince = (hash, pattern) => {
30966
30966
  const head = () => ctx().head();
30967
30967
  /** The process's diff base: the commit before it began, or the base `gtd --entry` fixed. */
30968
30968
  const start = () => ctx().start();
30969
- /** The merged workflow variables. */
30970
- const vars = new Proxy({}, {
30971
- get: (_target, key) => typeof key === "string" ? ctx().vars[key] : void 0,
30972
- has: (_target, key) => typeof key === "string" && key in ctx().vars,
30973
- ownKeys: () => Object.keys(ctx().vars),
30974
- getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && key in ctx().vars ? {
30969
+ /** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. See `FlowContext.skillsFor` for `ownSkills`. */
30970
+ const skillsFor = (localName, ownSkills) => ctx().skillsFor(localName, ownSkills);
30971
+ const settingsProxy = (read) => new Proxy({}, {
30972
+ get: (_target, key) => typeof key === "string" ? read()[key] : void 0,
30973
+ has: (_target, key) => typeof key === "string" && key in read(),
30974
+ ownKeys: () => Object.keys(read()),
30975
+ getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && key in read() ? {
30975
30976
  enumerable: true,
30976
30977
  configurable: true,
30977
- value: ctx().vars[key]
30978
+ value: read()[key]
30978
30979
  } : void 0
30979
30980
  });
30981
+ /** The process settings: pinned at process start, so flow code may branch on them. */
30982
+ const vars = settingsProxy(() => ctx().vars);
30983
+ /**
30984
+ * The environment settings: resolved live on every invocation. Flow code must
30985
+ * read them only where they cannot change the next step (step options, script
30986
+ * bodies) — replay cannot enforce this.
30987
+ */
30988
+ const env = settingsProxy(() => ctx().env);
30980
30989
  /** The top-level `## ` heading texts of markdown `text`. */
30981
30990
  const sections = (text) => ctx().sections(text);
30982
30991
  /** The top-level `## ` sections of markdown `text`, each with its own markdown. */
@@ -31160,6 +31169,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
31160
31169
  checkScript: () => checkScript,
31161
31170
  codeChanges: () => codeChanges,
31162
31171
  codeThreads: () => codeThreads,
31172
+ env: () => env,
31163
31173
  glob: () => glob,
31164
31174
  hasThreadFor: () => hasThreadFor,
31165
31175
  head: () => head,
@@ -31185,6 +31195,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
31185
31195
  scope: () => scope,
31186
31196
  sectionBodies: () => sectionBodies,
31187
31197
  sections: () => sections,
31198
+ skillsFor: () => skillsFor,
31188
31199
  start: () => start,
31189
31200
  threads: () => threads,
31190
31201
  vars: () => vars,
@@ -47611,7 +47622,7 @@ const formatCommitMessage = (spec) => {
47611
47622
  const lines = [];
47612
47623
  if (spec.step !== void 0) lines.push(`Gtd-Step: ${formatStepId(spec.step)}`);
47613
47624
  if (spec.reviewBase !== void 0) lines.push(`Gtd-Review-Base: ${spec.reviewBase}`);
47614
- for (const [name, value] of Object.entries(spec.vars ?? {})) lines.push(`Gtd-Var: ${name}=${value}`);
47625
+ 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}`);
47615
47626
  if (spec.cost !== void 0) lines.push(`Gtd-Cost: ${spec.cost.cost}${spec.cost.model !== void 0 ? ` ${spec.cost.model}` : ""}`);
47616
47627
  for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`);
47617
47628
  const subject = formatSubject(spec.actor, spec.to, spec.from);
@@ -47773,7 +47784,11 @@ const replay = async (input) => {
47773
47784
  truncated: judgeCuts.get(request) ?? []
47774
47785
  } : void 0;
47775
47786
  const judgeCuts = /* @__PURE__ */ new WeakMap();
47776
- const resolve = (request) => {
47787
+ const resolveSkills = (name, ownSkills) => {
47788
+ const resolved = input.configuredSkills?.[name] ?? ownSkills ?? input.skills?.[name];
47789
+ return resolved !== void 0 && resolved.length === 0 ? void 0 : resolved;
47790
+ };
47791
+ const resolve = (request, name) => {
47777
47792
  if (request.kind === "agent") {
47778
47793
  const persona = {};
47779
47794
  for (const s of scopes) {
@@ -47784,7 +47799,8 @@ const replay = async (input) => {
47784
47799
  ...request,
47785
47800
  options: {
47786
47801
  ...persona,
47787
- ...request.options
47802
+ ...request.options,
47803
+ skills: resolveSkills(name, request.options.skills)
47788
47804
  }
47789
47805
  };
47790
47806
  }
@@ -47803,7 +47819,7 @@ const replay = async (input) => {
47803
47819
  const name = scoped(request.name);
47804
47820
  const occurrence = (occurrences.get(name) ?? 0) + 1;
47805
47821
  occurrences.set(name, occurrence);
47806
- const resolved = resolve(request);
47822
+ const resolved = resolve(request, name);
47807
47823
  const reached = {
47808
47824
  id: {
47809
47825
  name,
@@ -47967,7 +47983,9 @@ const replay = async (input) => {
47967
47983
  }));
47968
47984
  }),
47969
47985
  vars: input.vars,
47986
+ env: input.env,
47970
47987
  start: () => input.start,
47988
+ skillsFor: (localName, ownSkills) => resolveSkills(scoped(localName), ownSkills) ?? [],
47971
47989
  head: () => {
47972
47990
  const rest = commits.slice(cursor);
47973
47991
  return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
@@ -48241,21 +48259,18 @@ const withSkills = (skills, prompt) => {
48241
48259
  if (skills === void 0 || skills.trim() === "") return prompt;
48242
48260
  return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`;
48243
48261
  };
48244
- /** `skills:` split on `,`, each name trimmed, empty entries dropped — `[]` for `undefined`, `""`, or blank. */
48245
- const splitSkills = (raw) => (raw ?? "").split(",").map((name) => name.trim()).filter((name) => name.length > 0);
48246
48262
  /**
48247
- * An `agent()` step declaring `skills`: the raw, comma-separated var feeds
48248
- * both the `skills` option (the wire's `skills` key, split) and, re-joined
48249
- * with `", "`, `withSkills`'s preamble — one declaration, so the two can
48250
- * never drift apart.
48263
+ * An `agent()` step whose preamble names its resolved skills: `skillsFor`
48264
+ * resolves `name` (scoped from here, same as `agent()`'s own resolver) for
48265
+ * the preamble prose, passing `options.skills` through as `ownSkills` — the
48266
+ * same precedence tier `agent()`'s own wire resolver gives a call's own
48267
+ * `skills` option, so a step that declares one (`reviewQuality`'s per-turn
48268
+ * lens) gets it in the preamble too, not just on the wire, whenever no
48269
+ * bundled or configured entry outranks it. Both reads share one resolver
48270
+ * (`Replay.ts`'s `resolveSkills`), so the preamble and the wire can't drift
48271
+ * apart.
48251
48272
  */
48252
- const agentWithSkills = (name, skills, prompt, options = {}) => {
48253
- const list = splitSkills(skills);
48254
- return agent(name, withSkills(list.join(", "), prompt), {
48255
- ...options,
48256
- skills: list
48257
- });
48258
- };
48273
+ const agentWithSkills = (name, prompt, options = {}) => agent(name, withSkills(skillsFor(name, options.skills).join(", "), prompt), options);
48259
48274
  const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
48260
48275
  const idleMessage = () => `No active gtd process.
48261
48276
 
@@ -48381,7 +48396,7 @@ ${questionBarReturn}
48381
48396
  never re-open one. A genuinely open PRODUCT point may still
48382
48397
  raise a fresh \`## Open Questions\` entry: the product gate sits
48383
48398
  on this path exactly as on the first lap
48384
- - Before grouping anything, run \`${vars.testCommand}\`
48399
+ - Before grouping anything, run \`${env.testCommand}\`
48385
48400
  yourself — a loop-back runs no green-baseline gate, so the
48386
48401
  tree may already be red (reverting the edit can undo a fix it
48387
48402
  made). If red, make that breakage the first concern, ahead of
@@ -48899,10 +48914,13 @@ every commit is machine-authored.
48899
48914
  high-level architectural changes — never which files changed; \`git
48900
48915
  diff --stat\` is for that, not this message
48901
48916
  - If the range removes or renames a documented config key, CLI flag, or
48902
- workflow state — a public surface, not an internal one — add a literal
48903
- \`BREAKING CHANGE:\` footer naming what disappears. A \`!\` on the type
48904
- prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's own
48905
- commit-analyzer has missed that marker before, cutting no release
48917
+ workflow state, OR makes a previously-accepted config value now fail to
48918
+ load (a stricter validation, an escape a value must now spell differently)
48919
+ — a public surface, not an internal one, either way — add a literal
48920
+ \`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
48921
+ the exact escape or rewrite a committed \`.gtdrc\` needs. A \`!\` on the
48922
+ type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
48923
+ own commit-analyzer has missed that marker before, cutting no release
48906
48924
  until a follow-up commit carried the literal footer instead
48907
48925
 
48908
48926
  The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
@@ -48923,24 +48941,24 @@ const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
48923
48941
  const REVIEW = ".gtd/REVIEW.md";
48924
48942
  const QUALITY = ".gtd/QUALITY.md";
48925
48943
  const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md";
48926
- const planner = () => vars.plannerModel ?? "";
48927
- const coder = () => vars.coderModel ?? "";
48944
+ const planner = () => env.plannerModel ?? "";
48945
+ const coder = () => env.coderModel ?? "";
48928
48946
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
48929
- const triage$1 = (base) => agentWithSkills("triage", vars.triageSkills, designTriagePrompt(base), {
48947
+ const triage$1 = (base) => agentWithSkills("triage", designTriagePrompt(base), {
48930
48948
  label: "Triaging the change",
48931
48949
  file: REQUIREMENTS,
48932
48950
  mode: "qa",
48933
48951
  model: planner(),
48934
48952
  system: designSystem()
48935
48953
  });
48936
- const author = () => agentWithSkills("author", vars.architectureSkills, architectureAuthorPrompt(), {
48954
+ const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
48937
48955
  label: "Refining the technical plan",
48938
48956
  file: ARCHITECTURE,
48939
48957
  mode: "qa",
48940
48958
  model: planner(),
48941
48959
  system: architectSystem()
48942
48960
  });
48943
- const decompose = () => agentWithSkills("decompose", vars.decomposeSkills, architectureDecomposePrompt(), {
48961
+ const decompose = () => agentWithSkills("decompose", architectureDecomposePrompt(), {
48944
48962
  label: "Decomposing into packages",
48945
48963
  model: planner(),
48946
48964
  system: architectSystem()
@@ -48959,37 +48977,37 @@ const answerTechnicalQuestions = () => human("gate.answer", {
48959
48977
  mode: "qa",
48960
48978
  acceptClean: true
48961
48979
  });
48962
- const build = (pkg) => agentWithSkills("building", vars.buildSkills, packagesItemBuildingPrompt(pkg), {
48980
+ const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
48963
48981
  label: "Building",
48964
48982
  model: coder(),
48965
48983
  system: builderSystem()
48966
48984
  });
48967
- const fixSuite = () => agentWithSkills("fix-suite", vars.fixSkills, packagesItemFixSuitePrompt(), {
48985
+ const fixSuite = () => agentWithSkills("fix-suite", packagesItemFixSuitePrompt(), {
48968
48986
  label: "Fixing the check",
48969
48987
  file: FEEDBACK,
48970
48988
  model: coder(),
48971
48989
  system: builderSystem()
48972
48990
  });
48973
- const fixSpec = (pkg) => agentWithSkills("fix-spec", vars.reviewFixSkills, packagesItemFixSpecPrompt(pkg), {
48991
+ const fixSpec = (pkg) => agentWithSkills("fix-spec", packagesItemFixSpecPrompt(pkg), {
48974
48992
  label: "Fixing review feedback",
48975
48993
  file: SPEC_FEEDBACK,
48976
48994
  model: coder(),
48977
48995
  system: builderSystem()
48978
48996
  });
48979
48997
  /** Review `pkg` against its spec, focused on the `failing` sections the pre-judge could not clear. */
48980
- const reviewPackage = (pkg, failing = []) => agentWithSkills("spec.review", vars.specReviewSkills, packagesItemSpecReviewPrompt(pkg, failing), {
48998
+ const reviewPackage = (pkg, failing = []) => agentWithSkills("spec.review", packagesItemSpecReviewPrompt(pkg, failing), {
48981
48999
  label: "Reviewing the package",
48982
49000
  model: planner(),
48983
49001
  system: specReviewerSystem(),
48984
49002
  allowEmpty: true
48985
49003
  });
48986
- const fix = () => agentWithSkills("fix", vars.fixSkills, buildFixPrompt(), {
49004
+ const fix = () => agentWithSkills("fix", buildFixPrompt(), {
48987
49005
  label: "Fixing the check",
48988
49006
  file: FEEDBACK,
48989
49007
  model: coder(),
48990
49008
  system: finisherSystem()
48991
49009
  });
48992
- const describeEscalation = () => agentWithSkills("health.describe", vars.escalateSkills, healthDescribePrompt(), {
49010
+ const describeEscalation = () => agentWithSkills("health.describe", healthDescribePrompt(), {
48993
49011
  label: "Describing the escalation",
48994
49012
  file: FEEDBACK,
48995
49013
  model: coder(),
@@ -49008,15 +49026,21 @@ const escalationExhausted = () => human("health.exhausted", {
49008
49026
  file: ESCALATION,
49009
49027
  acceptClean: true
49010
49028
  });
49011
- /** One quality review, through the skill `lens`. */
49012
- const reviewQuality = (lens) => agentWithSkills("quality.reviewing", lens, buildQualityReviewingPrompt(lens), {
49029
+ /**
49030
+ * One quality review, through the skill `lens`. `lens` rides as this turn's
49031
+ * own `skills` option — a `.gtdrc` `build.quality.reviewing` entry still
49032
+ * overrides it (config beats a flow-supplied list same as any other step),
49033
+ * but absent one the lens itself is what the turn loads by default.
49034
+ */
49035
+ const reviewQuality = (lens) => agentWithSkills("quality.reviewing", buildQualityReviewingPrompt(lens), {
49013
49036
  label: "Reviewing (one quality lens)",
49014
49037
  file: QUALITY,
49015
49038
  model: planner(),
49016
49039
  system: reviewerSystem(),
49017
- allowEmpty: true
49040
+ allowEmpty: true,
49041
+ skills: [lens]
49018
49042
  });
49019
- const fixQuality = () => agentWithSkills("fix-quality", vars.reviewFixSkills, buildFixQualityPrompt(), {
49043
+ const fixQuality = () => agentWithSkills("fix-quality", buildFixQualityPrompt(), {
49020
49044
  label: "Fixing quality findings",
49021
49045
  file: QUALITY,
49022
49046
  model: coder(),
@@ -49024,7 +49048,7 @@ const fixQuality = () => agentWithSkills("fix-quality", vars.reviewFixSkills, bu
49024
49048
  allowEmpty: true
49025
49049
  });
49026
49050
  /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
49027
- const reviewing = (base, carry) => agentWithSkills("review.reviewing", vars.reviewSkills, buildReviewReviewingPrompt(base, carry), {
49051
+ const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
49028
49052
  label: "Reviewing",
49029
49053
  file: REVIEW,
49030
49054
  mode: "review",
@@ -49033,7 +49057,7 @@ const reviewing = (base, carry) => agentWithSkills("review.reviewing", vars.revi
49033
49057
  base
49034
49058
  });
49035
49059
  /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
49036
- const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", vars.reviewSkills, buildReviewAnswerQuestionsPrompt(notes), {
49060
+ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
49037
49061
  label: "Answering your questions",
49038
49062
  file: REVIEW,
49039
49063
  mode: "review",
@@ -49044,7 +49068,7 @@ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-q
49044
49068
  * Fix every `nit` note in one turn. Its name puts it in the `build.review`
49045
49069
  * conversation, which has one identity — so it runs as the reviewer, not the coder.
49046
49070
  */
49047
- const fixNits = (notes) => agentWithSkills("review.fix-nits", vars.reviewFixSkills, buildReviewFixNitsPrompt(notes), {
49071
+ const fixNits = (notes) => agentWithSkills("review.fix-nits", buildReviewFixNitsPrompt(notes), {
49048
49072
  label: "Fixing your nits",
49049
49073
  file: REVIEW,
49050
49074
  model: planner(),
@@ -49063,7 +49087,7 @@ const reviewMissing = (round) => human("review.review-missing", {
49063
49087
  label: "Nothing to review"
49064
49088
  });
49065
49089
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
49066
- const collecting = (capture) => agentWithSkills("review.collecting", vars.reviewSkills, buildReviewCollectingPrompt(capture), {
49090
+ const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
49067
49091
  label: "Collecting your feedback",
49068
49092
  file: REQUIREMENTS,
49069
49093
  mode: "qa",
@@ -49072,7 +49096,7 @@ const collecting = (capture) => agentWithSkills("review.collecting", vars.review
49072
49096
  allowEmpty: true
49073
49097
  });
49074
49098
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
49075
- const baseline = (name, label = "Checking the baseline") => check(name, vars.testCommand ?? "", {
49099
+ const baseline = (name, label = "Checking the baseline") => check(name, env.testCommand ?? "", {
49076
49100
  report: FEEDBACK,
49077
49101
  label
49078
49102
  });
@@ -49124,7 +49148,7 @@ const healthy = async (fix, options = {}) => {
49124
49148
  let fixes = options.fixesSoFar ?? 0;
49125
49149
  let previous;
49126
49150
  for (;;) {
49127
- if (await check("health.check", vars.testCommand ?? "", {
49151
+ if (await check("health.check", env.testCommand ?? "", {
49128
49152
  report: ".gtd/FEEDBACK.md",
49129
49153
  label: "Running checks",
49130
49154
  sweepOnGreen: [".gtd/ESCALATION.md"]
@@ -49528,8 +49552,8 @@ const architecturePass = async () => {
49528
49552
  };
49529
49553
  //#endregion
49530
49554
  //#region src/workflows/review.ts
49531
- /** The skills the quality lap reviews with, one turn each: the `qualityReviews` var. */
49532
- const qualityLenses = () => splitSkills(vars.qualityReviews);
49555
+ /** The lenses the quality lap reviews with, one turn each: the `qualityReviews` var, split on `,` and trimmed. Unlike a `skills:` entry, this fans out into one whole turn per entry rather than naming one step's skill list — see `build.quality.reviewing` in `./skills.ts` for the (separate) skills a lens turn itself loads. */
49556
+ const qualityLenses = () => (vars.qualityReviews ?? "").split(",").map((lens) => lens.trim()).filter((lens) => lens.length > 0);
49533
49557
  /**
49534
49558
  * One review turn per lens over the whole change, each appending what it
49535
49559
  * finds blocking to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
@@ -49751,27 +49775,46 @@ const buildTail = (fixFirst, base) => scope("build", async () => {
49751
49775
  });
49752
49776
  //#endregion
49753
49777
  //#region src/workflows/vars.ts
49778
+ /** Process settings. */
49754
49779
  const defaults = {
49755
- testCommand: "npm test",
49756
- plannerModel: "smart",
49757
- coderModel: "base",
49758
49780
  reviewBase: "",
49759
49781
  judgeBudgetBytes: "32768",
49760
49782
  judgeIdenticalMinP: "0.7",
49761
49783
  specPreJudge: "0.9",
49762
49784
  reviewNoteActionable: "0.7",
49763
49785
  architectureSkipMinP: "0.85",
49764
- triageSkills: "spec-driven-development, planning-and-task-breakdown",
49765
- architectureSkills: "api-and-interface-design, documentation-and-adrs, ponytail",
49766
- decomposeSkills: "incremental-implementation, planning-and-task-breakdown",
49767
- buildSkills: "test-driven-development, incremental-implementation",
49768
- fixSkills: "debugging-and-error-recovery",
49769
- reviewFixSkills: "incremental-implementation, code-simplification",
49770
- reviewSkills: "code-review-and-quality",
49771
- specReviewSkills: "code-review-and-quality, spec-driven-development",
49772
- escalateSkills: "debugging-and-error-recovery",
49773
49786
  qualityReviews: "owasp-security, ponytail-review, test-audit"
49774
49787
  };
49788
+ /** Environment settings. */
49789
+ const envDefaults = {
49790
+ testCommand: "npm test",
49791
+ plannerModel: "smart",
49792
+ coderModel: "base"
49793
+ };
49794
+ //#endregion
49795
+ //#region src/workflows/skills.ts
49796
+ const skills = {
49797
+ "design.triage": ["spec-driven-development", "planning-and-task-breakdown"],
49798
+ "architecture.author": [
49799
+ "api-and-interface-design",
49800
+ "documentation-and-adrs",
49801
+ "ponytail"
49802
+ ],
49803
+ "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
49804
+ "packages.item.building": ["test-driven-development", "incremental-implementation"],
49805
+ "packages.item.fix-suite": ["debugging-and-error-recovery"],
49806
+ "packages.item.fix-spec": ["incremental-implementation", "code-simplification"],
49807
+ "packages.item.spec.review": ["code-review-and-quality", "spec-driven-development"],
49808
+ "packages.item.health.describe": ["debugging-and-error-recovery"],
49809
+ "build.fix": ["debugging-and-error-recovery"],
49810
+ "build.health.describe": ["debugging-and-error-recovery"],
49811
+ "build.quality.reviewing": [],
49812
+ "build.fix-quality": ["incremental-implementation", "code-simplification"],
49813
+ "build.review.reviewing": ["code-review-and-quality"],
49814
+ "build.review.answer-review-questions": ["code-review-and-quality"],
49815
+ "build.review.fix-nits": ["incremental-implementation", "code-simplification"],
49816
+ "build.review.collecting": ["code-review-and-quality"]
49817
+ };
49775
49818
  //#endregion
49776
49819
  //#region src/workflows/unified.ts
49777
49820
  var unified_exports = /* @__PURE__ */ __exportAll({
@@ -49802,6 +49845,7 @@ var unified_exports = /* @__PURE__ */ __exportAll({
49802
49845
  defaults: () => defaults,
49803
49846
  describeEscalation: () => describeEscalation,
49804
49847
  design: () => design,
49848
+ envDefaults: () => envDefaults,
49805
49849
  escalate: () => escalate,
49806
49850
  escalation: () => escalation,
49807
49851
  escalationExhausted: () => escalationExhausted,
@@ -49827,6 +49871,7 @@ var unified_exports = /* @__PURE__ */ __exportAll({
49827
49871
  reviewPackage: () => reviewPackage,
49828
49872
  reviewQuality: () => reviewQuality,
49829
49873
  reviewing: () => reviewing,
49874
+ skills: () => skills,
49830
49875
  specReview: () => specReview,
49831
49876
  steering: () => steering,
49832
49877
  summary: () => summary,
@@ -50615,7 +50660,17 @@ var Workspace = class Workspace extends Tag("Workspace")() {
50615
50660
  /** The `vars:` shape: a flat name -> scalar map (`compileVarsMap` coerces every scalar to a string). */
50616
50661
  const varsJsonSchema = {
50617
50662
  type: "object",
50618
- description: "Flat name -> scalar map merged into the workflow's vars. Scalars are coerced to strings.",
50663
+ description: "Flat name -> scalar map merged into the workflow's process settings (defaults), pinned for the whole process at its start. Scalars are coerced to strings.",
50664
+ additionalProperties: { type: [
50665
+ "string",
50666
+ "number",
50667
+ "boolean"
50668
+ ] }
50669
+ };
50670
+ /** The `env:` shape: environment settings, read live on every invocation and never pinned to a process. */
50671
+ const envJsonSchema = {
50672
+ type: "object",
50673
+ description: "Flat name -> scalar map merged into the workflow's environment settings (envDefaults) — values that change how a step runs on this machine, like the test command or a model hint. Read fresh on every gtd call, never pinned to a process. GTD_<NAME> environment variables override these entries. Scalars are coerced to strings.",
50619
50674
  additionalProperties: { type: [
50620
50675
  "string",
50621
50676
  "number",
@@ -50643,6 +50698,15 @@ const modesJsonSchema = {
50643
50698
  }
50644
50699
  }
50645
50700
  };
50701
+ /** The `skills:` shape: full step name -> its skill list (`compileSkillsMap`). An entry REPLACES the step's bundled list wholesale, never adds to it — and the schema can validate an entry's VALUE but never its KEY, since step names come from the workflow in play, not a fixed set. */
50702
+ const skillsJsonSchema = {
50703
+ type: "object",
50704
+ description: "Flat step full-name -> skill-name array map. Each entry REPLACES the named step's bundled skill list wholesale (never merges into it); [] means no skills at all for that step, and no preamble. A key naming a step the workflow in play does not declare is a load error listing the known names — the schema itself cannot validate a key, only a value's shape.",
50705
+ additionalProperties: {
50706
+ type: "array",
50707
+ items: { type: "string" }
50708
+ }
50709
+ };
50646
50710
  /**
50647
50711
  * A real (not `Unknown`) schema, unlike `vars`/`modes`: it is a flat settings
50648
50712
  * struct with no per-mode map to compile, so its JSON Schema derives from the
@@ -50660,12 +50724,46 @@ const UiSchema = Struct({
50660
50724
  key: optional$1(String$.annotations({ description: "Path to a TLS private key file, enabling HTTPS. Requires `cert` too." })),
50661
50725
  format: optional$1(String$.annotations({ description: "Shell command run after every UI write, before it resolves ($GTD_FILE is the written file's absolute path). A non-zero exit or missing binary never reverts the write or refuses it — it's reported to the client as a notice naming the command and its exit code. Absent means no command runs at all." }))
50662
50726
  }).annotations({ description: "Settings for `gtd ui`: where it listens, and optional TLS." });
50727
+ const JudgeSchema = Struct({
50728
+ provider: optional$1(Literal("fixed", "jev", "llm").annotations({ description: "Judge provider `gtd judge run` uses when no --provider flag and no GTD_JUDGE_PROVIDER is set. Absent means auto: jev when TYPESAFE_API_KEY is set, otherwise llm." })),
50729
+ model: optional$1(String$.annotations({ description: "Model for provider llm, used when no --model flag and no GTD_JUDGE_MODEL is set. Pairing it with another provider is an error." }))
50730
+ }).annotations({ description: "Which judge `gtd judge run` uses, when no flag or GTD_JUDGE_* variable says." });
50663
50731
  const ConfigSchema = Struct({
50664
50732
  vars: optional$1(Unknown.annotations({ jsonSchema: varsJsonSchema })),
50733
+ env: optional$1(Unknown.annotations({ jsonSchema: envJsonSchema })),
50734
+ judge: optional$1(JudgeSchema),
50665
50735
  modes: optional$1(Unknown.annotations({ jsonSchema: modesJsonSchema })),
50666
- ui: optional$1(UiSchema)
50736
+ ui: optional$1(UiSchema),
50737
+ skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema }))
50667
50738
  });
50668
50739
  //#endregion
50740
+ //#region src/workflow/vars.ts
50741
+ const PREFIX = "GTD_";
50742
+ /**
50743
+ * One kind of setting, later wins: the workflow's own defaults, the `.gtdrc`
50744
+ * map (`vars:` or `env:`), `--var` overrides (process settings only; environment
50745
+ * settings pass `{}`), then `GTD_<NAME>` for any name an earlier layer declared
50746
+ * (a shell variable never introduces a name).
50747
+ */
50748
+ const resolveVars = (workflowVars, rcVars, entryVars, env) => {
50749
+ const merged = {
50750
+ ...workflowVars,
50751
+ ...rcVars,
50752
+ ...entryVars
50753
+ };
50754
+ for (const name of Object.keys(merged)) {
50755
+ const value = env[PREFIX + name.toUpperCase()];
50756
+ if (value !== void 0) merged[name] = value;
50757
+ }
50758
+ return merged;
50759
+ };
50760
+ /** The first process setting whose value spans lines — a `Gtd-Var:` trailer cannot carry it. */
50761
+ const multilineSetting = (vars) => Object.keys(vars).sort().find((name) => /[\r\n]/.test(vars[name]));
50762
+ const SETTING_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
50763
+ /** A name is written verbatim into a `Gtd-Var: <name>=<value>` trailer, so `=` or a newline would corrupt it. */
50764
+ const isSettingName = (name) => SETTING_NAME.test(name);
50765
+ const SETTING_NAME_RULE = "a setting name is a letter or \"_\", then letters, digits or \"_\"";
50766
+ //#endregion
50669
50767
  //#region src/SteeringFormats.ts
50670
50768
  /** The unrendered `validate:` template a workflow compiler seeds for a built-in mode — quoted so a path with spaces survives interpolation. */
50671
50769
  const seededValidateCommand = (mode) => `gtd check ${mode} "$GTD_FILE"`;
@@ -50736,7 +50834,7 @@ const dedupeDiagnostics = (diagnostics) => {
50736
50834
  };
50737
50835
  //#endregion
50738
50836
  //#region src/workflow/compile.ts
50739
- const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50837
+ const isPlainObject$3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50740
50838
  const describeType = (v) => {
50741
50839
  if (v === null) return "null";
50742
50840
  if (Array.isArray(v)) return "array";
@@ -50750,14 +50848,14 @@ const err = (path, message) => ({
50750
50848
  origin: ""
50751
50849
  });
50752
50850
  /** A flat `name -> scalar` map; a malformed value is a load error and is dropped. */
50753
- const compileVarsMap = (raw) => {
50851
+ const compileVarsMap = (raw, keyName) => {
50754
50852
  const diagnostics = [];
50755
50853
  if (raw === void 0) return {
50756
50854
  vars: {},
50757
50855
  diagnostics
50758
50856
  };
50759
- if (!isPlainObject$2(raw)) {
50760
- diagnostics.push(err(["vars"], `"vars" must be a mapping of name -> scalar value, got ${describeType(raw)}`));
50857
+ if (!isPlainObject$3(raw)) {
50858
+ diagnostics.push(err([keyName], `"${keyName}" must be a mapping of name -> scalar value, got ${describeType(raw)}`));
50761
50859
  return {
50762
50860
  vars: {},
50763
50861
  diagnostics
@@ -50765,8 +50863,12 @@ const compileVarsMap = (raw) => {
50765
50863
  }
50766
50864
  const vars = {};
50767
50865
  for (const [key, value] of Object.entries(raw)) {
50866
+ if (!isSettingName(key)) {
50867
+ diagnostics.push(err([keyName], `"${keyName}" key ${JSON.stringify(key)} is not a valid setting name — ${SETTING_NAME_RULE}`));
50868
+ continue;
50869
+ }
50768
50870
  if (!isScalar(value)) {
50769
- diagnostics.push(err(["vars", key], `"vars.${key}" must be a string, number, or boolean, got ${describeType(value)}`));
50871
+ diagnostics.push(err([keyName, key], `"${keyName}.${key}" must be a string, number, or boolean, got ${describeType(value)}`));
50770
50872
  continue;
50771
50873
  }
50772
50874
  vars[key] = String(value);
@@ -50776,10 +50878,75 @@ const compileVarsMap = (raw) => {
50776
50878
  diagnostics
50777
50879
  };
50778
50880
  };
50881
+ const isStringArray$1 = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
50882
+ /** A flat `step full name -> skill list` map; a malformed value is a load error and is dropped. */
50883
+ const compileSkillsMap = (raw) => {
50884
+ const diagnostics = [];
50885
+ if (raw === void 0) return {
50886
+ skills: {},
50887
+ diagnostics
50888
+ };
50889
+ if (!isPlainObject$3(raw)) {
50890
+ diagnostics.push(err(["skills"], `"skills" must be a mapping of step name -> array of skill names, got ${describeType(raw)}`));
50891
+ return {
50892
+ skills: {},
50893
+ diagnostics
50894
+ };
50895
+ }
50896
+ const skills = Object.create(null);
50897
+ for (const [key, value] of Object.entries(raw)) {
50898
+ if (!isStringArray$1(value)) {
50899
+ diagnostics.push(err(["skills", key], `"skills.${key}" must be an array of skill names (strings), got ${describeType(value)}`));
50900
+ continue;
50901
+ }
50902
+ skills[key] = value;
50903
+ }
50904
+ return {
50905
+ skills,
50906
+ diagnostics
50907
+ };
50908
+ };
50909
+ /**
50910
+ * The nine `*Skills` workflow vars this package deletes, each naming the
50911
+ * `.gtdrc` `skills:` step key(s) that replace it — four of the nine fan out to
50912
+ * several keys, because a var once shared between steps now addresses
50913
+ * them independently. Deliberately the only place gtd special-cases a var
50914
+ * name by string; meant to be deleted a major release after this ships.
50915
+ */
50916
+ const DEAD_SKILLS_VARS = {
50917
+ triageSkills: ["design.triage"],
50918
+ architectureSkills: ["architecture.author"],
50919
+ decomposeSkills: ["architecture.decompose"],
50920
+ buildSkills: ["packages.item.building"],
50921
+ fixSkills: ["packages.item.fix-suite", "build.fix"],
50922
+ reviewFixSkills: [
50923
+ "packages.item.fix-spec",
50924
+ "build.fix-quality",
50925
+ "build.review.fix-nits"
50926
+ ],
50927
+ reviewSkills: [
50928
+ "build.review.reviewing",
50929
+ "build.review.answer-review-questions",
50930
+ "build.review.collecting"
50931
+ ],
50932
+ specReviewSkills: ["packages.item.spec.review"],
50933
+ escalateSkills: ["packages.item.health.describe", "build.health.describe"]
50934
+ };
50935
+ /** `vars:` entries naming one of the nine deleted `*Skills` vars — scoped to `.gtdrc` `vars:` only; there is no environment scan anywhere in this check, so a `GTD_BUILDSKILLS` gets neither this diagnostic nor any other. */
50936
+ const deadSkillsVarDiagnostics = (rawVars) => {
50937
+ if (!isPlainObject$3(rawVars)) return [];
50938
+ const diagnostics = [];
50939
+ for (const key of Object.keys(rawVars)) {
50940
+ const replacement = DEAD_SKILLS_VARS[key];
50941
+ if (replacement === void 0) continue;
50942
+ diagnostics.push(err(["vars", key], `"vars.${key}" was removed — skills are now configured per step, under the "skills:" key, addressed by: ${replacement.join(", ")}`));
50943
+ }
50944
+ return diagnostics;
50945
+ };
50779
50946
  const MODE_COMMAND_KEYS = ["format", "validate"];
50780
50947
  /** One `modes:` entry; malformed entries are load errors and are dropped. */
50781
50948
  const compileMode = (name, entry, diagnostics) => {
50782
- if (!isPlainObject$2(entry)) {
50949
+ if (!isPlainObject$3(entry)) {
50783
50950
  diagnostics.push(err(["modes", name], `mode "${name}": must be an object with "format" and/or "validate", got ${describeType(entry)}`));
50784
50951
  return;
50785
50952
  }
@@ -50809,7 +50976,7 @@ const compileModesMap = (raw) => {
50809
50976
  modes: {},
50810
50977
  diagnostics
50811
50978
  };
50812
- if (!isPlainObject$2(raw)) {
50979
+ if (!isPlainObject$3(raw)) {
50813
50980
  diagnostics.push(err(["modes"], `"modes" must be a mapping of mode name -> { format, validate }, got ${describeType(raw)}`));
50814
50981
  return {
50815
50982
  modes: {},
@@ -50838,7 +51005,7 @@ const mergeModes = (base, override) => {
50838
51005
  };
50839
51006
  /** Stamp a whole (possibly deeply nested) value with ONE origin, recursively — used when `inner` replaces `base` wholesale: every key inside still needs its own node, or a LATER layer partially overriding just one nested key would silently lose the rest of this subtree's origin. */
50840
51007
  const uniformOrigin = (value, origin) => {
50841
- if (!isPlainObject$2(value)) return { self: origin };
51008
+ if (!isPlainObject$3(value)) return { self: origin };
50842
51009
  const children = {};
50843
51010
  for (const [key, v] of Object.entries(value)) children[key] = uniformOrigin(v, origin);
50844
51011
  return {
@@ -50856,7 +51023,7 @@ const uniformOrigin = (value, origin) => {
50856
51023
  * findings, not one origin for the whole merged key.
50857
51024
  */
50858
51025
  const mergeWithOrigin = (base, baseOrigin, inner, innerOrigin) => {
50859
- if (!isPlainObject$2(base) || !isPlainObject$2(inner)) return {
51026
+ if (!isPlainObject$3(base) || !isPlainObject$3(inner)) return {
50860
51027
  value: inner,
50861
51028
  origin: uniformOrigin(inner, innerOrigin)
50862
51029
  };
@@ -50896,9 +51063,16 @@ const withOrigin = (diagnostics, lookup) => diagnostics.map((d) => ({
50896
51063
  ...d,
50897
51064
  origin: lookup(d.path)
50898
51065
  }));
51066
+ const layerKeys = (layers, name) => layers.flatMap((layer) => {
51067
+ const own = isPlainObject$3(layer.value) ? layer.value[name] : void 0;
51068
+ return isPlainObject$3(own) ? Object.keys(own).map((key) => ({
51069
+ key,
51070
+ origin: layer.origin
51071
+ })) : [];
51072
+ });
50899
51073
  /**
50900
51074
  * Merge every layer outermost→innermost and compile what a `.gtdrc` may carry:
50901
- * `vars`, `modes` and `ui`. A workflow is defined only in `gtd.config.ts`, so a
51075
+ * `vars`, `env`, `judge`, `modes` and `ui`. A workflow is defined only in `gtd.config.ts`, so a
50902
51076
  * leftover `workflow:` key is an error pointing there. Pure and total.
50903
51077
  */
50904
51078
  const compileConfig = (layers) => {
@@ -50907,7 +51081,7 @@ const compileConfig = (layers) => {
50907
51081
  let merged = {};
50908
51082
  let originTree = { self: BUILT_IN_ORIGIN };
50909
51083
  for (const layer of layers) {
50910
- const value = isPlainObject$2(layer.value) ? layer.value : {};
51084
+ const value = isPlainObject$3(layer.value) ? layer.value : {};
50911
51085
  const step = mergeWithOrigin(merged, originTree, value, layer.origin);
50912
51086
  merged = step.value;
50913
51087
  originTree = step.origin;
@@ -50915,37 +51089,100 @@ const compileConfig = (layers) => {
50915
51089
  const mergedConfig = merged;
50916
51090
  const lookupIn = (path) => originAt(originTree, path);
50917
51091
  const ui = mergedConfig["ui"];
50918
- const { vars: rcVars, diagnostics: varsDiagnostics } = compileVarsMap(mergedConfig["vars"]);
51092
+ const judge = mergedConfig["judge"];
51093
+ const { vars: rcVars, diagnostics: varsDiagnostics } = compileVarsMap(mergedConfig["vars"], "vars");
50919
51094
  diagnostics.push(...withOrigin(varsDiagnostics, lookupIn));
51095
+ const { vars: rcEnv, diagnostics: envDiagnostics } = compileVarsMap(mergedConfig["env"], "env");
51096
+ diagnostics.push(...withOrigin(envDiagnostics, lookupIn));
51097
+ const varsKeys = layerKeys(layers, "vars");
51098
+ const envKeys = layerKeys(layers, "env");
51099
+ diagnostics.push(...withOrigin(deadSkillsVarDiagnostics(mergedConfig["vars"]), lookupIn));
50920
51100
  const { modes: rcModes, diagnostics: modesDiagnostics } = compileModesMap(mergedConfig["modes"]);
50921
51101
  diagnostics.push(...withOrigin(modesDiagnostics, lookupIn));
51102
+ const skillsKeys = [];
51103
+ for (const layer of layers) {
51104
+ if (!isPlainObject$3(layer.value)) continue;
51105
+ const own = compileSkillsMap(layer.value["skills"]);
51106
+ diagnostics.push(...own.diagnostics.map((d) => ({
51107
+ ...d,
51108
+ origin: layer.origin
51109
+ })));
51110
+ skillsKeys.push(...Object.keys(own.skills).map((key) => ({
51111
+ key,
51112
+ origin: layer.origin
51113
+ })));
51114
+ }
51115
+ const { skills: rcSkills } = compileSkillsMap(mergedConfig["skills"]);
50922
51116
  const seeded = Object.fromEntries(BUILT_IN_MODE_NAMES.map((name) => [name, { validate: seededValidateCommand(name) }]));
50923
51117
  return {
50924
51118
  rcVars,
51119
+ rcEnv,
51120
+ varsKeys,
51121
+ envKeys,
51122
+ ...judge !== void 0 ? { judge } : {},
50925
51123
  modes: mergeModes(seeded, rcModes),
50926
51124
  ...ui !== void 0 ? { ui } : {},
51125
+ rcSkills,
51126
+ skillsKeys,
50927
51127
  diagnostics: dedupeDiagnostics(sortDiagnostics(diagnostics, layerOrder))
50928
51128
  };
50929
51129
  };
50930
51130
  //#endregion
50931
- //#region src/workflow/vars.ts
50932
- const PREFIX = "GTD_";
51131
+ //#region src/workflow/interpolate.ts
51132
+ const VAR_PATTERN = /\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
51133
+ const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50933
51134
  /**
50934
- * The merged vars, later wins: the workflow's own defaults, `.gtdrc` `vars:`,
50935
- * the process's entry vars, then `GTD_<NAME>` for any name an earlier layer
50936
- * declared (an env var never introduces a name).
51135
+ * `modes.*.format`, `modes.*.validate` and `ui.format` — gtd hands these
51136
+ * straight to bash, where `$` already means the shell's own expansion. The
51137
+ * predicate reads only the config PATH shape, never the value: sniffing for
51138
+ * `$GTD_FILE` would exempt whichever command happens not to name it, and
51139
+ * silently interpolate one that names something else instead.
50937
51140
  */
50938
- const resolveVars = (workflowVars, rcVars, entryVars, env) => {
50939
- const merged = {
50940
- ...workflowVars,
50941
- ...rcVars,
50942
- ...entryVars
50943
- };
50944
- for (const name of Object.keys(merged)) {
50945
- const value = env[PREFIX + name.toUpperCase()];
50946
- if (value !== void 0) merged[name] = value;
51141
+ const isBashExemptPath = (path) => {
51142
+ if (path.length === 3 && path[0] === "modes" && (path[2] === "format" || path[2] === "validate")) return true;
51143
+ if (path.length === 2 && path[0] === "ui" && path[1] === "format") return true;
51144
+ return false;
51145
+ };
51146
+ const interpolateString = (value, path, env, diagnostics) => value.replace(VAR_PATTERN, (match, braced, bare) => {
51147
+ if (match === "$$") return "$";
51148
+ const name = braced ?? bare;
51149
+ if (name === void 0) return match;
51150
+ const resolved = env[name];
51151
+ if (resolved !== void 0) return resolved;
51152
+ diagnostics.push({
51153
+ severity: "error",
51154
+ path,
51155
+ message: `"${match}" references environment variable "${name}", which is not set`,
51156
+ origin: ""
51157
+ });
51158
+ return match;
51159
+ });
51160
+ const walk = (value, path, env, diagnostics) => {
51161
+ if (typeof value === "string") return isBashExemptPath(path) ? value : interpolateString(value, path, env, diagnostics);
51162
+ if (Array.isArray(value)) return value.map((v, i) => walk(v, [...path, i], env, diagnostics));
51163
+ if (isPlainObject$2(value)) {
51164
+ const out = {};
51165
+ for (const [key, v] of Object.entries(value)) out[key] = walk(v, [...path, key], env, diagnostics);
51166
+ return out;
50947
51167
  }
50948
- return merged;
51168
+ return value;
51169
+ };
51170
+ /**
51171
+ * Expand `$NAME`/`${NAME}` from `env` over every string leaf of a decoded
51172
+ * `.gtdrc` layer, except the three paths gtd hands to bash. `$$` yields a
51173
+ * literal `$`; any other `$` not followed by a valid name (a trailing `$`,
51174
+ * `$1`, `$-`, `$ `) is left untouched. Exactly one pass: `String.replace`
51175
+ * never rescans a replacement's own text, so an environment variable's value
51176
+ * can never be read back as config syntax. An unset name is a load-error
51177
+ * `Diagnostic` naming the config path; the original `$NAME` text is left in
51178
+ * place rather than blanked.
51179
+ */
51180
+ const interpolate = (config, env) => {
51181
+ const diagnostics = [];
51182
+ return {
51183
+ value: walk(config, [], env, diagnostics),
51184
+ diagnostics
51185
+ };
50949
51186
  };
50950
51187
  //#endregion
50951
51188
  //#region node_modules/yaml/dist/nodes/identity.js
@@ -57718,7 +57955,7 @@ const dropOptionalUndefinedArtifacts = (all) => {
57718
57955
  * findings surface together through `load`'s single sorted/deduped report,
57719
57956
  * the same as every other producer in this package.
57720
57957
  */
57721
- const decodeLevel = (level) => gen(function* () {
57958
+ const decodeLevel = (level, env) => gen(function* () {
57722
57959
  if (!isPlainObject$1(level.config)) return {
57723
57960
  layer: void 0,
57724
57961
  diagnostics: [{
@@ -57739,53 +57976,83 @@ const decodeLevel = (level) => gen(function* () {
57739
57976
  origin: level.filepath
57740
57977
  }))
57741
57978
  };
57979
+ const interpolated = interpolate(result.right, env);
57742
57980
  return {
57743
57981
  layer: {
57744
57982
  origin: level.filepath,
57745
57983
  dir: dirname(level.filepath),
57746
- value: result.right
57984
+ value: interpolated.value
57747
57985
  },
57748
- diagnostics: []
57986
+ diagnostics: interpolated.diagnostics.map((d) => ({
57987
+ ...d,
57988
+ origin: level.filepath
57989
+ }))
57749
57990
  };
57750
57991
  });
57992
+ /** Everything the `.gtdrc` half of a load reads, findings not yet judged. */
57993
+ const readRc = gen(function* () {
57994
+ const narrator = yield* Narrator;
57995
+ const host = yield* Host;
57996
+ const levels = yield* (yield* ConfigDiscovery).levels(host.root, host.home);
57997
+ for (const level of levels) yield* narrator.narrate(`config: layer ${level.filepath}`);
57998
+ const decoded = yield* forEach$2(levels, (level) => decodeLevel(level, host.env));
57999
+ return {
58000
+ levels,
58001
+ compiled: compileConfig(decoded.flatMap((d) => d.layer !== void 0 ? [d.layer] : [])),
58002
+ decodeDiagnostics: decoded.flatMap((d) => d.diagnostics)
58003
+ };
58004
+ });
58005
+ const failOnErrors = (diagnostics) => {
58006
+ const fatal = diagnostics.filter((d) => d.severity === "error");
58007
+ if (fatal.length === 0) return _void;
58008
+ const lines = fatal.map(formatDiagnostic);
58009
+ return fail$6(new GtdError(`gtd config:\n${lines.map((line) => ` - ${line}`).join("\n")}`));
58010
+ };
58011
+ /**
58012
+ * The `.gtdrc` half of `load`: no repository, no `gtd.config.ts` evaluation.
58013
+ * Any error diagnostic fails it.
58014
+ */
58015
+ const loadRcConfig = gen(function* () {
58016
+ const { levels, compiled, decodeDiagnostics } = yield* readRc;
58017
+ yield* failOnErrors(dedupeDiagnostics(sortDiagnostics([...decodeDiagnostics, ...compiled.diagnostics], levels.map((level) => level.filepath))));
58018
+ return compiled;
58019
+ });
57751
58020
  /**
57752
58021
  * Build the whole config pipeline — discover levels by walking `Host`'s
57753
58022
  * cwd→home chain (`ConfigDiscovery`, kept behind a `Context.Tag` since its
57754
58023
  * production implementation reads real disk through cosmiconfig directly,
57755
58024
  * incompatible with an in-memory `@inmem` `Workspace`), decode each level
57756
- * individually against `ConfigSchema`, then hand every successfully-decoded
57757
- * layer to the pure `compileWorkflow` boundary (`src/workflow/compile.ts`),
57758
- * which does its own merging and `./`/`../` file-ref inlining (through
57759
- * `Workspace`, which — unlike discovery — both production and an in-memory
57760
- * scenario back identically). A decode failure never short-circuits the
58025
+ * individually against `ConfigSchema`, then compile every successfully-decoded
58026
+ * layer (`src/workflow/compile.ts`). A decode failure never short-circuits the
57761
58027
  * whole load: every level's own findings — decode AND compile alike — are
57762
- * sorted/deduped together (`levels`' own order is the layer order, decode
57763
- * failures included) into ONE report, so a config broken at two ancestor
58028
+ * sorted/deduped together into ONE report, so a config broken at two ancestor
57764
58029
  * layers reports both instead of just the first one reached.
57765
58030
  */
57766
58031
  const load = gen(function* () {
57767
- const narrator = yield* Narrator;
57768
58032
  const host = yield* Host;
57769
58033
  const discovery = yield* ConfigDiscovery;
57770
- const levels = yield* discovery.levels(host.root, host.home);
57771
- for (const level of levels) yield* narrator.narrate(`config: layer ${level.filepath}`);
57772
- const decoded = yield* forEach$2(levels, decodeLevel);
57773
- const layers = decoded.flatMap((d) => d.layer !== void 0 ? [d.layer] : []);
57774
- const decodeDiagnostics = decoded.flatMap((d) => d.diagnostics);
57775
- const compiled = compileConfig(layers);
58034
+ const { levels, compiled, decodeDiagnostics } = yield* readRc;
57776
58035
  const module = yield* discovery.workflowModule(host.root, host.home);
57777
58036
  const loaded = yield* try_({
57778
58037
  try: () => loadWorkflow(module),
57779
58038
  catch: (e) => new GtdError(`gtd config:\n - ${module?.filepath ?? "(built-in default)"}: ${e instanceof Error ? e.message : String(e)}`)
57780
58039
  });
57781
58040
  const layerOrder = [...levels.map((level) => level.filepath), ...module ? [module.filepath] : []];
57782
- const diagnostics = dedupeDiagnostics(sortDiagnostics([...decodeDiagnostics, ...compiled.diagnostics], layerOrder));
57783
- const fatal = diagnostics.filter((d) => d.severity === "error");
57784
- if (fatal.length > 0) {
57785
- const lines = fatal.map(formatDiagnostic);
57786
- return yield* fail$6(new GtdError(`gtd config:\n${lines.map((line) => ` - ${line}`).join("\n")}`));
57787
- }
57788
- const initial = yield* firstStep(loaded, resolveVars(loaded.defaults, compiled.rcVars, {}, host.env), headTree(yield* Workspace));
58041
+ const knownSkillNames = Object.keys(loaded.skills).sort();
58042
+ const unknownSkillDiagnostics = compiled.skillsKeys.filter(({ key }) => !Object.hasOwn(loaded.skills, key)).map(({ key, origin }) => ({
58043
+ severity: "error",
58044
+ path: ["skills", key],
58045
+ message: `"skills.${key}" names a step this workflow does not declare — known step names: ${knownSkillNames.join(", ")}`,
58046
+ origin
58047
+ }));
58048
+ const diagnostics = dedupeDiagnostics(sortDiagnostics([
58049
+ ...decodeDiagnostics,
58050
+ ...compiled.diagnostics,
58051
+ ...unknownSkillDiagnostics,
58052
+ ...wrongKindDiagnostics(loaded, compiled)
58053
+ ], layerOrder));
58054
+ yield* failOnErrors(diagnostics);
58055
+ const initial = yield* firstStep(loaded, headTree(yield* Workspace));
57789
58056
  return {
57790
58057
  workflow: {
57791
58058
  flow: loaded.flow,
@@ -57793,16 +58060,36 @@ const load = gen(function* () {
57793
58060
  base: loaded.base,
57794
58061
  steering: loaded.steering,
57795
58062
  modes: compiled.modes,
58063
+ skills: loaded.skills,
58064
+ configuredSkills: compiled.rcSkills,
57796
58065
  initial
57797
58066
  },
57798
58067
  workflowVars: { ...loaded.defaults },
57799
58068
  rcVars: compiled.rcVars,
58069
+ workflowEnv: { ...loaded.envDefaults },
58070
+ rcEnv: compiled.rcEnv,
57800
58071
  ...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
57801
58072
  warnings: diagnostics.filter((d) => d.severity === "warning"),
57802
58073
  configFiles: levels.map((level) => level.filepath),
57803
58074
  workflowFiles: loaded.dependencyFiles
57804
58075
  };
57805
58076
  });
58077
+ /** A name under the wrong key (or under both) in a `.gtdrc` layer: each is an error at its own origin. */
58078
+ const wrongKindDiagnostics = (loaded, compiled) => {
58079
+ const error = (path, message, origin) => ({
58080
+ severity: "error",
58081
+ path,
58082
+ message,
58083
+ origin
58084
+ });
58085
+ const diagnostics = [];
58086
+ 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));
58087
+ for (const { key, origin } of compiled.envKeys) {
58088
+ if (Object.hasOwn(loaded.defaults, key)) diagnostics.push(error(["env", key], `"env.${key}" is a process setting — move it under "vars:"`, origin));
58089
+ 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));
58090
+ }
58091
+ return diagnostics;
58092
+ };
57806
58093
  const TRANSFORM_MEMO_SIZE = 512;
57807
58094
  const memoized = (transform) => {
57808
58095
  const memo = /* @__PURE__ */ new Map();
@@ -57839,6 +58126,13 @@ const stringRecord = (exports, name) => {
57839
58126
  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`);
57840
58127
  return value;
57841
58128
  };
58129
+ const isStringArray = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
58130
+ /** Read a module's optional `skills` export: full step name -> skill list, mirroring `stringRecord` for the vars-shaped exports. */
58131
+ const skillsRecord = (exports) => {
58132
+ const value = exports["skills"] ?? {};
58133
+ 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`);
58134
+ return value;
58135
+ };
57842
58136
  const optionalFunction = (exports, name) => {
57843
58137
  const value = exports[name];
57844
58138
  if (value === void 0) return void 0;
@@ -57847,16 +58141,26 @@ const optionalFunction = (exports, name) => {
57847
58141
  };
57848
58142
  /**
57849
58143
  * Read a workflow module: the default export is the flow; `defaults`,
57850
- * `summary`, `base` and `steering` are optional; any other export is ignored.
58144
+ * `envDefaults`, `summary`, `base` and `steering` are optional; any other export is ignored.
57851
58145
  */
57852
58146
  const fromModule = (exported, origin, dependencyFiles) => {
57853
58147
  const exports = typeof exported === "object" && exported !== null ? exported : {};
57854
58148
  const flow = exports.default;
57855
58149
  if (typeof flow !== "function") throw new Error("the default export is not a flow — export default an async function");
58150
+ const defaults = stringRecord(exports, "defaults");
58151
+ const envDefaults = stringRecord(exports, "envDefaults");
58152
+ for (const [exportName, record] of [["defaults", defaults], ["envDefaults", envDefaults]]) {
58153
+ const bad = Object.keys(record).find((name) => !isSettingName(name));
58154
+ if (bad !== void 0) throw new Error(`the "${exportName}" export declares ${JSON.stringify(bad)}, not a valid setting name — ${SETTING_NAME_RULE}`);
58155
+ }
58156
+ const both = Object.keys(defaults).filter((name) => Object.hasOwn(envDefaults, name));
58157
+ 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`);
57856
58158
  return {
57857
58159
  flow,
57858
- defaults: stringRecord(exports, "defaults"),
58160
+ defaults,
58161
+ envDefaults,
57859
58162
  steering: stringRecord(exports, "steering"),
58163
+ skills: skillsRecord(exports),
57860
58164
  summary: optionalFunction(exports, "summary"),
57861
58165
  base: optionalFunction(exports, "base"),
57862
58166
  origin,
@@ -57901,7 +58205,38 @@ const watchedEmptyTree = () => {
57901
58205
  read: () => read
57902
58206
  };
57903
58207
  };
57904
- const replayToFirstStep = (loaded, vars, tree) => flatMap$4(promise(() => replay({
58208
+ /** An empty settings record that logs every read: a named `get`/`has`, or an enumeration. */
58209
+ const watchedEmptySettings = () => {
58210
+ const names = /* @__PURE__ */ new Set();
58211
+ let enumerated = false;
58212
+ return {
58213
+ settings: new Proxy({}, {
58214
+ get: (_target, key) => {
58215
+ if (typeof key === "string") names.add(key);
58216
+ },
58217
+ has: (_target, key) => {
58218
+ if (typeof key === "string") names.add(key);
58219
+ return false;
58220
+ },
58221
+ ownKeys: () => {
58222
+ enumerated = true;
58223
+ return [];
58224
+ }
58225
+ }),
58226
+ names,
58227
+ enumerated: () => enumerated
58228
+ };
58229
+ };
58230
+ const settingReads = (kind, watch) => {
58231
+ const reads = [];
58232
+ if (watch.names.size > 0) {
58233
+ const sorted = [...watch.names].sort().map((name) => JSON.stringify(name));
58234
+ reads.push(`the ${kind} setting${sorted.length > 1 ? "s" : ""} ${sorted.join(", ")}`);
58235
+ }
58236
+ if (watch.enumerated()) reads.push(`every ${kind} setting`);
58237
+ return reads;
58238
+ };
58239
+ const replayToFirstStep = (loaded, vars, env, tree) => flatMap$4(promise(() => replay({
57905
58240
  flow: loaded.flow,
57906
58241
  episode: {
57907
58242
  entry: void 0,
@@ -57912,6 +58247,7 @@ const replayToFirstStep = (loaded, vars, tree) => flatMap$4(promise(() => replay
57912
58247
  commits: []
57913
58248
  },
57914
58249
  vars,
58250
+ env,
57915
58251
  start: "",
57916
58252
  budgetBytes: Number.MAX_SAFE_INTEGER
57917
58253
  })), (outcome) => {
@@ -57922,15 +58258,22 @@ const replayToFirstStep = (loaded, vars, tree) => flatMap$4(promise(() => replay
57922
58258
  /**
57923
58259
  * The default entry's first step — where a finished process waits — found the
57924
58260
  * only way a flow can be read: by replaying it over an empty history. Episode
57925
- * boundaries are that step's commits, so it must not move with the tree: a
57926
- * flow that reads the repository before reaching it is replayed again over
57927
- * HEAD, and must reach the same step there.
58261
+ * boundaries are that step's commits, so it must depend on neither a setting
58262
+ * (edited mid-process, it would move the boundary before pinned values are
58263
+ * read) nor the tree: a flow that reads the repository before reaching it is
58264
+ * replayed again over HEAD, and must reach the same step there.
57928
58265
  */
57929
- const firstStep = (loaded, vars, head) => gen(function* () {
58266
+ const firstStep = (loaded, head) => gen(function* () {
57930
58267
  const empty = watchedEmptyTree();
57931
- const name = yield* replayToFirstStep(loaded, vars, empty.tree);
58268
+ const vars = watchedEmptySettings();
58269
+ const env = watchedEmptySettings();
58270
+ const outcome = yield* either(replayToFirstStep(loaded, vars.settings, env.settings, empty.tree));
58271
+ const reads = [...settingReads("process", vars), ...settingReads("environment", env)];
58272
+ if (reads.length > 0) return yield* fail$6(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`));
58273
+ if (outcome._tag === "Left") return yield* fail$6(outcome.left);
58274
+ const name = outcome.right;
57932
58275
  if (!empty.read()) return name;
57933
- const atHead = yield* replayToFirstStep(loaded, vars, head);
58276
+ const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head);
57934
58277
  if (atHead === name) return name;
57935
58278
  return yield* fail$6(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`));
57936
58279
  });
@@ -57940,14 +58283,14 @@ var ConfigService = class ConfigService extends Tag("ConfigService")() {
57940
58283
  //#endregion
57941
58284
  //#region src/workflow/init.ts
57942
58285
  const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
57943
- const INIT_VARS = { testCommand: "npm test" };
58286
+ const INIT_ENV = { testCommand: "npm test" };
57944
58287
  const MODES_SUGGESTION = {
57945
58288
  qa: { format: "npx prettier --write \"$GTD_FILE\"" },
57946
58289
  review: { format: "npx prettier --write \"$GTD_FILE\"" }
57947
58290
  };
57948
58291
  const renderInitScaffold = () => ({ config: JSON.stringify({
57949
58292
  $schema: SCHEMA_URL,
57950
- vars: INIT_VARS,
58293
+ env: INIT_ENV,
57951
58294
  modes: MODES_SUGGESTION
57952
58295
  }, null, 2) + "\n" });
57953
58296
  //#endregion
@@ -58244,12 +58587,37 @@ const llm = ({ env, cwd, model = DEFAULT_MODEL, timeoutMs = TIMEOUT_MS }) => asy
58244
58587
  };
58245
58588
  //#endregion
58246
58589
  //#region src/judges/run.ts
58590
+ const PROVIDERS = [
58591
+ "fixed",
58592
+ "jev",
58593
+ "llm"
58594
+ ];
58247
58595
  const fail = (message) => {
58248
58596
  throw new Error(`gtd judge run: ${message}`);
58249
58597
  };
58598
+ const envProvider = (env) => {
58599
+ const value = env["GTD_JUDGE_PROVIDER"] || void 0;
58600
+ if (value !== void 0 && !PROVIDERS.includes(value)) return fail(`GTD_JUDGE_PROVIDER must be one of ${PROVIDERS.join(", ")} — got "${value}"`);
58601
+ return value;
58602
+ };
58603
+ const checkPairing = (resolved) => resolved.model !== void 0 && resolved.provider !== void 0 && resolved.provider !== "llm" ? fail(`a model only applies to provider llm, not ${resolved.provider}`) : resolved;
58604
+ /**
58605
+ * Per field, the first one set wins: flag, then `GTD_JUDGE_PROVIDER` /
58606
+ * `GTD_JUDGE_MODEL` (empty counts as unset), then `.gtdrc` `judge:`. The
58607
+ * provider/model pairing is checked on the resolved pair, whichever layer
58608
+ * supplied each half.
58609
+ */
58610
+ const selectJudge = ({ provider, model, configured, env }) => {
58611
+ const resolved = {
58612
+ provider: provider ?? envProvider(env) ?? configured?.provider,
58613
+ model: model ?? (env["GTD_JUDGE_MODEL"] || void 0) ?? configured?.model
58614
+ };
58615
+ return checkPairing(resolved);
58616
+ };
58250
58617
  /** The only place auto selection lives; it never falls back across providers. */
58251
- const answererFor = ({ provider, answers, env, model, cwd }) => {
58252
- if (model !== void 0 && provider !== void 0 && provider !== "llm") return fail(`--model only applies to --provider llm, not ${provider}`);
58618
+ const answererFor = (opts) => {
58619
+ const { answers, env, cwd } = opts;
58620
+ const { provider, model } = selectJudge(opts);
58253
58621
  if (provider === "fixed") return fixed({
58254
58622
  answersPath: answers,
58255
58623
  env
@@ -63596,7 +63964,7 @@ const runOf = (history, location, closingHash = void 0) => {
63596
63964
  trace: processCommits.map((c, i) => traceEntryOf(c.hash, parsed[i])),
63597
63965
  costEntries: parsed.flatMap(costEntriesOf),
63598
63966
  judgeVerdicts: parsed.flatMap((m) => m.judge),
63599
- entryVars: location.entry === void 0 ? {} : { ...first?.vars },
63967
+ pinnedVars: { ...first?.vars },
63600
63968
  headTurn: headTurnOf(head),
63601
63969
  closingHash,
63602
63970
  episode: {
@@ -63752,6 +64120,20 @@ const pendingTree = (workspace, entries, rewrite) => {
63752
64120
  };
63753
64121
  };
63754
64122
  /**
64123
+ * The one resolver of both setting kinds. Process settings: the recorded
64124
+ * (pinned) value wins per name, anything unrecorded resolves live — the
64125
+ * fallback for a process started before pinning existed. Environment settings
64126
+ * are always live. `entryOverrides` are `--var` values, passed only while a
64127
+ * process is being entered.
64128
+ */
64129
+ const settingsFor = (config, pinnedVars, hostEnv, entryOverrides = {}) => ({
64130
+ vars: {
64131
+ ...resolveVars(config.workflowVars, config.rcVars, entryOverrides, hostEnv),
64132
+ ...pinnedVars
64133
+ },
64134
+ env: resolveVars(config.workflowEnv, config.rcEnv, {}, hostEnv)
64135
+ });
64136
+ /**
63755
64137
  * `judgeBudgetBytes` is the one var that refuses rather than disabling its
63756
64138
  * mechanism when blanked: without a bound the judge payload is rejected
63757
64139
  * anyway. Absent falls back to a conservative default.
@@ -63772,9 +64154,12 @@ const replayFor = (setup, pending) => replay({
63772
64154
  commits: setup.episode
63773
64155
  },
63774
64156
  vars: setup.vars,
64157
+ env: setup.env,
63775
64158
  start: setup.run.diffBase,
63776
64159
  startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
63777
64160
  budgetBytes: setup.budget,
64161
+ skills: setup.def.skills,
64162
+ configuredSkills: setup.def.configuredSkills,
63778
64163
  ...pending !== void 0 ? { pending } : {}
63779
64164
  });
63780
64165
  const replayError = (outcome) => {
@@ -63912,7 +64297,7 @@ const restAt = (ref) => gen(function* () {
63912
64297
  const host = yield* Host;
63913
64298
  const def = config.workflow;
63914
64299
  const run = yield* computeProcessRun(git, def, ref);
63915
- const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, host.env);
64300
+ const { vars, env } = settingsFor(config, run.pinnedVars, host.env);
63916
64301
  const budget = yield* try_({
63917
64302
  try: () => judgeBudgetBytes(vars),
63918
64303
  catch: (e) => e instanceof Error ? e : new Error(String(e))
@@ -63935,6 +64320,7 @@ const restAt = (ref) => gen(function* () {
63935
64320
  def,
63936
64321
  run,
63937
64322
  vars,
64323
+ env,
63938
64324
  budget,
63939
64325
  workspace,
63940
64326
  base,
@@ -63962,6 +64348,7 @@ const restAt = (ref) => gen(function* () {
63962
64348
  trace: outcome.trace,
63963
64349
  run,
63964
64350
  vars,
64351
+ env,
63965
64352
  changes,
63966
64353
  memory: memory.key,
63967
64354
  memoryResumed: memory.resumed,
@@ -63982,7 +64369,7 @@ const currentRest = restAt(void 0);
63982
64369
  const entryRefusal = (rest, name, entryVars) => gen(function* () {
63983
64370
  const config = yield* (yield* ConfigService).load;
63984
64371
  const host = yield* Host;
63985
- const vars = resolveVars(config.workflowVars, config.rcVars, entryVars, host.env);
64372
+ const { vars, env } = settingsFor(config, {}, host.env, entryVars);
63986
64373
  const workspace = rest.setup.workspace;
63987
64374
  const outcome = yield* promise(() => replay({
63988
64375
  flow: rest.def.flow,
@@ -63995,8 +64382,11 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
63995
64382
  commits: []
63996
64383
  },
63997
64384
  vars,
64385
+ env,
63998
64386
  start: "",
63999
- budgetBytes: rest.setup.budget
64387
+ budgetBytes: rest.setup.budget,
64388
+ skills: rest.def.skills,
64389
+ configuredSkills: rest.def.configuredSkills
64000
64390
  }));
64001
64391
  if (outcome.kind === "refused") return outcome.message;
64002
64392
  if (outcome.kind === "rest") return outcome.entryRead ? void 0 : `"${name}" is not an enterable state — this workflow reads no entry`;
@@ -64045,28 +64435,29 @@ const reviewGateRewrite = (def) => {
64045
64435
  apply: (content) => reviewFormat.clearTicks(content)
64046
64436
  };
64047
64437
  };
64048
- /**
64049
- * What landing the pending turn at `rest` does — a pure decision from a
64050
- * replay with the working tree as the pending turn. The target step is
64051
- * computed by replaying, never matched.
64052
- */
64053
- const decideLanding = (rest, verdicts) => gen(function* () {
64054
- const early = cleanLanding(rest);
64055
- if (early !== void 0) return early;
64056
- const outcome = yield* promise(() => replayFor(rest.setup, {
64057
- tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
64058
- ...verdicts !== void 0 ? { verdicts } : {}
64059
- }));
64060
- if (outcome.kind === "refused") return {
64438
+ const multilineRefusal = (vars) => {
64439
+ const multiline = multilineSetting(vars);
64440
+ return multiline === void 0 ? void 0 : {
64061
64441
  kind: "refusal",
64062
- message: outcome.message
64442
+ message: `gtd: process setting "${multiline}" spans several lines — a process setting is pinned in a commit trailer and must be a single line`
64063
64443
  };
64064
- if (outcome.kind === "divergence" || outcome.kind === "failed") return yield* fail$6(new Error(outcome.message));
64065
- const to = outcome.kind === "rest" ? outcome.rest.name : rest.def.initial;
64444
+ };
64445
+ const landingTarget = (outcome, rest) => {
64446
+ if (outcome.kind === "refused") return succeed$3({
64447
+ kind: "refusal",
64448
+ message: outcome.message
64449
+ });
64450
+ if (outcome.kind === "divergence" || outcome.kind === "failed") return fail$6(new Error(outcome.message));
64451
+ return succeed$3(outcome.kind === "rest" ? outcome.rest.name : rest.def.initial);
64452
+ };
64453
+ const landingTo = (rest, to) => {
64066
64454
  if (rest.changes.length === 0 && rest.stepDef.kind === "script" && to === rest.state) return {
64067
64455
  kind: "noop",
64068
64456
  settled: true
64069
64457
  };
64458
+ const starts = noProcessUnderway(rest) && to !== rest.def.initial;
64459
+ const refused = starts ? multilineRefusal(rest.vars) : void 0;
64460
+ if (refused !== void 0) return refused;
64070
64461
  return {
64071
64462
  kind: "commit",
64072
64463
  to,
@@ -64074,9 +64465,26 @@ const decideLanding = (rest, verdicts) => gen(function* () {
64074
64465
  actor: rest.actor,
64075
64466
  from: rest.state,
64076
64467
  to,
64077
- step: rest.step.id
64468
+ step: rest.step.id,
64469
+ ...starts ? { vars: rest.vars } : {}
64078
64470
  }
64079
64471
  };
64472
+ };
64473
+ /**
64474
+ * What landing the pending turn at `rest` does — a pure decision from a
64475
+ * replay with the working tree as the pending turn. The target step is
64476
+ * computed by replaying, never matched.
64477
+ */
64478
+ const decideLanding = (rest, verdicts) => gen(function* () {
64479
+ const early = cleanLanding(rest);
64480
+ if (early !== void 0) return early;
64481
+ const outcome = yield* promise(() => replayFor(rest.setup, {
64482
+ tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
64483
+ ...verdicts !== void 0 ? { verdicts } : {}
64484
+ }));
64485
+ const to = yield* landingTarget(outcome, rest);
64486
+ if (typeof to !== "string") return to;
64487
+ return landingTo(rest, to);
64080
64488
  });
64081
64489
  /** Where landing the pending turn would leave the process, or `undefined` when it would land nothing. */
64082
64490
  const previewLanding = (rest) => decideLanding(rest, void 0).pipe(map$4((landing) => landing.kind === "commit" ? landing.to : void 0), catchAll(() => succeed$3(void 0)));
@@ -64109,7 +64517,7 @@ const summaryFor = (run) => gen(function* () {
64109
64517
  })),
64110
64518
  processCost: run.costEntries.reduce((sum, entry) => sum + entry.cost, 0),
64111
64519
  processCostByModel: costByModel(run.costEntries),
64112
- vars: resolveVars(config.workflowVars, config.rcVars, run.entryVars, host.env)
64520
+ ...settingsFor(config, run.pinnedVars, host.env)
64113
64521
  });
64114
64522
  });
64115
64523
  //#endregion
@@ -64208,9 +64616,15 @@ const refusal = (message) => ({
64208
64616
  kind: "refusal",
64209
64617
  message
64210
64618
  });
64619
+ const checkOverrides = (commandLabel, varOverrides, declaredNames, envNames) => gen(function* () {
64620
+ const environmentOnly = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n) && envNames.includes(n));
64621
+ if (environmentOnly.length > 0) return yield* fail$6(new GtdUsageError(`${commandLabel}: --var pins process settings only; ${environmentOnly.join(", ")} ${environmentOnly.length === 1 ? "is an environment setting" : "are environment settings"} — set it under "env:" in .gtdrc or with a GTD_<NAME> environment variable`));
64622
+ const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
64623
+ if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
64624
+ });
64211
64625
  /**
64212
64626
  * `gtd --entry <name>`: start a brand NEW process at a workflow entry, writing
64213
- * an opening commit that carries zero or more `Gtd-Var:` trailers, plus a
64627
+ * an opening commit that carries every process setting as a `Gtd-Var:` trailer, plus a
64214
64628
  * `Gtd-Review-Base:` trailer when the entry fixes the process's diff base.
64215
64629
  * All validation is a REFUSAL, not an Effect failure.
64216
64630
  */
@@ -64223,11 +64637,16 @@ const planEntry = (current, actor, entry) => gen(function* () {
64223
64637
  ...config.workflowVars,
64224
64638
  ...config.rcVars
64225
64639
  });
64226
- const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n));
64227
- if (undeclared.length > 0) return refusal(`${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(", ")} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`);
64640
+ const overrideRefusal = yield* checkOverrides(commandLabel, varOverrides, declaredNames, Object.keys({
64641
+ ...config.workflowEnv,
64642
+ ...config.rcEnv
64643
+ }));
64644
+ if (overrideRefusal !== void 0) return overrideRefusal;
64228
64645
  let base;
64229
64646
  const baseOf = current.def.base;
64230
64647
  const vars = resolveVars(config.workflowVars, config.rcVars, varOverrides, (yield* Host).env);
64648
+ const multiline = multilineSetting(vars);
64649
+ 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`);
64231
64650
  const template = yield* try_({
64232
64651
  try: () => baseOf?.(name, vars),
64233
64652
  catch: (e) => /* @__PURE__ */ new Error(`${commandLabel}: ${e instanceof Error ? e.message : String(e)}`)
@@ -64250,7 +64669,7 @@ const planEntry = (current, actor, entry) => gen(function* () {
64250
64669
  actor,
64251
64670
  to: name,
64252
64671
  ...base !== void 0 ? { reviewBase: base } : {},
64253
- vars: varOverrides
64672
+ vars
64254
64673
  })
64255
64674
  }
64256
64675
  }, {
@@ -76836,10 +77255,12 @@ const readStdin = () => tryPromise({
76836
77255
  /** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
76837
77256
  const runJudgeRunCommand = (command, out) => gen(function* () {
76838
77257
  const host = yield* Host;
77258
+ const { judge } = yield* loadRcConfig;
76839
77259
  const input = yield* readStdin();
76840
77260
  const verdict = yield* tryPromise({
76841
77261
  try: () => runJudge({
76842
77262
  provider: command.provider,
77263
+ configured: judge,
76843
77264
  answers: command.answers,
76844
77265
  model: command.model,
76845
77266
  cwd: host.scratchDir,
@@ -77407,7 +77828,11 @@ const FLAGS = [
77407
77828
  decode: ([raw]) => raw === "fixed" || raw === "jev" || raw === "llm" ? right(raw) : left(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
77408
77829
  scopeError: "gtd: --provider is only valid for `gtd judge run`",
77409
77830
  valueHint: "<name>",
77410
- help: ["(gtd judge run only) the answerer: fixed | jev | llm;", "omitted: jev when TYPESAFE_API_KEY is set, else llm"]
77831
+ help: [
77832
+ "(gtd judge run only) the answerer: fixed | jev | llm;",
77833
+ "omitted: GTD_JUDGE_PROVIDER, then .gtdrc judge:, then jev",
77834
+ "when TYPESAFE_API_KEY is set, else llm"
77835
+ ]
77411
77836
  },
77412
77837
  {
77413
77838
  name: "--answers",
@@ -77441,7 +77866,11 @@ const FLAGS = [
77441
77866
  decode: ([raw]) => raw === void 0 || raw.trim() === "" || /[\r\n]/.test(raw) ? left("gtd: --model must be a non-empty, single-line value") : right(raw),
77442
77867
  scopeError: "gtd: --model is only valid for `gtd land` (with --cost) or `gtd judge run`",
77443
77868
  valueHint: "<name>",
77444
- help: ["(gtd land, with --cost) tag that cost's model", "(gtd judge run, llm answerer) the claude model; default haiku"]
77869
+ help: [
77870
+ "(gtd land, with --cost) tag that cost's model",
77871
+ "(gtd judge run, llm answerer) the claude model; omitted:",
77872
+ "GTD_JUDGE_MODEL, then .gtdrc judge:, then haiku"
77873
+ ]
77445
77874
  },
77446
77875
  {
77447
77876
  name: "--entry",
@@ -77470,6 +77899,7 @@ const FLAGS = [
77470
77899
  if (eq <= 0) return left(`gtd: --var must be <name>=<value> with a non-empty name — got "${raw}"`);
77471
77900
  const name = raw.slice(0, eq);
77472
77901
  const value = raw.slice(eq + 1);
77902
+ if (!isSettingName(name)) return left(`gtd: --var ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`);
77473
77903
  if (/[\r\n]/.test(value)) return left(`gtd: --var ${name} must be a single-line value`);
77474
77904
  if (seen.has(name)) return left(`gtd: --var ${name} specified more than once`);
77475
77905
  seen.add(name);
@@ -77480,9 +77910,10 @@ const FLAGS = [
77480
77910
  scopeError: "gtd: --var requires --entry",
77481
77911
  valueHint: "<name>=<value>",
77482
77912
  help: [
77483
- "(with --entry; repeatable) supply a fixed variable",
77484
- "override for the new process; the name must already be",
77485
- "declared by the workflow's defaults or the .gtdrc vars:"
77913
+ "(with --entry; repeatable) pin a process setting for the",
77914
+ "new process; the name must already be declared by the",
77915
+ "workflow's defaults or the .gtdrc vars: (environment",
77916
+ "settings are not pinnable)"
77486
77917
  ]
77487
77918
  },
77488
77919
  {
@@ -77555,8 +77986,8 @@ const COMMAND_ROWS = [
77555
77986
  arity: "none",
77556
77987
  details: [
77557
77988
  "Scaffold a minimal .gtdrc.json for this repo, seeding the",
77558
- "default variables you are most likely to change (the test",
77559
- "command) and a Prettier formatting suggestion. gtd runs its",
77989
+ "environment setting you are most likely to change (the test",
77990
+ "command, under env:) and a Prettier formatting suggestion. gtd runs its",
77560
77991
  "built-in workflow by default, so no workflow is written —",
77561
77992
  "write a gtd.config.ts only to customize the workflow itself.",
77562
77993
  "Takes no argument. Run once per repo; refuses if a gtd",
@@ -77800,8 +78231,11 @@ const COMMAND_ROWS = [
77800
78231
  "does not cover are left out. --provider jev asks TypeSafe",
77801
78232
  "(TYPESAFE_API_KEY). --provider llm asks the `claude` CLI",
77802
78233
  "on PATH (Claude Code only), model haiku unless --model.",
77803
- "No --provider: jev when TYPESAFE_API_KEY is set and no",
77804
- "--model is given, else llm. Needs no repository. jev and",
78234
+ "No --provider: GTD_JUDGE_PROVIDER, then .gtdrc judge:",
78235
+ "provider, else jev when TYPESAFE_API_KEY is set and no",
78236
+ "model is given, else llm; --model likewise falls back to",
78237
+ "GTD_JUDGE_MODEL, then judge: model. Needs no repository,",
78238
+ "only .gtdrc files. jev and",
77805
78239
  "llm exit 1 when they cannot answer every question, with",
77806
78240
  "nothing on stdout"
77807
78241
  ]