@pmelab/gtd 15.10.0 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,10 @@ for the settings most projects tune.
26
26
  Also install all three skill sources the bundled workflow names in its prompts —
27
27
  none is optional. See
28
28
  [Setup](https://github.com/pmelab/gtd/blob/main/docs/setup.md) for the list, the
29
- install flags, and the never-install-the-plugin caveat.
29
+ install flags, and the never-install-the-plugin caveat. Each bundled agent step
30
+ has its own addressable `.gtdrc` `skills:` entry — see
31
+ [Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#the-skills-key)
32
+ to repoint one to a set your own harness has instead.
30
33
 
31
34
  > **A repository's `gtd.config.ts` is code, and gtd runs it.** A custom workflow
32
35
  > is a TypeScript module, and every gtd command that looks at workflow state —
@@ -30966,6 +30966,8 @@ 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 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);
30969
30971
  /** The merged workflow variables. */
30970
30972
  const vars = new Proxy({}, {
30971
30973
  get: (_target, key) => typeof key === "string" ? ctx().vars[key] : void 0,
@@ -31185,6 +31187,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
31185
31187
  scope: () => scope,
31186
31188
  sectionBodies: () => sectionBodies,
31187
31189
  sections: () => sections,
31190
+ skillsFor: () => skillsFor,
31188
31191
  start: () => start,
31189
31192
  threads: () => threads,
31190
31193
  vars: () => vars,
@@ -47773,7 +47776,11 @@ const replay = async (input) => {
47773
47776
  truncated: judgeCuts.get(request) ?? []
47774
47777
  } : void 0;
47775
47778
  const judgeCuts = /* @__PURE__ */ new WeakMap();
47776
- const resolve = (request) => {
47779
+ const resolveSkills = (name, ownSkills) => {
47780
+ const resolved = input.configuredSkills?.[name] ?? ownSkills ?? input.skills?.[name];
47781
+ return resolved !== void 0 && resolved.length === 0 ? void 0 : resolved;
47782
+ };
47783
+ const resolve = (request, name) => {
47777
47784
  if (request.kind === "agent") {
47778
47785
  const persona = {};
47779
47786
  for (const s of scopes) {
@@ -47784,7 +47791,8 @@ const replay = async (input) => {
47784
47791
  ...request,
47785
47792
  options: {
47786
47793
  ...persona,
47787
- ...request.options
47794
+ ...request.options,
47795
+ skills: resolveSkills(name, request.options.skills)
47788
47796
  }
47789
47797
  };
47790
47798
  }
@@ -47803,7 +47811,7 @@ const replay = async (input) => {
47803
47811
  const name = scoped(request.name);
47804
47812
  const occurrence = (occurrences.get(name) ?? 0) + 1;
47805
47813
  occurrences.set(name, occurrence);
47806
- const resolved = resolve(request);
47814
+ const resolved = resolve(request, name);
47807
47815
  const reached = {
47808
47816
  id: {
47809
47817
  name,
@@ -47968,6 +47976,7 @@ const replay = async (input) => {
47968
47976
  }),
47969
47977
  vars: input.vars,
47970
47978
  start: () => input.start,
47979
+ skillsFor: (localName, ownSkills) => resolveSkills(scoped(localName), ownSkills) ?? [],
47971
47980
  head: () => {
47972
47981
  const rest = commits.slice(cursor);
47973
47982
  return rest.length > 0 && rest.every((c) => c.parsed.step === void 0) ? rest[rest.length - 1].hash : position.hash;
@@ -48241,21 +48250,18 @@ const withSkills = (skills, prompt) => {
48241
48250
  if (skills === void 0 || skills.trim() === "") return prompt;
48242
48251
  return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`;
48243
48252
  };
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
48253
  /**
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.
48254
+ * An `agent()` step whose preamble names its resolved skills: `skillsFor`
48255
+ * resolves `name` (scoped from here, same as `agent()`'s own resolver) for
48256
+ * the preamble prose, passing `options.skills` through as `ownSkills` — the
48257
+ * same precedence tier `agent()`'s own wire resolver gives a call's own
48258
+ * `skills` option, so a step that declares one (`reviewQuality`'s per-turn
48259
+ * lens) gets it in the preamble too, not just on the wire, whenever no
48260
+ * bundled or configured entry outranks it. Both reads share one resolver
48261
+ * (`Replay.ts`'s `resolveSkills`), so the preamble and the wire can't drift
48262
+ * apart.
48251
48263
  */
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
- };
48264
+ const agentWithSkills = (name, prompt, options = {}) => agent(name, withSkills(skillsFor(name, options.skills).join(", "), prompt), options);
48259
48265
  const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
48260
48266
  const idleMessage = () => `No active gtd process.
48261
48267
 
@@ -48899,10 +48905,13 @@ every commit is machine-authored.
48899
48905
  high-level architectural changes — never which files changed; \`git
48900
48906
  diff --stat\` is for that, not this message
48901
48907
  - 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
48908
+ workflow state, OR makes a previously-accepted config value now fail to
48909
+ load (a stricter validation, an escape a value must now spell differently)
48910
+ — a public surface, not an internal one, either way — add a literal
48911
+ \`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
48912
+ the exact escape or rewrite a committed \`.gtdrc\` needs. A \`!\` on the
48913
+ type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
48914
+ own commit-analyzer has missed that marker before, cutting no release
48906
48915
  until a follow-up commit carried the literal footer instead
48907
48916
 
48908
48917
  The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
@@ -48926,21 +48935,21 @@ const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md";
48926
48935
  const planner = () => vars.plannerModel ?? "";
48927
48936
  const coder = () => vars.coderModel ?? "";
48928
48937
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
48929
- const triage$1 = (base) => agentWithSkills("triage", vars.triageSkills, designTriagePrompt(base), {
48938
+ const triage$1 = (base) => agentWithSkills("triage", designTriagePrompt(base), {
48930
48939
  label: "Triaging the change",
48931
48940
  file: REQUIREMENTS,
48932
48941
  mode: "qa",
48933
48942
  model: planner(),
48934
48943
  system: designSystem()
48935
48944
  });
48936
- const author = () => agentWithSkills("author", vars.architectureSkills, architectureAuthorPrompt(), {
48945
+ const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
48937
48946
  label: "Refining the technical plan",
48938
48947
  file: ARCHITECTURE,
48939
48948
  mode: "qa",
48940
48949
  model: planner(),
48941
48950
  system: architectSystem()
48942
48951
  });
48943
- const decompose = () => agentWithSkills("decompose", vars.decomposeSkills, architectureDecomposePrompt(), {
48952
+ const decompose = () => agentWithSkills("decompose", architectureDecomposePrompt(), {
48944
48953
  label: "Decomposing into packages",
48945
48954
  model: planner(),
48946
48955
  system: architectSystem()
@@ -48959,37 +48968,37 @@ const answerTechnicalQuestions = () => human("gate.answer", {
48959
48968
  mode: "qa",
48960
48969
  acceptClean: true
48961
48970
  });
48962
- const build = (pkg) => agentWithSkills("building", vars.buildSkills, packagesItemBuildingPrompt(pkg), {
48971
+ const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
48963
48972
  label: "Building",
48964
48973
  model: coder(),
48965
48974
  system: builderSystem()
48966
48975
  });
48967
- const fixSuite = () => agentWithSkills("fix-suite", vars.fixSkills, packagesItemFixSuitePrompt(), {
48976
+ const fixSuite = () => agentWithSkills("fix-suite", packagesItemFixSuitePrompt(), {
48968
48977
  label: "Fixing the check",
48969
48978
  file: FEEDBACK,
48970
48979
  model: coder(),
48971
48980
  system: builderSystem()
48972
48981
  });
48973
- const fixSpec = (pkg) => agentWithSkills("fix-spec", vars.reviewFixSkills, packagesItemFixSpecPrompt(pkg), {
48982
+ const fixSpec = (pkg) => agentWithSkills("fix-spec", packagesItemFixSpecPrompt(pkg), {
48974
48983
  label: "Fixing review feedback",
48975
48984
  file: SPEC_FEEDBACK,
48976
48985
  model: coder(),
48977
48986
  system: builderSystem()
48978
48987
  });
48979
48988
  /** 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), {
48989
+ const reviewPackage = (pkg, failing = []) => agentWithSkills("spec.review", packagesItemSpecReviewPrompt(pkg, failing), {
48981
48990
  label: "Reviewing the package",
48982
48991
  model: planner(),
48983
48992
  system: specReviewerSystem(),
48984
48993
  allowEmpty: true
48985
48994
  });
48986
- const fix = () => agentWithSkills("fix", vars.fixSkills, buildFixPrompt(), {
48995
+ const fix = () => agentWithSkills("fix", buildFixPrompt(), {
48987
48996
  label: "Fixing the check",
48988
48997
  file: FEEDBACK,
48989
48998
  model: coder(),
48990
48999
  system: finisherSystem()
48991
49000
  });
48992
- const describeEscalation = () => agentWithSkills("health.describe", vars.escalateSkills, healthDescribePrompt(), {
49001
+ const describeEscalation = () => agentWithSkills("health.describe", healthDescribePrompt(), {
48993
49002
  label: "Describing the escalation",
48994
49003
  file: FEEDBACK,
48995
49004
  model: coder(),
@@ -49008,15 +49017,21 @@ const escalationExhausted = () => human("health.exhausted", {
49008
49017
  file: ESCALATION,
49009
49018
  acceptClean: true
49010
49019
  });
49011
- /** One quality review, through the skill `lens`. */
49012
- const reviewQuality = (lens) => agentWithSkills("quality.reviewing", lens, buildQualityReviewingPrompt(lens), {
49020
+ /**
49021
+ * One quality review, through the skill `lens`. `lens` rides as this turn's
49022
+ * own `skills` option — a `.gtdrc` `build.quality.reviewing` entry still
49023
+ * overrides it (config beats a flow-supplied list same as any other step),
49024
+ * but absent one the lens itself is what the turn loads by default.
49025
+ */
49026
+ const reviewQuality = (lens) => agentWithSkills("quality.reviewing", buildQualityReviewingPrompt(lens), {
49013
49027
  label: "Reviewing (one quality lens)",
49014
49028
  file: QUALITY,
49015
49029
  model: planner(),
49016
49030
  system: reviewerSystem(),
49017
- allowEmpty: true
49031
+ allowEmpty: true,
49032
+ skills: [lens]
49018
49033
  });
49019
- const fixQuality = () => agentWithSkills("fix-quality", vars.reviewFixSkills, buildFixQualityPrompt(), {
49034
+ const fixQuality = () => agentWithSkills("fix-quality", buildFixQualityPrompt(), {
49020
49035
  label: "Fixing quality findings",
49021
49036
  file: QUALITY,
49022
49037
  model: coder(),
@@ -49024,7 +49039,7 @@ const fixQuality = () => agentWithSkills("fix-quality", vars.reviewFixSkills, bu
49024
49039
  allowEmpty: true
49025
49040
  });
49026
49041
  /** 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), {
49042
+ const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
49028
49043
  label: "Reviewing",
49029
49044
  file: REVIEW,
49030
49045
  mode: "review",
@@ -49033,7 +49048,7 @@ const reviewing = (base, carry) => agentWithSkills("review.reviewing", vars.revi
49033
49048
  base
49034
49049
  });
49035
49050
  /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
49036
- const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", vars.reviewSkills, buildReviewAnswerQuestionsPrompt(notes), {
49051
+ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
49037
49052
  label: "Answering your questions",
49038
49053
  file: REVIEW,
49039
49054
  mode: "review",
@@ -49044,7 +49059,7 @@ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-q
49044
49059
  * Fix every `nit` note in one turn. Its name puts it in the `build.review`
49045
49060
  * conversation, which has one identity — so it runs as the reviewer, not the coder.
49046
49061
  */
49047
- const fixNits = (notes) => agentWithSkills("review.fix-nits", vars.reviewFixSkills, buildReviewFixNitsPrompt(notes), {
49062
+ const fixNits = (notes) => agentWithSkills("review.fix-nits", buildReviewFixNitsPrompt(notes), {
49048
49063
  label: "Fixing your nits",
49049
49064
  file: REVIEW,
49050
49065
  model: planner(),
@@ -49063,7 +49078,7 @@ const reviewMissing = (round) => human("review.review-missing", {
49063
49078
  label: "Nothing to review"
49064
49079
  });
49065
49080
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
49066
- const collecting = (capture) => agentWithSkills("review.collecting", vars.reviewSkills, buildReviewCollectingPrompt(capture), {
49081
+ const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
49067
49082
  label: "Collecting your feedback",
49068
49083
  file: REQUIREMENTS,
49069
49084
  mode: "qa",
@@ -49528,8 +49543,8 @@ const architecturePass = async () => {
49528
49543
  };
49529
49544
  //#endregion
49530
49545
  //#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);
49546
+ /** 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. */
49547
+ const qualityLenses = () => (vars.qualityReviews ?? "").split(",").map((lens) => lens.trim()).filter((lens) => lens.length > 0);
49533
49548
  /**
49534
49549
  * One review turn per lens over the whole change, each appending what it
49535
49550
  * finds blocking to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
@@ -49761,18 +49776,33 @@ const defaults = {
49761
49776
  specPreJudge: "0.9",
49762
49777
  reviewNoteActionable: "0.7",
49763
49778
  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
49779
  qualityReviews: "owasp-security, ponytail-review, test-audit"
49774
49780
  };
49775
49781
  //#endregion
49782
+ //#region src/workflows/skills.ts
49783
+ const skills = {
49784
+ "design.triage": ["spec-driven-development", "planning-and-task-breakdown"],
49785
+ "architecture.author": [
49786
+ "api-and-interface-design",
49787
+ "documentation-and-adrs",
49788
+ "ponytail"
49789
+ ],
49790
+ "architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
49791
+ "packages.item.building": ["test-driven-development", "incremental-implementation"],
49792
+ "packages.item.fix-suite": ["debugging-and-error-recovery"],
49793
+ "packages.item.fix-spec": ["incremental-implementation", "code-simplification"],
49794
+ "packages.item.spec.review": ["code-review-and-quality", "spec-driven-development"],
49795
+ "packages.item.health.describe": ["debugging-and-error-recovery"],
49796
+ "build.fix": ["debugging-and-error-recovery"],
49797
+ "build.health.describe": ["debugging-and-error-recovery"],
49798
+ "build.quality.reviewing": [],
49799
+ "build.fix-quality": ["incremental-implementation", "code-simplification"],
49800
+ "build.review.reviewing": ["code-review-and-quality"],
49801
+ "build.review.answer-review-questions": ["code-review-and-quality"],
49802
+ "build.review.fix-nits": ["incremental-implementation", "code-simplification"],
49803
+ "build.review.collecting": ["code-review-and-quality"]
49804
+ };
49805
+ //#endregion
49776
49806
  //#region src/workflows/unified.ts
49777
49807
  var unified_exports = /* @__PURE__ */ __exportAll({
49778
49808
  ARCHITECTURE: () => ARCHITECTURE,
@@ -49827,6 +49857,7 @@ var unified_exports = /* @__PURE__ */ __exportAll({
49827
49857
  reviewPackage: () => reviewPackage,
49828
49858
  reviewQuality: () => reviewQuality,
49829
49859
  reviewing: () => reviewing,
49860
+ skills: () => skills,
49830
49861
  specReview: () => specReview,
49831
49862
  steering: () => steering,
49832
49863
  summary: () => summary,
@@ -50643,6 +50674,15 @@ const modesJsonSchema = {
50643
50674
  }
50644
50675
  }
50645
50676
  };
50677
+ /** 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. */
50678
+ const skillsJsonSchema = {
50679
+ type: "object",
50680
+ 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.",
50681
+ additionalProperties: {
50682
+ type: "array",
50683
+ items: { type: "string" }
50684
+ }
50685
+ };
50646
50686
  /**
50647
50687
  * A real (not `Unknown`) schema, unlike `vars`/`modes`: it is a flat settings
50648
50688
  * struct with no per-mode map to compile, so its JSON Schema derives from the
@@ -50663,7 +50703,8 @@ const UiSchema = Struct({
50663
50703
  const ConfigSchema = Struct({
50664
50704
  vars: optional$1(Unknown.annotations({ jsonSchema: varsJsonSchema })),
50665
50705
  modes: optional$1(Unknown.annotations({ jsonSchema: modesJsonSchema })),
50666
- ui: optional$1(UiSchema)
50706
+ ui: optional$1(UiSchema),
50707
+ skills: optional$1(Unknown.annotations({ jsonSchema: skillsJsonSchema }))
50667
50708
  });
50668
50709
  //#endregion
50669
50710
  //#region src/SteeringFormats.ts
@@ -50736,7 +50777,7 @@ const dedupeDiagnostics = (diagnostics) => {
50736
50777
  };
50737
50778
  //#endregion
50738
50779
  //#region src/workflow/compile.ts
50739
- const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50780
+ const isPlainObject$3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
50740
50781
  const describeType = (v) => {
50741
50782
  if (v === null) return "null";
50742
50783
  if (Array.isArray(v)) return "array";
@@ -50756,7 +50797,7 @@ const compileVarsMap = (raw) => {
50756
50797
  vars: {},
50757
50798
  diagnostics
50758
50799
  };
50759
- if (!isPlainObject$2(raw)) {
50800
+ if (!isPlainObject$3(raw)) {
50760
50801
  diagnostics.push(err(["vars"], `"vars" must be a mapping of name -> scalar value, got ${describeType(raw)}`));
50761
50802
  return {
50762
50803
  vars: {},
@@ -50776,10 +50817,75 @@ const compileVarsMap = (raw) => {
50776
50817
  diagnostics
50777
50818
  };
50778
50819
  };
50820
+ const isStringArray$1 = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
50821
+ /** A flat `step full name -> skill list` map; a malformed value is a load error and is dropped. */
50822
+ const compileSkillsMap = (raw) => {
50823
+ const diagnostics = [];
50824
+ if (raw === void 0) return {
50825
+ skills: {},
50826
+ diagnostics
50827
+ };
50828
+ if (!isPlainObject$3(raw)) {
50829
+ diagnostics.push(err(["skills"], `"skills" must be a mapping of step name -> array of skill names, got ${describeType(raw)}`));
50830
+ return {
50831
+ skills: {},
50832
+ diagnostics
50833
+ };
50834
+ }
50835
+ const skills = Object.create(null);
50836
+ for (const [key, value] of Object.entries(raw)) {
50837
+ if (!isStringArray$1(value)) {
50838
+ diagnostics.push(err(["skills", key], `"skills.${key}" must be an array of skill names (strings), got ${describeType(value)}`));
50839
+ continue;
50840
+ }
50841
+ skills[key] = value;
50842
+ }
50843
+ return {
50844
+ skills,
50845
+ diagnostics
50846
+ };
50847
+ };
50848
+ /**
50849
+ * The nine `*Skills` workflow vars this package deletes, each naming the
50850
+ * `.gtdrc` `skills:` step key(s) that replace it — four of the nine fan out to
50851
+ * several keys, because a var once shared between steps now addresses
50852
+ * them independently. Deliberately the only place gtd special-cases a var
50853
+ * name by string; meant to be deleted a major release after this ships.
50854
+ */
50855
+ const DEAD_SKILLS_VARS = {
50856
+ triageSkills: ["design.triage"],
50857
+ architectureSkills: ["architecture.author"],
50858
+ decomposeSkills: ["architecture.decompose"],
50859
+ buildSkills: ["packages.item.building"],
50860
+ fixSkills: ["packages.item.fix-suite", "build.fix"],
50861
+ reviewFixSkills: [
50862
+ "packages.item.fix-spec",
50863
+ "build.fix-quality",
50864
+ "build.review.fix-nits"
50865
+ ],
50866
+ reviewSkills: [
50867
+ "build.review.reviewing",
50868
+ "build.review.answer-review-questions",
50869
+ "build.review.collecting"
50870
+ ],
50871
+ specReviewSkills: ["packages.item.spec.review"],
50872
+ escalateSkills: ["packages.item.health.describe", "build.health.describe"]
50873
+ };
50874
+ /** `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. */
50875
+ const deadSkillsVarDiagnostics = (rawVars) => {
50876
+ if (!isPlainObject$3(rawVars)) return [];
50877
+ const diagnostics = [];
50878
+ for (const key of Object.keys(rawVars)) {
50879
+ const replacement = DEAD_SKILLS_VARS[key];
50880
+ if (replacement === void 0) continue;
50881
+ diagnostics.push(err(["vars", key], `"vars.${key}" was removed — skills are now configured per step, under the "skills:" key, addressed by: ${replacement.join(", ")}`));
50882
+ }
50883
+ return diagnostics;
50884
+ };
50779
50885
  const MODE_COMMAND_KEYS = ["format", "validate"];
50780
50886
  /** One `modes:` entry; malformed entries are load errors and are dropped. */
50781
50887
  const compileMode = (name, entry, diagnostics) => {
50782
- if (!isPlainObject$2(entry)) {
50888
+ if (!isPlainObject$3(entry)) {
50783
50889
  diagnostics.push(err(["modes", name], `mode "${name}": must be an object with "format" and/or "validate", got ${describeType(entry)}`));
50784
50890
  return;
50785
50891
  }
@@ -50809,7 +50915,7 @@ const compileModesMap = (raw) => {
50809
50915
  modes: {},
50810
50916
  diagnostics
50811
50917
  };
50812
- if (!isPlainObject$2(raw)) {
50918
+ if (!isPlainObject$3(raw)) {
50813
50919
  diagnostics.push(err(["modes"], `"modes" must be a mapping of mode name -> { format, validate }, got ${describeType(raw)}`));
50814
50920
  return {
50815
50921
  modes: {},
@@ -50838,7 +50944,7 @@ const mergeModes = (base, override) => {
50838
50944
  };
50839
50945
  /** 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
50946
  const uniformOrigin = (value, origin) => {
50841
- if (!isPlainObject$2(value)) return { self: origin };
50947
+ if (!isPlainObject$3(value)) return { self: origin };
50842
50948
  const children = {};
50843
50949
  for (const [key, v] of Object.entries(value)) children[key] = uniformOrigin(v, origin);
50844
50950
  return {
@@ -50856,7 +50962,7 @@ const uniformOrigin = (value, origin) => {
50856
50962
  * findings, not one origin for the whole merged key.
50857
50963
  */
50858
50964
  const mergeWithOrigin = (base, baseOrigin, inner, innerOrigin) => {
50859
- if (!isPlainObject$2(base) || !isPlainObject$2(inner)) return {
50965
+ if (!isPlainObject$3(base) || !isPlainObject$3(inner)) return {
50860
50966
  value: inner,
50861
50967
  origin: uniformOrigin(inner, innerOrigin)
50862
50968
  };
@@ -50907,7 +51013,7 @@ const compileConfig = (layers) => {
50907
51013
  let merged = {};
50908
51014
  let originTree = { self: BUILT_IN_ORIGIN };
50909
51015
  for (const layer of layers) {
50910
- const value = isPlainObject$2(layer.value) ? layer.value : {};
51016
+ const value = isPlainObject$3(layer.value) ? layer.value : {};
50911
51017
  const step = mergeWithOrigin(merged, originTree, value, layer.origin);
50912
51018
  merged = step.value;
50913
51019
  originTree = step.origin;
@@ -50917,17 +51023,91 @@ const compileConfig = (layers) => {
50917
51023
  const ui = mergedConfig["ui"];
50918
51024
  const { vars: rcVars, diagnostics: varsDiagnostics } = compileVarsMap(mergedConfig["vars"]);
50919
51025
  diagnostics.push(...withOrigin(varsDiagnostics, lookupIn));
51026
+ diagnostics.push(...withOrigin(deadSkillsVarDiagnostics(mergedConfig["vars"]), lookupIn));
50920
51027
  const { modes: rcModes, diagnostics: modesDiagnostics } = compileModesMap(mergedConfig["modes"]);
50921
51028
  diagnostics.push(...withOrigin(modesDiagnostics, lookupIn));
51029
+ const skillsKeys = [];
51030
+ for (const layer of layers) {
51031
+ if (!isPlainObject$3(layer.value)) continue;
51032
+ const own = compileSkillsMap(layer.value["skills"]);
51033
+ diagnostics.push(...own.diagnostics.map((d) => ({
51034
+ ...d,
51035
+ origin: layer.origin
51036
+ })));
51037
+ skillsKeys.push(...Object.keys(own.skills).map((key) => ({
51038
+ key,
51039
+ origin: layer.origin
51040
+ })));
51041
+ }
51042
+ const { skills: rcSkills } = compileSkillsMap(mergedConfig["skills"]);
50922
51043
  const seeded = Object.fromEntries(BUILT_IN_MODE_NAMES.map((name) => [name, { validate: seededValidateCommand(name) }]));
50923
51044
  return {
50924
51045
  rcVars,
50925
51046
  modes: mergeModes(seeded, rcModes),
50926
51047
  ...ui !== void 0 ? { ui } : {},
51048
+ rcSkills,
51049
+ skillsKeys,
50927
51050
  diagnostics: dedupeDiagnostics(sortDiagnostics(diagnostics, layerOrder))
50928
51051
  };
50929
51052
  };
50930
51053
  //#endregion
51054
+ //#region src/workflow/interpolate.ts
51055
+ const VAR_PATTERN = /\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
51056
+ const isPlainObject$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
51057
+ /**
51058
+ * `modes.*.format`, `modes.*.validate` and `ui.format` — gtd hands these
51059
+ * straight to bash, where `$` already means the shell's own expansion. The
51060
+ * predicate reads only the config PATH shape, never the value: sniffing for
51061
+ * `$GTD_FILE` would exempt whichever command happens not to name it, and
51062
+ * silently interpolate one that names something else instead.
51063
+ */
51064
+ const isBashExemptPath = (path) => {
51065
+ if (path.length === 3 && path[0] === "modes" && (path[2] === "format" || path[2] === "validate")) return true;
51066
+ if (path.length === 2 && path[0] === "ui" && path[1] === "format") return true;
51067
+ return false;
51068
+ };
51069
+ const interpolateString = (value, path, env, diagnostics) => value.replace(VAR_PATTERN, (match, braced, bare) => {
51070
+ if (match === "$$") return "$";
51071
+ const name = braced ?? bare;
51072
+ if (name === void 0) return match;
51073
+ const resolved = env[name];
51074
+ if (resolved !== void 0) return resolved;
51075
+ diagnostics.push({
51076
+ severity: "error",
51077
+ path,
51078
+ message: `"${match}" references environment variable "${name}", which is not set`,
51079
+ origin: ""
51080
+ });
51081
+ return match;
51082
+ });
51083
+ const walk = (value, path, env, diagnostics) => {
51084
+ if (typeof value === "string") return isBashExemptPath(path) ? value : interpolateString(value, path, env, diagnostics);
51085
+ if (Array.isArray(value)) return value.map((v, i) => walk(v, [...path, i], env, diagnostics));
51086
+ if (isPlainObject$2(value)) {
51087
+ const out = {};
51088
+ for (const [key, v] of Object.entries(value)) out[key] = walk(v, [...path, key], env, diagnostics);
51089
+ return out;
51090
+ }
51091
+ return value;
51092
+ };
51093
+ /**
51094
+ * Expand `$NAME`/`${NAME}` from `env` over every string leaf of a decoded
51095
+ * `.gtdrc` layer, except the three paths gtd hands to bash. `$$` yields a
51096
+ * literal `$`; any other `$` not followed by a valid name (a trailing `$`,
51097
+ * `$1`, `$-`, `$ `) is left untouched. Exactly one pass: `String.replace`
51098
+ * never rescans a replacement's own text, so an environment variable's value
51099
+ * can never be read back as config syntax. An unset name is a load-error
51100
+ * `Diagnostic` naming the config path; the original `$NAME` text is left in
51101
+ * place rather than blanked.
51102
+ */
51103
+ const interpolate = (config, env) => {
51104
+ const diagnostics = [];
51105
+ return {
51106
+ value: walk(config, [], env, diagnostics),
51107
+ diagnostics
51108
+ };
51109
+ };
51110
+ //#endregion
50931
51111
  //#region src/workflow/vars.ts
50932
51112
  const PREFIX = "GTD_";
50933
51113
  /**
@@ -57718,7 +57898,7 @@ const dropOptionalUndefinedArtifacts = (all) => {
57718
57898
  * findings surface together through `load`'s single sorted/deduped report,
57719
57899
  * the same as every other producer in this package.
57720
57900
  */
57721
- const decodeLevel = (level) => gen(function* () {
57901
+ const decodeLevel = (level, env) => gen(function* () {
57722
57902
  if (!isPlainObject$1(level.config)) return {
57723
57903
  layer: void 0,
57724
57904
  diagnostics: [{
@@ -57739,13 +57919,17 @@ const decodeLevel = (level) => gen(function* () {
57739
57919
  origin: level.filepath
57740
57920
  }))
57741
57921
  };
57922
+ const interpolated = interpolate(result.right, env);
57742
57923
  return {
57743
57924
  layer: {
57744
57925
  origin: level.filepath,
57745
57926
  dir: dirname(level.filepath),
57746
- value: result.right
57927
+ value: interpolated.value
57747
57928
  },
57748
- diagnostics: []
57929
+ diagnostics: interpolated.diagnostics.map((d) => ({
57930
+ ...d,
57931
+ origin: level.filepath
57932
+ }))
57749
57933
  };
57750
57934
  });
57751
57935
  /**
@@ -57769,7 +57953,7 @@ const load = gen(function* () {
57769
57953
  const discovery = yield* ConfigDiscovery;
57770
57954
  const levels = yield* discovery.levels(host.root, host.home);
57771
57955
  for (const level of levels) yield* narrator.narrate(`config: layer ${level.filepath}`);
57772
- const decoded = yield* forEach$2(levels, decodeLevel);
57956
+ const decoded = yield* forEach$2(levels, (level) => decodeLevel(level, host.env));
57773
57957
  const layers = decoded.flatMap((d) => d.layer !== void 0 ? [d.layer] : []);
57774
57958
  const decodeDiagnostics = decoded.flatMap((d) => d.diagnostics);
57775
57959
  const compiled = compileConfig(layers);
@@ -57779,7 +57963,18 @@ const load = gen(function* () {
57779
57963
  catch: (e) => new GtdError(`gtd config:\n - ${module?.filepath ?? "(built-in default)"}: ${e instanceof Error ? e.message : String(e)}`)
57780
57964
  });
57781
57965
  const layerOrder = [...levels.map((level) => level.filepath), ...module ? [module.filepath] : []];
57782
- const diagnostics = dedupeDiagnostics(sortDiagnostics([...decodeDiagnostics, ...compiled.diagnostics], layerOrder));
57966
+ const knownSkillNames = Object.keys(loaded.skills).sort();
57967
+ const unknownSkillDiagnostics = compiled.skillsKeys.filter(({ key }) => !Object.hasOwn(loaded.skills, key)).map(({ key, origin }) => ({
57968
+ severity: "error",
57969
+ path: ["skills", key],
57970
+ message: `"skills.${key}" names a step this workflow does not declare — known step names: ${knownSkillNames.join(", ")}`,
57971
+ origin
57972
+ }));
57973
+ const diagnostics = dedupeDiagnostics(sortDiagnostics([
57974
+ ...decodeDiagnostics,
57975
+ ...compiled.diagnostics,
57976
+ ...unknownSkillDiagnostics
57977
+ ], layerOrder));
57783
57978
  const fatal = diagnostics.filter((d) => d.severity === "error");
57784
57979
  if (fatal.length > 0) {
57785
57980
  const lines = fatal.map(formatDiagnostic);
@@ -57793,6 +57988,8 @@ const load = gen(function* () {
57793
57988
  base: loaded.base,
57794
57989
  steering: loaded.steering,
57795
57990
  modes: compiled.modes,
57991
+ skills: loaded.skills,
57992
+ configuredSkills: compiled.rcSkills,
57796
57993
  initial
57797
57994
  },
57798
57995
  workflowVars: { ...loaded.defaults },
@@ -57839,6 +58036,13 @@ const stringRecord = (exports, name) => {
57839
58036
  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
58037
  return value;
57841
58038
  };
58039
+ const isStringArray = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
58040
+ /** Read a module's optional `skills` export: full step name -> skill list, mirroring `stringRecord` for the vars-shaped exports. */
58041
+ const skillsRecord = (exports) => {
58042
+ const value = exports["skills"] ?? {};
58043
+ 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`);
58044
+ return value;
58045
+ };
57842
58046
  const optionalFunction = (exports, name) => {
57843
58047
  const value = exports[name];
57844
58048
  if (value === void 0) return void 0;
@@ -57857,6 +58061,7 @@ const fromModule = (exported, origin, dependencyFiles) => {
57857
58061
  flow,
57858
58062
  defaults: stringRecord(exports, "defaults"),
57859
58063
  steering: stringRecord(exports, "steering"),
58064
+ skills: skillsRecord(exports),
57860
58065
  summary: optionalFunction(exports, "summary"),
57861
58066
  base: optionalFunction(exports, "base"),
57862
58067
  origin,
@@ -63775,6 +63980,8 @@ const replayFor = (setup, pending) => replay({
63775
63980
  start: setup.run.diffBase,
63776
63981
  startTree: setup.run.diffBase === EMPTY_TREE$1 ? treeFromRecord({}) : commitTree(setup.workspace, setup.run.diffBase),
63777
63982
  budgetBytes: setup.budget,
63983
+ skills: setup.def.skills,
63984
+ configuredSkills: setup.def.configuredSkills,
63778
63985
  ...pending !== void 0 ? { pending } : {}
63779
63986
  });
63780
63987
  const replayError = (outcome) => {
@@ -63996,7 +64203,9 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
63996
64203
  },
63997
64204
  vars,
63998
64205
  start: "",
63999
- budgetBytes: rest.setup.budget
64206
+ budgetBytes: rest.setup.budget,
64207
+ skills: rest.def.skills,
64208
+ configuredSkills: rest.def.configuredSkills
64000
64209
  }));
64001
64210
  if (outcome.kind === "refused") return outcome.message;
64002
64211
  if (outcome.kind === "rest") return outcome.entryRead ? void 0 : `"${name}" is not an enterable state — this workflow reads no entry`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "15.10.0",
3
+ "version": "16.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {