@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 +4 -1
- package/dist/gtd.bundle.mjs +271 -62
- package/package.json +1 -1
- package/schema.json +10 -0
- package/src/flows/runtime.ts +13 -0
- package/src/workflows/review.ts +6 -2
- package/src/workflows/skills.test.ts +93 -0
- package/src/workflows/skills.ts +37 -0
- package/src/workflows/steps.test.ts +75 -68
- package/src/workflows/steps.ts +47 -61
- package/src/workflows/text.fixture.ts +36 -0
- package/src/workflows/text.test.ts +21 -28
- package/src/workflows/text.ts +19 -23
- package/src/workflows/unified.ts +1 -0
- package/src/workflows/vars.ts +7 -9
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 —
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -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
|
|
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
|
|
48248
|
-
*
|
|
48249
|
-
*
|
|
48250
|
-
*
|
|
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,
|
|
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
|
|
48903
|
-
|
|
48904
|
-
|
|
48905
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
/**
|
|
49012
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
|
49532
|
-
const qualityLenses = () =>
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
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
|
|
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`;
|