@pmelab/gtd 58.2.0 → 59.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/.claude-plugin/plugin.json +1 -1
- package/README.md +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/twins/FlowScripts.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import {
|
|
3
|
-
conflictFor,
|
|
4
|
-
conflictTitle,
|
|
5
|
-
parseStandards,
|
|
6
|
-
renderStandards,
|
|
7
|
-
standardsBlock,
|
|
8
|
-
} from "./standards.js"
|
|
9
|
-
|
|
10
|
-
const conflict = { text: "Calculators are functions.", olds: ["Calculators are classes."] }
|
|
11
|
-
|
|
12
|
-
describe("standards file", () => {
|
|
13
|
-
it("round-trips decided and undecided standards", () => {
|
|
14
|
-
const standards = { decided: ["Prefer early returns."], undecided: [conflict] }
|
|
15
|
-
expect(parseStandards(renderStandards(standards))).toEqual(standards)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("reads a hand-edited file: any heading groups decided ones, wrapped lines join", () => {
|
|
19
|
-
const text = [
|
|
20
|
-
"# Standards",
|
|
21
|
-
"",
|
|
22
|
-
"Some prose.",
|
|
23
|
-
"",
|
|
24
|
-
"## Testing",
|
|
25
|
-
"",
|
|
26
|
-
"1. Every bug fix",
|
|
27
|
-
" gets a test.",
|
|
28
|
-
"",
|
|
29
|
-
"## Undecided",
|
|
30
|
-
"",
|
|
31
|
-
"- New",
|
|
32
|
-
" - contradicts: Old",
|
|
33
|
-
].join("\n")
|
|
34
|
-
expect(parseStandards(text)).toEqual({
|
|
35
|
-
decided: ["Every bug fix gets a test."],
|
|
36
|
-
undecided: [{ text: "New", olds: ["Old"] }],
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe("a formatter-wrapped file", () => {
|
|
42
|
-
it("keeps a wrapped contradicts line with its old standard", () => {
|
|
43
|
-
const text = [
|
|
44
|
-
"## Undecided",
|
|
45
|
-
"",
|
|
46
|
-
"- Calculators are plain functions. (re: subtract has no class)",
|
|
47
|
-
" - contradicts: Won't fix: subtract has no class — Calculators read better as",
|
|
48
|
-
" classes.",
|
|
49
|
-
].join("\n")
|
|
50
|
-
expect(parseStandards(text).undecided).toEqual([
|
|
51
|
-
{
|
|
52
|
-
text: "Calculators are plain functions. (re: subtract has no class)",
|
|
53
|
-
olds: ["Won't fix: subtract has no class — Calculators read better as classes."],
|
|
54
|
-
},
|
|
55
|
-
])
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe("conflict notes", () => {
|
|
60
|
-
it("keep their title when a formatter rewrites the emphasis", () => {
|
|
61
|
-
const standards = { decided: [], undecided: [{ text: "Use _named_ exports.", olds: [] }] }
|
|
62
|
-
expect(
|
|
63
|
-
conflictFor(standards, conflictTitle({ text: "Use *named* exports.", olds: [] })),
|
|
64
|
-
).toBeDefined()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it("are one per standard, however often the file lists it", () => {
|
|
68
|
-
const text = "## Undecided\n\n- New\n - contradicts: Old\n- New\n - contradicts: Old\n"
|
|
69
|
-
expect(parseStandards(text).undecided).toHaveLength(1)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it("title back to the conflict it asks about", () => {
|
|
73
|
-
const standards = { decided: [], undecided: [conflict] }
|
|
74
|
-
expect(conflictFor(standards, conflictTitle(conflict))).toBe(conflict)
|
|
75
|
-
expect(conflictFor(standards, "Standard conflict 00000000")).toBeUndefined()
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
describe("standardsBlock", () => {
|
|
80
|
-
it("is empty without decided standards", () => {
|
|
81
|
-
expect(standardsBlock(undefined)).toBe("")
|
|
82
|
-
expect(standardsBlock(renderStandards({ decided: [], undecided: [conflict] }))).toBe("")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it("lists only the decided ones", () => {
|
|
86
|
-
const block = standardsBlock(renderStandards({ decided: ["A."], undecided: [conflict] }))
|
|
87
|
-
expect(block).toContain("- A.")
|
|
88
|
-
expect(block).not.toContain(conflict.text)
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { ANSWER_LEAD, FREE_TEXT_PLACEHOLDER } from "../../steering/index.js"
|
|
2
|
-
|
|
3
|
-
export const STANDARDS = ".gtd/STANDARDS.md"
|
|
4
|
-
|
|
5
|
-
/** A learned standard that contradicts decided ones, waiting on the human. */
|
|
6
|
-
export interface Conflict {
|
|
7
|
-
readonly text: string
|
|
8
|
-
/** The decided standards it contradicts. */
|
|
9
|
-
readonly olds: readonly string[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Standards {
|
|
13
|
-
readonly decided: readonly string[]
|
|
14
|
-
readonly undecided: readonly Conflict[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const UNDECIDED = "Undecided"
|
|
18
|
-
|
|
19
|
-
// The same class as the review's `Gtd-Mark` anchors, so a title reads alike in both.
|
|
20
|
-
export const oneLine = (text: string): string =>
|
|
21
|
-
// eslint-disable-next-line no-control-regex -- the control range IS the point
|
|
22
|
-
text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim()
|
|
23
|
-
|
|
24
|
-
const ITEM = /^(?:[-*+]|\d+[.)])\s+(.*)$/
|
|
25
|
-
const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i
|
|
26
|
-
|
|
27
|
-
interface Item {
|
|
28
|
-
text: string
|
|
29
|
-
/** Set under `## Undecided`. */
|
|
30
|
-
olds?: string[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// A formatter wraps long lines: the tail belongs to the last `contradicts:` once there is one.
|
|
34
|
-
const wrapped = (item: Item, line: string): void => {
|
|
35
|
-
const olds = item.olds
|
|
36
|
-
if (olds !== undefined && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`
|
|
37
|
-
else item.text += ` ${line.trim()}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
|
|
41
|
-
export const standardKey = (text: string): string =>
|
|
42
|
-
oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase()
|
|
43
|
-
|
|
44
|
-
// Each standard once, so every conflict note gets a title of its own.
|
|
45
|
-
const toStandards = (items: readonly Item[]): Standards => {
|
|
46
|
-
const seen = new Set<string>()
|
|
47
|
-
const fresh = (text: string): boolean => {
|
|
48
|
-
const key = standardKey(text)
|
|
49
|
-
if (key === "" || seen.has(key)) return false
|
|
50
|
-
seen.add(key)
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
const decided = items.filter((i) => i.olds === undefined).map((i) => oneLine(i.text))
|
|
54
|
-
const undecided = items.flatMap((i) =>
|
|
55
|
-
i.olds === undefined
|
|
56
|
-
? []
|
|
57
|
-
: [{ text: oneLine(i.text), olds: i.olds.map(oneLine).filter((o) => o !== "") }],
|
|
58
|
-
)
|
|
59
|
-
return { decided: decided.filter(fresh), undecided: undecided.filter((c) => fresh(c.text)) }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Every top-level bullet is one standard, those under `## Undecided` waiting
|
|
64
|
-
* on the human; a hand-edited file may group the rest under any heading.
|
|
65
|
-
*/
|
|
66
|
-
export const parseStandards = (text: string): Standards => {
|
|
67
|
-
const items: Item[] = []
|
|
68
|
-
let pending = false
|
|
69
|
-
let last: Item | undefined
|
|
70
|
-
for (const line of text.split("\n")) {
|
|
71
|
-
const heading = /^(#{1,6})\s+(.*)$/.exec(line)
|
|
72
|
-
const item = ITEM.exec(line)
|
|
73
|
-
const old = CONTRADICTS.exec(line)
|
|
74
|
-
if (heading !== null) {
|
|
75
|
-
pending = heading[1]!.length > 1 && heading[2]!.trim() === UNDECIDED
|
|
76
|
-
last = undefined
|
|
77
|
-
} else if (item !== null) {
|
|
78
|
-
last = pending ? { text: item[1]!, olds: [] } : { text: item[1]! }
|
|
79
|
-
items.push(last)
|
|
80
|
-
} else if (old !== null && last?.olds !== undefined) last.olds.push(old[1]!)
|
|
81
|
-
else if (/^\s+\S/.test(line) && last !== undefined) wrapped(last, line)
|
|
82
|
-
else if (line.trim() !== "") last = undefined
|
|
83
|
-
}
|
|
84
|
-
return toStandards(items)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const renderStandards = ({ decided, undecided }: Standards): string => {
|
|
88
|
-
const parts = [
|
|
89
|
-
"# Standards",
|
|
90
|
-
"Kept from your review answers for every agent turn. One standard per bullet.",
|
|
91
|
-
...(decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")]),
|
|
92
|
-
...(undecided.length === 0
|
|
93
|
-
? []
|
|
94
|
-
: [
|
|
95
|
-
`## ${UNDECIDED}`,
|
|
96
|
-
undecided
|
|
97
|
-
.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n"))
|
|
98
|
-
.join("\n"),
|
|
99
|
-
]),
|
|
100
|
-
]
|
|
101
|
-
return `${parts.join("\n\n")}\n`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// FNV-1a over the key: a conflict's chunk title must survive markdown parsing and a
|
|
105
|
-
// formatter's rewrite of STANDARDS.md, which its text may not.
|
|
106
|
-
const hash = (raw: string): string => {
|
|
107
|
-
const text = standardKey(raw)
|
|
108
|
-
let h = 0x811c9dc5
|
|
109
|
-
for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 0x01000193)
|
|
110
|
-
return (h >>> 0).toString(16).padStart(8, "0")
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/
|
|
114
|
-
|
|
115
|
-
export const conflictTitle = (c: Conflict): string => `Standard conflict ${hash(c.text)}`
|
|
116
|
-
|
|
117
|
-
export const conflictFor = (standards: Standards, title: string): Conflict | undefined => {
|
|
118
|
-
const id = CONFLICT_TITLE.exec(title)?.[1]
|
|
119
|
-
return id === undefined ? undefined : standards.undecided.find((c) => hash(c.text) === id)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const isConflictTitle = (title: string): boolean => CONFLICT_TITLE.test(title)
|
|
123
|
-
|
|
124
|
-
const KEEP_NEW = "keep the new one (drop the old)"
|
|
125
|
-
const KEEP_OLD = "keep the old one (discard the new)"
|
|
126
|
-
|
|
127
|
-
/** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
|
|
128
|
-
export const conflictBody = (c: Conflict): string =>
|
|
129
|
-
[
|
|
130
|
-
...(c.olds.length === 0
|
|
131
|
-
? [
|
|
132
|
-
`A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`,
|
|
133
|
-
`New: ${c.text}`,
|
|
134
|
-
]
|
|
135
|
-
: [
|
|
136
|
-
`A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
|
|
137
|
-
`New: ${c.text}`,
|
|
138
|
-
`Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`,
|
|
139
|
-
]),
|
|
140
|
-
`${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`,
|
|
141
|
-
].join("\n\n")
|
|
142
|
-
|
|
143
|
-
/** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
|
|
144
|
-
export const standardsBlock = (text: string | undefined): string => {
|
|
145
|
-
const { decided } = parseStandards(text ?? "")
|
|
146
|
-
if (decided.length === 0) return ""
|
|
147
|
-
return `## Standards
|
|
148
|
-
|
|
149
|
-
\`${STANDARDS}\` holds the standards the human set in earlier reviews.
|
|
150
|
-
Follow every one in what you write. Reviewing, never raise a finding a
|
|
151
|
-
standard rules out — a \`Won't fix:\` standard is a finding the human
|
|
152
|
-
rejected, so never raise it or one like it again — and raise breaking a
|
|
153
|
-
standard as a finding.
|
|
154
|
-
|
|
155
|
-
${decided.map((s) => `- ${s}`).join("\n")}`
|
|
156
|
-
}
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { installContext, type ScopeOptions, type StepRequest } from "../../flows/index.js"
|
|
3
|
-
import { captureStep, fixtureContext } from "./text.fixture.js"
|
|
4
|
-
import * as steps from "./steps.js"
|
|
5
|
-
|
|
6
|
-
const capture = (
|
|
7
|
-
fn: () => Promise<void>,
|
|
8
|
-
scope: string,
|
|
9
|
-
skills?: Readonly<Record<string, readonly string[]>>,
|
|
10
|
-
): Promise<StepRequest> => captureStep(fn, { scope, ...(skills !== undefined ? { skills } : {}) })
|
|
11
|
-
|
|
12
|
-
const agentPrompt = (request: StepRequest): string => {
|
|
13
|
-
if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
|
|
14
|
-
return request.prompt
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe("the bundled workflow's steps declare skills — a bundled step's rendered prompt carries its bundled skills.ts preamble", () => {
|
|
18
|
-
it("fix carries build.fix's bundled skills", async () => {
|
|
19
|
-
const prompt = agentPrompt(await capture(() => steps.fixCheck(), "build"))
|
|
20
|
-
expect(prompt).toContain("debugging-and-error-recovery")
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("describeEscalation carries health.describe's bundled skills", async () => {
|
|
24
|
-
const prompt = agentPrompt(await capture(() => steps.describeEscalation(), "build"))
|
|
25
|
-
expect(prompt).toContain("debugging-and-error-recovery")
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it("reviewQuality names its lens scope, build.quality.<lens>.reviewing", async () => {
|
|
29
|
-
const request = await capture(() => steps.reviewQuality("owasp-security"), "build")
|
|
30
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
31
|
-
expect(request.name).toBe("quality.owasp-security.reviewing")
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it("an unknown lens's preamble names the lens itself, from the bundled per-lens key", async () => {
|
|
35
|
-
const prompt = agentPrompt(await capture(() => steps.reviewQuality("owasp-security"), "build"))
|
|
36
|
-
expect(prompt).toContain("missing one: owasp-security")
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it("a configured build.quality.<lens> entry replaces the lens's skills in the preamble, every other lens untouched", async () => {
|
|
40
|
-
const skills = { "build.quality.owasp-security": ["my-org-checklist"] }
|
|
41
|
-
const prompt = agentPrompt(
|
|
42
|
-
await capture(() => steps.reviewQuality("owasp-security"), "build", skills),
|
|
43
|
-
)
|
|
44
|
-
expect(prompt).toContain("my-org-checklist")
|
|
45
|
-
expect(prompt).toContain("owasp-security")
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it("correctness carries all four trace points", async () => {
|
|
49
|
-
const request = await capture(() => steps.reviewQuality("correctness"), "build")
|
|
50
|
-
const body = JSON.stringify(request)
|
|
51
|
-
for (const point of [
|
|
52
|
-
"partial-failure",
|
|
53
|
-
"format, not just its presence",
|
|
54
|
-
"parallel write paths",
|
|
55
|
-
"contract test",
|
|
56
|
-
])
|
|
57
|
-
expect(body).toContain(point)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it.each(["conventions", "spec-challenge", "criteria-coverage"])(
|
|
61
|
-
"%s carries its brief",
|
|
62
|
-
async (lens) => {
|
|
63
|
-
const request = await capture(() => steps.reviewQuality(lens), "build")
|
|
64
|
-
expect(JSON.stringify(request)).toContain(
|
|
65
|
-
steps.builtInLenses[lens]!.brief.split("\n")[0]!.slice(0, 40),
|
|
66
|
-
)
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
it("an unknown lens has no brief", async () => {
|
|
71
|
-
const request = await capture(() => steps.reviewQuality("my-lens"), "build")
|
|
72
|
-
expect(JSON.stringify(request)).not.toContain("This lens's brief")
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it("fixQuality carries build.fix.quality's bundled skills", async () => {
|
|
76
|
-
const prompt = agentPrompt(await capture(() => steps.fixQuality(), "build"))
|
|
77
|
-
expect(prompt).toContain("incremental-implementation, code-simplification")
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it("reviewing carries build.review.reviewing's bundled skills", async () => {
|
|
81
|
-
const prompt = agentPrompt(await capture(() => steps.reviewing("base"), "build"))
|
|
82
|
-
expect(prompt).toContain("code-review-and-quality")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it("an empty configured list leaves that step's skills empty, and its rendered prompt bare", async () => {
|
|
86
|
-
const request = await capture(() => steps.fixCheck(), "build", { build: [] })
|
|
87
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
88
|
-
expect(request.prompt).not.toContain("Load whatever's listed here")
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it("fixRisks fixes every risk, tolerates an empty turn, and leaves .gtd/REVIEW.md alone", async () => {
|
|
92
|
-
const request = await capture(
|
|
93
|
-
() => steps.fixRisks([{ id: "risk-1", anchor: "calc ./a.ts#1-1", text: "Risk: drops it" }]),
|
|
94
|
-
"build",
|
|
95
|
-
)
|
|
96
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
97
|
-
expect(request.name).toBe("review.fix.risks.fixing")
|
|
98
|
-
expect(request.options).toMatchObject({ allowEmpty: true })
|
|
99
|
-
expect(request.prompt).toContain("debugging-and-error-recovery, incremental-implementation")
|
|
100
|
-
expect(request.prompt).toContain("risk-1 — calc ./a.ts#1-1")
|
|
101
|
-
expect(request.prompt).toContain("Risk: drops it")
|
|
102
|
-
expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it("reviewing names the carry-over commit only when given one", async () => {
|
|
106
|
-
const bare = await capture(() => steps.reviewing("base"), "build")
|
|
107
|
-
const carried = await capture(() => steps.reviewing("base", "abc1234"), "build")
|
|
108
|
-
if (bare.kind !== "agent" || carried.kind !== "agent") throw new Error("unreachable")
|
|
109
|
-
expect(bare.prompt).not.toContain("Carry-over")
|
|
110
|
-
expect(carried.prompt).toContain("Carry-over: commit `abc1234`")
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe("the bundled workflow's model hints", () => {
|
|
115
|
-
const modelOf = async (
|
|
116
|
-
fn: () => Promise<void>,
|
|
117
|
-
context: Parameters<typeof captureStep>[1] = {},
|
|
118
|
-
): Promise<string | undefined> => {
|
|
119
|
-
const request = await captureStep(fn, { scope: "build", ...context })
|
|
120
|
-
if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
|
|
121
|
-
return request.options.model
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
it.each([
|
|
125
|
-
["correctness", "smart"],
|
|
126
|
-
["owasp-security", "smart"],
|
|
127
|
-
["spec-challenge", "smart"],
|
|
128
|
-
["conventions", "base"],
|
|
129
|
-
["ponytail-review", "base"],
|
|
130
|
-
["test-audit", "base"],
|
|
131
|
-
["my-lens", "smart"],
|
|
132
|
-
])("lens %s defaults to %s", async (lens, model) => {
|
|
133
|
-
expect(await modelOf(() => steps.reviewQuality(lens))).toBe(model)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it("a lens's default follows the planner or coder setting it stands for", async () => {
|
|
137
|
-
const env = { plannerModel: "opus", coderModel: "sonnet" }
|
|
138
|
-
expect(await modelOf(() => steps.reviewQuality("correctness"), { env })).toBe("opus")
|
|
139
|
-
expect(await modelOf(() => steps.reviewQuality("conventions"), { env })).toBe("sonnet")
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it("a model passed in wins over the lens's default; a blank one is none", async () => {
|
|
143
|
-
expect(await modelOf(() => steps.reviewQuality("correctness", undefined, "base"))).toBe("base")
|
|
144
|
-
expect(await modelOf(() => steps.reviewQuality("conventions", undefined, "opus"))).toBe("opus")
|
|
145
|
-
expect(await modelOf(() => steps.reviewQuality("conventions", undefined, ""))).toBe("base")
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it("a lens named like an Object member still gets its default", async () => {
|
|
149
|
-
expect(await modelOf(() => steps.reviewQuality("constructor"))).toBe("smart")
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it("the fix loop runs on the coder model, base by default", async () => {
|
|
153
|
-
expect(await modelOf(() => steps.fixCheck())).toBe("base")
|
|
154
|
-
expect(await modelOf(() => steps.fixQuality())).toBe("base")
|
|
155
|
-
expect(await modelOf(() => steps.fixCheck(), { env: { coderModel: "sonnet" } })).toBe("sonnet")
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
|
|
159
|
-
const request = await capture(
|
|
160
|
-
() =>
|
|
161
|
-
steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
|
|
162
|
-
"build",
|
|
163
|
-
)
|
|
164
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
165
|
-
expect(request.name).toBe("review.answer-review-questions")
|
|
166
|
-
expect(request.options).toMatchObject({
|
|
167
|
-
file: ".gtd/REVIEW.md",
|
|
168
|
-
mode: "review",
|
|
169
|
-
})
|
|
170
|
-
expect(request.prompt).toContain("code-review-and-quality")
|
|
171
|
-
expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
|
|
172
|
-
expect(request.prompt).toContain("`A: ` line")
|
|
173
|
-
expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
|
|
177
|
-
const request = await capture(
|
|
178
|
-
() =>
|
|
179
|
-
steps.fixNits([
|
|
180
|
-
{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
|
|
181
|
-
{ id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
|
|
182
|
-
]),
|
|
183
|
-
"build",
|
|
184
|
-
)
|
|
185
|
-
if (request.kind !== "agent") throw new Error("unreachable")
|
|
186
|
-
expect(request.name).toBe("review.fix.nits.fixing")
|
|
187
|
-
expect(request.prompt).toContain("incremental-implementation, code-simplification")
|
|
188
|
-
expect(request.prompt).toContain("typo")
|
|
189
|
-
expect(request.prompt).toContain("semicolon")
|
|
190
|
-
expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it("fixNits with access runs in a review.fix.nits scope, the step named fixing inside it", async () => {
|
|
194
|
-
const scopes: ScopeOptions[] = []
|
|
195
|
-
const steps_: StepRequest[] = []
|
|
196
|
-
const access = { write: ["src/a.ts", ".gtd/**"] }
|
|
197
|
-
installContext(
|
|
198
|
-
fixtureContext(
|
|
199
|
-
{ scope: "refactor.build" },
|
|
200
|
-
{
|
|
201
|
-
step: (request) => {
|
|
202
|
-
steps_.push(request)
|
|
203
|
-
return Promise.resolve()
|
|
204
|
-
},
|
|
205
|
-
pushScope: (s) => void scopes.push(s),
|
|
206
|
-
popScope: () => undefined,
|
|
207
|
-
changesSince: () => [],
|
|
208
|
-
},
|
|
209
|
-
),
|
|
210
|
-
)
|
|
211
|
-
try {
|
|
212
|
-
await steps.fixNits([], access)
|
|
213
|
-
} finally {
|
|
214
|
-
installContext(undefined)
|
|
215
|
-
}
|
|
216
|
-
expect(scopes).toEqual([{ name: "review.fix.nits", access }])
|
|
217
|
-
expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
|
|
218
|
-
})
|
|
219
|
-
})
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { changesSince, env, human, scope, type ScopeAccess } from "../../flows/index.js"
|
|
2
|
-
import { removedAssertions, type CollectRound } from "../../workflow-kit/index.js"
|
|
3
|
-
import * as t from "./text.js"
|
|
4
|
-
|
|
5
|
-
// The bundled workflow's single steps. A step's name is relative to the
|
|
6
|
-
// scope its caller runs it in — `triage` inside `scope("design")` is
|
|
7
|
-
// `design.triage` — and the full name is public API: renaming it strands
|
|
8
|
-
// every process resting on it.
|
|
9
|
-
|
|
10
|
-
export const FEEDBACK = ".gtd/FEEDBACK.md"
|
|
11
|
-
export const ESCALATION = ".gtd/ESCALATION.md"
|
|
12
|
-
export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
|
|
13
|
-
export const REVIEW = ".gtd/REVIEW.md"
|
|
14
|
-
export const REFACTOR = ".gtd/REFACTOR.md"
|
|
15
|
-
export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
|
|
16
|
-
export const QUALITY = ".gtd/QUALITY.md"
|
|
17
|
-
export const NAIVE_TEST = ".gtd/NAIVE-TEST.md"
|
|
18
|
-
|
|
19
|
-
export const planner = (): string => env.plannerModel ?? ""
|
|
20
|
-
export const coder = (): string => env.coderModel ?? ""
|
|
21
|
-
|
|
22
|
-
// ── Keeping the suite green ─────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
export const fixCheck = (rules?: string): Promise<void> =>
|
|
25
|
-
t.agentWithSkills("fix", t.buildFixPrompt(rules), {
|
|
26
|
-
label: "Fixing the check",
|
|
27
|
-
file: FEEDBACK,
|
|
28
|
-
model: coder(),
|
|
29
|
-
system: t.finisherSystem(),
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
export const describeEscalation = (rules?: string): Promise<void> =>
|
|
33
|
-
t.agentWithSkills("health.describe", t.healthDescribePrompt(rules), {
|
|
34
|
-
label: "Describing the escalation",
|
|
35
|
-
file: FEEDBACK,
|
|
36
|
-
model: coder(),
|
|
37
|
-
system: t.escalationSystem(),
|
|
38
|
-
allowEmpty: true,
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
export const escalate = (): Promise<void> =>
|
|
42
|
-
human("health.stop", {
|
|
43
|
-
message: t.healthStopMessage(),
|
|
44
|
-
label: "Escalating to a human",
|
|
45
|
-
file: ESCALATION,
|
|
46
|
-
acceptClean: true,
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
export const escalationExhausted = (): Promise<void> =>
|
|
50
|
-
human("health.exhausted", {
|
|
51
|
-
message: t.healthExhaustedMessage(),
|
|
52
|
-
label: "Escalation exhausted",
|
|
53
|
-
file: ESCALATION,
|
|
54
|
-
acceptClean: true,
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// ── Quality and review ──────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export interface BuiltInLens {
|
|
60
|
-
readonly skills: readonly string[]
|
|
61
|
-
readonly brief: string
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Lenses the workflow defines itself, not bundled skills: `skills/` is not in the npm package, and a missing lens skill burns a turn silently. */
|
|
65
|
-
export const builtInLenses: Readonly<Record<string, BuiltInLens>> = {
|
|
66
|
-
correctness: { skills: ["code-review-and-quality"], brief: t.correctnessBrief },
|
|
67
|
-
conventions: { skills: [], brief: t.conventionsBrief },
|
|
68
|
-
"spec-challenge": { skills: [], brief: t.specChallengeBrief },
|
|
69
|
-
"criteria-coverage": { skills: [], brief: t.criteriaCoverageBrief },
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const coderLenses: ReadonlySet<string> = new Set(["conventions", "ponytail-review", "test-audit"])
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* One quality review through `lens`; each lens is its own scope, so its own
|
|
76
|
-
* conversation and skills. `requirements`, when given, are quoted in the prompt.
|
|
77
|
-
*/
|
|
78
|
-
export const reviewQuality = (
|
|
79
|
-
lens: string,
|
|
80
|
-
base?: string,
|
|
81
|
-
model?: string,
|
|
82
|
-
requirements?: string,
|
|
83
|
-
): Promise<void> =>
|
|
84
|
-
t.agentWithSkills(
|
|
85
|
-
`quality.${lens}.reviewing`,
|
|
86
|
-
t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base, requirements),
|
|
87
|
-
{
|
|
88
|
-
label: "Reviewing (one quality lens)",
|
|
89
|
-
file: QUALITY,
|
|
90
|
-
model: model || (coderLenses.has(lens) ? coder() : planner()),
|
|
91
|
-
system: t.reviewerSystem(),
|
|
92
|
-
allowEmpty: true,
|
|
93
|
-
},
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
export const fixQuality = (rules?: string): Promise<void> =>
|
|
97
|
-
t.agentWithSkills("fix.quality.fixing", t.buildFixQualityPrompt(rules), {
|
|
98
|
-
label: "Fixing quality findings",
|
|
99
|
-
file: QUALITY,
|
|
100
|
-
model: coder(),
|
|
101
|
-
system: t.finisherSystem(),
|
|
102
|
-
allowEmpty: true,
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given; `extra` joins the prompt. */
|
|
106
|
-
export const reviewing = (
|
|
107
|
-
base: string,
|
|
108
|
-
carry?: string,
|
|
109
|
-
lap: t.ReviewRange = "first",
|
|
110
|
-
extra = "",
|
|
111
|
-
): Promise<void> =>
|
|
112
|
-
t.agentWithSkills(
|
|
113
|
-
"review.reviewing",
|
|
114
|
-
t.buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base)), extra),
|
|
115
|
-
{
|
|
116
|
-
label: "Reviewing",
|
|
117
|
-
file: REVIEW,
|
|
118
|
-
mode: "review",
|
|
119
|
-
model: planner(),
|
|
120
|
-
system: t.reviewerSystem(),
|
|
121
|
-
base,
|
|
122
|
-
},
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
126
|
-
export const fixRisks = (
|
|
127
|
-
notes: readonly t.NoteInput[],
|
|
128
|
-
access?: ScopeAccess,
|
|
129
|
-
rules?: string,
|
|
130
|
-
): Promise<void> => {
|
|
131
|
-
const fixing = (name: string): Promise<void> =>
|
|
132
|
-
t.agentWithSkills(name, t.buildReviewFixRisksPrompt(notes, rules), {
|
|
133
|
-
label: "Fixing the reviewer's risks",
|
|
134
|
-
file: REVIEW,
|
|
135
|
-
model: planner(),
|
|
136
|
-
system: t.reviewerSystem(),
|
|
137
|
-
allowEmpty: true,
|
|
138
|
-
})
|
|
139
|
-
return access === undefined
|
|
140
|
-
? fixing("review.fix.risks.fixing")
|
|
141
|
-
: scope({ name: "review.fix.risks", access }, () => fixing("fixing"))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export const awaitReview = (
|
|
145
|
-
base: string,
|
|
146
|
-
message = t.buildReviewAwaitReviewMessage(base),
|
|
147
|
-
): Promise<void> =>
|
|
148
|
-
human("review.await-review", {
|
|
149
|
-
message,
|
|
150
|
-
label: "Awaiting your review",
|
|
151
|
-
file: REVIEW,
|
|
152
|
-
mode: "review",
|
|
153
|
-
base,
|
|
154
|
-
acceptClean: true,
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
158
|
-
export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
|
|
159
|
-
t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
|
|
160
|
-
label: "Answering your questions",
|
|
161
|
-
file: REVIEW,
|
|
162
|
-
mode: "review",
|
|
163
|
-
model: planner(),
|
|
164
|
-
system: t.reviewerSystem(),
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
/** Fix every `nit` note in one turn. */
|
|
168
|
-
export const fixNits = (
|
|
169
|
-
notes: readonly t.NoteInput[],
|
|
170
|
-
access?: ScopeAccess,
|
|
171
|
-
rules?: string,
|
|
172
|
-
): Promise<void> => {
|
|
173
|
-
const fixing = (name: string): Promise<void> =>
|
|
174
|
-
t.agentWithSkills(name, t.buildReviewFixNitsPrompt(notes, rules), {
|
|
175
|
-
label: "Fixing your nits",
|
|
176
|
-
file: REVIEW,
|
|
177
|
-
model: planner(),
|
|
178
|
-
system: t.reviewerSystem(),
|
|
179
|
-
})
|
|
180
|
-
return access === undefined
|
|
181
|
-
? fixing("review.fix.nits.fixing")
|
|
182
|
-
: scope({ name: "review.fix.nits", access }, () => fixing("fixing"))
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
186
|
-
export const collecting = (capture: string, round?: CollectRound): Promise<void> =>
|
|
187
|
-
t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture, undefined, round), {
|
|
188
|
-
label: "Collecting your feedback",
|
|
189
|
-
file: REQUIREMENTS,
|
|
190
|
-
mode: "qa",
|
|
191
|
-
model: planner(),
|
|
192
|
-
system: t.reviewerSystem(),
|
|
193
|
-
allowEmpty: true,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
197
|
-
export const reviewMissing = (round: string): Promise<void> =>
|
|
198
|
-
human("review.review-missing", {
|
|
199
|
-
message: t.buildReviewReviewMissingMessage(round),
|
|
200
|
-
label: "Nothing to review",
|
|
201
|
-
})
|