@pmelab/gtd 15.9.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 +22 -9
- package/dist/gtd.bundle.mjs +619 -270
- 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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type FlowContext,
|
|
5
5
|
type StepRequest,
|
|
6
6
|
} from "../flows/index.js"
|
|
7
|
+
import { skills as bundledSkills } from "./skills.js"
|
|
7
8
|
import { defaults } from "./vars.js"
|
|
8
9
|
|
|
9
10
|
export interface TextContext {
|
|
@@ -12,12 +13,46 @@ export interface TextContext {
|
|
|
12
13
|
readonly start?: string
|
|
13
14
|
readonly codeThreads?: readonly CodeThreadInfo[]
|
|
14
15
|
readonly read?: (path: string) => string | undefined
|
|
16
|
+
/**
|
|
17
|
+
* A PER-KEY `skillsFor` override, keyed by the LOCAL name a bundled step
|
|
18
|
+
* calls itself with (`captureStep`/`renderText` push no scopes, so a local
|
|
19
|
+
* name is also the only name here) — stands in for a `.gtdrc` entry, so
|
|
20
|
+
* only the key it names is overridden; every other step still falls back
|
|
21
|
+
* to the call's own `skills` option, then the bundled map (`./skills.ts`),
|
|
22
|
+
* so an existing step test keeps its skills without having to declare them
|
|
23
|
+
* itself.
|
|
24
|
+
*/
|
|
25
|
+
readonly skills?: Readonly<Record<string, readonly string[]>>
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
const unavailable = (): never => {
|
|
18
29
|
throw new Error("not available while rendering a text outside a replay")
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
/** The bundled map is keyed by FULL name, but a bundled step calls itself by its own LOCAL name and this fixture pushes no scope to prefix it with — so the default lookup matches on the local name alone, or as the last `.`-separated segment of a bundled key. `health.describe`'s two bundled entries (`packages.item.health.describe`, `build.health.describe`) share one list, so the ambiguity this leaves resolves to the same answer either way. */
|
|
33
|
+
const bundledSkillsFor = (localName: string): readonly string[] => {
|
|
34
|
+
const exact = bundledSkills[localName]
|
|
35
|
+
if (exact !== undefined) return exact
|
|
36
|
+
const suffix = `.${localName}`
|
|
37
|
+
const hit = Object.entries(bundledSkills).find(([key]) => key.endsWith(suffix))
|
|
38
|
+
return hit?.[1] ?? []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mirrors `Replay.ts`'s `resolveSkills` three-way precedence: `context.skills`
|
|
43
|
+
* (a per-key override, standing in for `.gtdrc`) beats `ownSkills` (a call's
|
|
44
|
+
* own `skills` option), which beats the bundled default — so a step test that
|
|
45
|
+
* passes its own `skills` option (like `reviewQuality`'s lens) sees it in the
|
|
46
|
+
* preamble too, the same as a real replay would.
|
|
47
|
+
*/
|
|
48
|
+
const skillsForOf =
|
|
49
|
+
(context: TextContext) =>
|
|
50
|
+
(localName: string, ownSkills?: readonly string[]): readonly string[] => {
|
|
51
|
+
const configured = context.skills?.[localName]
|
|
52
|
+
if (configured !== undefined) return configured
|
|
53
|
+
return ownSkills ?? bundledSkillsFor(localName)
|
|
54
|
+
}
|
|
55
|
+
|
|
21
56
|
/** The fixed replay context texts render against; steps, refusals and scopes are unavailable unless overridden. */
|
|
22
57
|
export const fixtureContext = (
|
|
23
58
|
context: TextContext = {},
|
|
@@ -40,6 +75,7 @@ export const fixtureContext = (
|
|
|
40
75
|
vars: { ...defaults, ...context.vars },
|
|
41
76
|
head: () => context.head ?? "",
|
|
42
77
|
start: () => context.start ?? "",
|
|
78
|
+
skillsFor: skillsForOf(context),
|
|
43
79
|
...overrides,
|
|
44
80
|
})
|
|
45
81
|
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
buildReviewCollectingPrompt,
|
|
9
9
|
designGateAnswerMessage,
|
|
10
10
|
designTriagePrompt,
|
|
11
|
-
splitSkills,
|
|
12
11
|
summaryPrompt,
|
|
13
12
|
withSkills,
|
|
14
13
|
} from "./text.js"
|
|
@@ -30,42 +29,36 @@ describe("withSkills", () => {
|
|
|
30
29
|
})
|
|
31
30
|
})
|
|
32
31
|
|
|
33
|
-
describe("splitSkills", () => {
|
|
34
|
-
it("splits on comma, trims each name, drops empty entries", () => {
|
|
35
|
-
expect(splitSkills("a, b")).toEqual(["a", "b"])
|
|
36
|
-
expect(splitSkills(" a ,, b ")).toEqual(["a", "b"])
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it("returns [] for undefined, empty, or blank input", () => {
|
|
40
|
-
expect(splitSkills(undefined)).toEqual([])
|
|
41
|
-
expect(splitSkills("")).toEqual([])
|
|
42
|
-
expect(splitSkills(" ")).toEqual([])
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
32
|
describe("agentWithSkills", () => {
|
|
47
|
-
const capture = (fn: () => Promise<void
|
|
33
|
+
const capture = (fn: () => Promise<void>, skills?: Readonly<Record<string, readonly string[]>>) =>
|
|
34
|
+
captureStep(fn, skills !== undefined ? { skills } : {})
|
|
48
35
|
|
|
49
|
-
it("
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
it("puts skillsFor(name)'s list, joined, in the preamble", async () => {
|
|
37
|
+
const request = await capture(() => agentWithSkills("name", "do-the-work"), {
|
|
38
|
+
name: ["code-review", "testing"],
|
|
39
|
+
})
|
|
40
|
+
if (request.kind !== "agent") throw new Error("unreachable")
|
|
41
|
+
expect(request.prompt).toBe(withSkills("code-review, testing", "do-the-work"))
|
|
54
42
|
})
|
|
55
43
|
|
|
56
|
-
it("passes
|
|
57
|
-
const request = await capture(() =>
|
|
58
|
-
agentWithSkills("name", "code-review, testing", "do-the-work"),
|
|
59
|
-
)
|
|
44
|
+
it("passes no skills option itself — the replay resolver fills the wire field", async () => {
|
|
45
|
+
const request = await capture(() => agentWithSkills("name", "do-the-work"))
|
|
60
46
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
61
|
-
expect(request.options.skills).
|
|
47
|
+
expect(request.options.skills).toBeUndefined()
|
|
62
48
|
})
|
|
63
49
|
|
|
64
|
-
it("leaves the prompt bare
|
|
65
|
-
const request = await capture(() => agentWithSkills("name",
|
|
50
|
+
it("leaves the prompt bare when the name resolves to no configured skills", async () => {
|
|
51
|
+
const request = await capture(() => agentWithSkills("name", "do-the-work"), { name: [] })
|
|
66
52
|
if (request.kind !== "agent") throw new Error("unreachable")
|
|
67
53
|
expect(request.prompt).toBe("do-the-work")
|
|
68
|
-
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("still accepts an explicit options.skills of its own, untouched", async () => {
|
|
57
|
+
const request = await capture(() =>
|
|
58
|
+
agentWithSkills("name", "do-the-work", { skills: ["own-skill"] }),
|
|
59
|
+
)
|
|
60
|
+
if (request.kind !== "agent") throw new Error("unreachable")
|
|
61
|
+
expect(request.options.skills).toEqual(["own-skill"])
|
|
69
62
|
})
|
|
70
63
|
})
|
|
71
64
|
|
package/src/workflows/text.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
agent,
|
|
3
3
|
codeThreads,
|
|
4
4
|
head,
|
|
5
|
+
skillsFor,
|
|
5
6
|
start,
|
|
6
7
|
vars,
|
|
7
8
|
type AgentOptions,
|
|
@@ -36,31 +37,23 @@ export const withSkills = (skills: string | undefined, prompt: string): string =
|
|
|
36
37
|
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** `skills:` split on `,`, each name trimmed, empty entries dropped — `[]` for `undefined`, `""`, or blank. */
|
|
40
|
-
export const splitSkills = (raw: string | undefined): readonly string[] =>
|
|
41
|
-
(raw ?? "")
|
|
42
|
-
.split(",")
|
|
43
|
-
.map((name) => name.trim())
|
|
44
|
-
.filter((name) => name.length > 0)
|
|
45
|
-
|
|
46
40
|
/**
|
|
47
|
-
* An `agent()` step
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
41
|
+
* An `agent()` step whose preamble names its resolved skills: `skillsFor`
|
|
42
|
+
* resolves `name` (scoped from here, same as `agent()`'s own resolver) for
|
|
43
|
+
* the preamble prose, passing `options.skills` through as `ownSkills` — the
|
|
44
|
+
* same precedence tier `agent()`'s own wire resolver gives a call's own
|
|
45
|
+
* `skills` option, so a step that declares one (`reviewQuality`'s per-turn
|
|
46
|
+
* lens) gets it in the preamble too, not just on the wire, whenever no
|
|
47
|
+
* bundled or configured entry outranks it. Both reads share one resolver
|
|
48
|
+
* (`Replay.ts`'s `resolveSkills`), so the preamble and the wire can't drift
|
|
49
|
+
* apart.
|
|
51
50
|
*/
|
|
52
51
|
export const agentWithSkills = (
|
|
53
52
|
name: string,
|
|
54
|
-
skills: string | undefined,
|
|
55
53
|
prompt: string,
|
|
56
54
|
options: AgentOptions = {},
|
|
57
|
-
): Promise<void> =>
|
|
58
|
-
|
|
59
|
-
return agent(name, withSkills(list.join(", "), prompt), {
|
|
60
|
-
...options,
|
|
61
|
-
skills: list,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
55
|
+
): Promise<void> =>
|
|
56
|
+
agent(name, withSkills(skillsFor(name, options.skills).join(", "), prompt), options)
|
|
64
57
|
|
|
65
58
|
export const unwindFailure = (commit: string): string =>
|
|
66
59
|
`gtd could not unwind ${commit} out of your working tree.`
|
|
@@ -814,10 +807,13 @@ every commit is machine-authored.
|
|
|
814
807
|
high-level architectural changes — never which files changed; \`git
|
|
815
808
|
diff --stat\` is for that, not this message
|
|
816
809
|
- If the range removes or renames a documented config key, CLI flag, or
|
|
817
|
-
workflow state
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
810
|
+
workflow state, OR makes a previously-accepted config value now fail to
|
|
811
|
+
load (a stricter validation, an escape a value must now spell differently)
|
|
812
|
+
— a public surface, not an internal one, either way — add a literal
|
|
813
|
+
\`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
|
|
814
|
+
the exact escape or rewrite a committed \`.gtdrc\` needs. A \`!\` on the
|
|
815
|
+
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
816
|
+
own commit-analyzer has missed that marker before, cutting no release
|
|
821
817
|
until a follow-up commit carried the literal footer instead
|
|
822
818
|
|
|
823
819
|
The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
|
package/src/workflows/unified.ts
CHANGED
|
@@ -32,6 +32,7 @@ import * as t from "./text.js"
|
|
|
32
32
|
|
|
33
33
|
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
34
34
|
export { defaults } from "./vars.js"
|
|
35
|
+
export { skills } from "./skills.js"
|
|
35
36
|
export { agentWithSkills } from "./text.js"
|
|
36
37
|
export * from "./steps.js"
|
|
37
38
|
export * from "./health.js"
|
package/src/workflows/vars.ts
CHANGED
|
@@ -10,14 +10,12 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
10
10
|
specPreJudge: "0.9",
|
|
11
11
|
reviewNoteActionable: "0.7",
|
|
12
12
|
architectureSkipMinP: "0.85",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
specReviewSkills: "code-review-and-quality, spec-driven-development",
|
|
21
|
-
escalateSkills: "debugging-and-error-recovery",
|
|
13
|
+
// The per-step skill lists formerly declared here as `*Skills` vars now
|
|
14
|
+
// live in `./skills.ts`, addressed per step by `.gtdrc` `skills:`.
|
|
15
|
+
// `qualityReviews` is the one exception: it fans out into one turn per
|
|
16
|
+
// entry (`qualityLenses`, in `./review.ts`) rather than naming one step's
|
|
17
|
+
// skill list, so it stays a var — pairing with `build.quality.reviewing`'s
|
|
18
|
+
// entry in `./skills.ts`, which replaces the lens on every one of those
|
|
19
|
+
// turns.
|
|
22
20
|
qualityReviews: "owasp-security, ponytail-review, test-audit",
|
|
23
21
|
}
|