@pmelab/gtd 24.4.0 → 25.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 +13 -18
- package/dist/chunks/{reserved-B9pG93Fc.mjs → reserved-6pAgFGgB.mjs} +69 -125
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +3 -3
- package/dist/workflow-kit.mjs +47 -90
- package/dist/workflows/feature.mjs +25 -47
- package/dist/workflows/fix.mjs +1 -1
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +135 -261
- package/package.json +1 -2
- package/src/replay/Replay.ts +2 -1
- package/src/workflow-kit/scenarios.test.ts +116 -0
- package/src/workflow-kit/scenarios.ts +53 -113
- package/src/workflows/feature/feature.ts +2 -5
- package/src/workflows/feature/packages.test.ts +17 -34
- package/src/workflows/feature/packages.ts +13 -52
- package/src/workflows/feature/planning.ts +1 -1
- package/src/workflows/feature/review.test.ts +0 -47
- package/src/workflows/feature/review.ts +6 -10
- package/src/workflows/index.ts +0 -2
- package/src/workflows/shared/health.test.ts +1 -21
- package/src/workflows/shared/health.ts +4 -39
- package/src/workflows/shared/options.ts +0 -1
- package/src/workflows/shared/steps.ts +16 -11
- package/src/workflows/shared/tail.test.ts +1 -41
- package/src/workflows/shared/tail.ts +14 -30
- package/src/workflows/shared/text.fixture.ts +1 -0
- package/src/workflows/shared/text.test.ts +23 -41
- package/src/workflows/shared/text.ts +54 -74
- package/src/workflows/shared/vars.ts +0 -1
- package/src/workflows/shared/scenarios.test.ts +0 -256
- package/src/workflows/shared/scenarios.ts +0 -35
package/src/workflows/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ export { ShippedOptions } from "./shared/options.js"
|
|
|
9
9
|
export { agentWithSkills } from "./shared/text.js"
|
|
10
10
|
export * from "./shared/steps.js"
|
|
11
11
|
export * from "./shared/health.js"
|
|
12
|
-
export * from "./shared/scenarios.js"
|
|
13
12
|
export {
|
|
14
13
|
closeReview,
|
|
15
14
|
fixQualityFindings,
|
|
@@ -18,7 +17,6 @@ export {
|
|
|
18
17
|
qualityLap,
|
|
19
18
|
REVIEW_ORIG,
|
|
20
19
|
tail,
|
|
21
|
-
type TailPlan,
|
|
22
20
|
} from "./shared/tail.js"
|
|
23
21
|
export { lensesOf } from "../workflow-kit/index.js"
|
|
24
22
|
export * from "./feature/steps.js"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { toRequest } from "../../judges/index.js"
|
|
3
|
-
import {
|
|
4
|
-
import { renderText } from "./text.fixture.js"
|
|
3
|
+
import { retryQuestion } from "./health.js"
|
|
5
4
|
|
|
6
5
|
describe("retryQuestion", () => {
|
|
7
6
|
it.each([true, false])("is sendable to jev (comparable=%s)", (comparable) => {
|
|
@@ -12,22 +11,3 @@ describe("retryQuestion", () => {
|
|
|
12
11
|
).toBeGreaterThanOrEqual(2)
|
|
13
12
|
})
|
|
14
13
|
})
|
|
15
|
-
|
|
16
|
-
describe("fastSuiteGuard", () => {
|
|
17
|
-
it("unset: names the setting and exits 0", () => {
|
|
18
|
-
const lines = renderText(fastSuiteGuard).join("\n")
|
|
19
|
-
expect(lines).toContain("fastTestCommand")
|
|
20
|
-
expect(lines).toContain("GTD_FASTTESTCOMMAND")
|
|
21
|
-
expect(lines).toContain("exit 0")
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it("blank after trim counts as unset", () => {
|
|
25
|
-
expect(renderText(fastSuiteGuard, { env: { fastTestCommand: " \t " } })).toContain("exit 0")
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it("set: only removes SETUP", () => {
|
|
29
|
-
const lines = renderText(fastSuiteGuard, { env: { fastTestCommand: "true" } })
|
|
30
|
-
expect(lines.join("\n")).not.toContain("exit 0")
|
|
31
|
-
expect(lines.join("\n")).toContain("rm -f")
|
|
32
|
-
})
|
|
33
|
-
})
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { dirname } from "node:path"
|
|
2
1
|
import {
|
|
3
2
|
answered,
|
|
4
3
|
check,
|
|
@@ -6,8 +5,6 @@ import {
|
|
|
6
5
|
human,
|
|
7
6
|
judge,
|
|
8
7
|
numeric,
|
|
9
|
-
quote,
|
|
10
|
-
read,
|
|
11
8
|
vars,
|
|
12
9
|
type JudgeQuestion,
|
|
13
10
|
} from "../../flows/index.js"
|
|
@@ -15,35 +12,12 @@ import * as kit from "../../workflow-kit/index.js"
|
|
|
15
12
|
import { describeEscalation, escalate, escalationExhausted, ESCALATION, FEEDBACK } from "./steps.js"
|
|
16
13
|
import * as t from "./text.js"
|
|
17
14
|
|
|
18
|
-
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
19
|
-
export const SETUP = ".gtd/SETUP.md"
|
|
20
|
-
|
|
21
|
-
const SETUP_TEXT =
|
|
22
|
-
"The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND."
|
|
23
|
-
|
|
24
|
-
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
25
|
-
export const fastSuiteGuard = (): string[] =>
|
|
26
|
-
(env.fastTestCommand ?? "").trim() === ""
|
|
27
|
-
? [
|
|
28
|
-
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
29
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
30
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
31
|
-
"exit 0",
|
|
32
|
-
]
|
|
33
|
-
: [`rm -f ${quote(SETUP)}`]
|
|
34
|
-
|
|
35
15
|
/** Fix turns a run of red checks gets before it escalates. */
|
|
36
16
|
export const FIX_CAP = 3
|
|
37
17
|
|
|
38
|
-
const unconfigured = (): boolean => read(SETUP) !== undefined
|
|
39
|
-
|
|
40
18
|
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
41
19
|
export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
|
|
42
|
-
|
|
43
|
-
() =>
|
|
44
|
-
check(name, env.testCommand ?? "", { report: FEEDBACK, label, preamble: fastSuiteGuard() }),
|
|
45
|
-
unconfigured,
|
|
46
|
-
)
|
|
20
|
+
check(name, env.testCommand ?? "", { report: FEEDBACK, label })
|
|
47
21
|
|
|
48
22
|
export type { EscalationCount } from "../../workflow-kit/index.js"
|
|
49
23
|
|
|
@@ -103,22 +77,17 @@ export interface HealthOptions {
|
|
|
103
77
|
readonly fixesSoFar?: number
|
|
104
78
|
/** Escalation rounds shared with other callers in the same run of red checks. */
|
|
105
79
|
readonly escalations?: kit.EscalationCount
|
|
106
|
-
/** `fast` runs `fastTestCommand` (default `full`: `testCommand`). */
|
|
107
|
-
readonly suite?: "full" | "fast"
|
|
108
80
|
/** Paths removed on a green run. */
|
|
109
81
|
readonly sweepOnGreen?: readonly string[]
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
113
|
-
|
|
114
|
-
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
84
|
+
const runSuite = (options: HealthOptions): Promise<boolean> =>
|
|
85
|
+
check("health.check", env.testCommand ?? "", {
|
|
115
86
|
report: FEEDBACK,
|
|
116
87
|
label: "Running checks",
|
|
117
88
|
// Swept only on green: an unresolved analysis survives every retry.
|
|
118
89
|
sweepOnGreen: [ESCALATION, ...(options.sweepOnGreen ?? [])],
|
|
119
|
-
...(fast ? { preamble: fastSuiteGuard() } : {}),
|
|
120
90
|
})
|
|
121
|
-
}
|
|
122
91
|
|
|
123
92
|
/**
|
|
124
93
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
@@ -127,11 +96,7 @@ const runSuite = (options: HealthOptions): Promise<boolean> => {
|
|
|
127
96
|
*/
|
|
128
97
|
export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
|
|
129
98
|
kit.healthy(fix, {
|
|
130
|
-
|
|
131
|
-
check:
|
|
132
|
-
options.suite === "fast"
|
|
133
|
-
? () => kit.untilSettled(() => runSuite(options), unconfigured)
|
|
134
|
-
: () => runSuite(options),
|
|
99
|
+
check: () => runSuite(options),
|
|
135
100
|
report: FEEDBACK,
|
|
136
101
|
fixCap: FIX_CAP,
|
|
137
102
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { env, human } from "../../flows/index.js"
|
|
1
|
+
import { changesSince, env, human } from "../../flows/index.js"
|
|
2
|
+
import { removedAssertions } from "../../workflow-kit/index.js"
|
|
2
3
|
import * as t from "./text.js"
|
|
3
4
|
|
|
4
5
|
// The bundled workflow's single steps. A step's name is relative to the
|
|
@@ -18,8 +19,8 @@ export const coder = (): string => env.coderModel ?? ""
|
|
|
18
19
|
|
|
19
20
|
// ── Keeping the suite green ─────────────────────────────────────────────────
|
|
20
21
|
|
|
21
|
-
export const fixCheck = (
|
|
22
|
-
t.agentWithSkills("fix", t.buildFixPrompt(
|
|
22
|
+
export const fixCheck = (): Promise<void> =>
|
|
23
|
+
t.agentWithSkills("fix", t.buildFixPrompt(), {
|
|
23
24
|
label: "Fixing the check",
|
|
24
25
|
file: FEEDBACK,
|
|
25
26
|
model: coder(),
|
|
@@ -90,14 +91,18 @@ export const fixQuality = (): Promise<void> =>
|
|
|
90
91
|
|
|
91
92
|
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
92
93
|
export const reviewing = (base: string, carry?: string): Promise<void> =>
|
|
93
|
-
t.agentWithSkills(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
t.agentWithSkills(
|
|
95
|
+
"review.reviewing",
|
|
96
|
+
t.buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))),
|
|
97
|
+
{
|
|
98
|
+
label: "Reviewing",
|
|
99
|
+
file: REVIEW,
|
|
100
|
+
mode: "review",
|
|
101
|
+
model: planner(),
|
|
102
|
+
system: t.reviewerSystem(),
|
|
103
|
+
base,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
101
106
|
|
|
102
107
|
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
103
108
|
export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
|
|
@@ -5,51 +5,11 @@ import {
|
|
|
5
5
|
type CodeThreadInfo,
|
|
6
6
|
type StepRequest,
|
|
7
7
|
} from "../../flows/index.js"
|
|
8
|
-
import {
|
|
8
|
+
import { handOffReview, REVIEW_ORIG } from "./tail.js"
|
|
9
9
|
import { fixtureContext } from "./text.fixture.js"
|
|
10
10
|
|
|
11
11
|
afterEach(() => installContext(undefined))
|
|
12
12
|
|
|
13
|
-
describe("the quality fix loop", () => {
|
|
14
|
-
it("a fix that resolves QUALITY.md and rewrites a scenario ends after the wording gate", async () => {
|
|
15
|
-
const QUALITY = ".gtd/QUALITY.md"
|
|
16
|
-
const log: string[] = []
|
|
17
|
-
const files = new Map([
|
|
18
|
-
[QUALITY, "- finding"],
|
|
19
|
-
["e2e/a.feature", "Given a"],
|
|
20
|
-
])
|
|
21
|
-
let last: readonly Change[] = []
|
|
22
|
-
installContext(
|
|
23
|
-
fixtureContext(
|
|
24
|
-
{},
|
|
25
|
-
{
|
|
26
|
-
step: (request: StepRequest) => {
|
|
27
|
-
if (request.kind === "restart") return Promise.resolve()
|
|
28
|
-
log.push(request.name)
|
|
29
|
-
last = []
|
|
30
|
-
if (request.name === "fix.quality.fixing") {
|
|
31
|
-
files.delete(QUALITY)
|
|
32
|
-
files.set("e2e/a.feature", "Given b")
|
|
33
|
-
last = [{ path: QUALITY, status: "deleted", before: "- finding", after: undefined }]
|
|
34
|
-
}
|
|
35
|
-
return Promise.resolve()
|
|
36
|
-
},
|
|
37
|
-
pushScope: () => undefined,
|
|
38
|
-
popScope: () => undefined,
|
|
39
|
-
read: (path) => files.get(path),
|
|
40
|
-
changes: () => last,
|
|
41
|
-
matches: (path, pattern) => path === pattern,
|
|
42
|
-
head: () => "h2",
|
|
43
|
-
start: () => "h0",
|
|
44
|
-
},
|
|
45
|
-
),
|
|
46
|
-
)
|
|
47
|
-
const frozen = { at: "h1", texts: { "e2e/a.feature": "Given a" } }
|
|
48
|
-
expect(await fixQualityFindings({ rounds: 0 }, frozen)).toBe(true)
|
|
49
|
-
expect(log).toEqual(["fix.quality.fixing", "scenario-wording"])
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
13
|
const REVIEW = ".gtd/REVIEW.md"
|
|
54
14
|
const reviewDoc = (note: string): string =>
|
|
55
15
|
[
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../../flows/index.js"
|
|
2
2
|
import * as kit from "../../workflow-kit/index.js"
|
|
3
3
|
import { escalation, FIX_CAP, healthy, type EscalationCount } from "./health.js"
|
|
4
|
-
import { guarded, type FrozenScenarios } from "./scenarios.js"
|
|
5
4
|
import {
|
|
6
|
-
ARCHITECTURE,
|
|
7
5
|
awaitReview,
|
|
8
6
|
fixCheck,
|
|
9
7
|
fixQuality,
|
|
@@ -15,7 +13,6 @@ import {
|
|
|
15
13
|
reviewQuality,
|
|
16
14
|
} from "./steps.js"
|
|
17
15
|
import * as t from "./text.js"
|
|
18
|
-
import type { BuildContext } from "./text.js"
|
|
19
16
|
|
|
20
17
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
21
18
|
export const REVIEW_ORIG = ".gtd/REVIEW.orig.md"
|
|
@@ -29,22 +26,14 @@ export const qualityLap = (): Promise<"clean" | "findings"> =>
|
|
|
29
26
|
kit.qualityLap(kit.lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY)
|
|
30
27
|
|
|
31
28
|
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
32
|
-
export const fixQualityFindings = (
|
|
33
|
-
escalations: EscalationCount,
|
|
34
|
-
frozen?: FrozenScenarios,
|
|
35
|
-
): Promise<boolean> =>
|
|
29
|
+
export const fixQualityFindings = (escalations: EscalationCount): Promise<boolean> =>
|
|
36
30
|
kit.fixFindings({
|
|
37
|
-
fix:
|
|
31
|
+
fix: fixQuality,
|
|
38
32
|
findings: QUALITY,
|
|
39
33
|
cap: FIX_CAP,
|
|
40
34
|
escalate: () => escalation(escalations),
|
|
41
35
|
})
|
|
42
36
|
|
|
43
|
-
/** What the tail's full run knows of a plan the process built. */
|
|
44
|
-
export interface TailPlan extends BuildContext {
|
|
45
|
-
readonly frozen: FrozenScenarios | undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
37
|
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
49
38
|
export const closeReview = (round: string): Promise<void> =>
|
|
50
39
|
run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
@@ -63,36 +52,31 @@ export const refusePendingHandOff = (): void => {
|
|
|
63
52
|
|
|
64
53
|
/**
|
|
65
54
|
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
66
|
-
* `finish` — the human review, as the calling workflow runs it.
|
|
55
|
+
* `finish` — the human review, as the calling workflow runs it. `green`: the
|
|
56
|
+
* caller just left the full suite green (the package loop), so no run opens it.
|
|
67
57
|
*/
|
|
68
58
|
export const tail = <T>(
|
|
69
59
|
fixFirst: boolean,
|
|
70
|
-
finish: (escalations: EscalationCount
|
|
71
|
-
|
|
60
|
+
finish: (escalations: EscalationCount) => Promise<T>,
|
|
61
|
+
green = false,
|
|
72
62
|
): Promise<T> =>
|
|
73
63
|
scope("build", async () => {
|
|
74
|
-
const frozen = built?.frozen
|
|
75
64
|
const escalations: EscalationCount = { rounds: 0 }
|
|
76
|
-
const
|
|
65
|
+
const fix = fixCheck
|
|
77
66
|
const repair = async () => {
|
|
78
|
-
await
|
|
79
|
-
await healthy(
|
|
67
|
+
await fix()
|
|
68
|
+
await healthy(fix, { fixesSoFar: 1, escalations })
|
|
80
69
|
}
|
|
81
70
|
// fixFirst's own fix + healthy below already is the full run.
|
|
82
71
|
if (fixFirst) await repair()
|
|
83
|
-
else {
|
|
84
|
-
await healthy(
|
|
85
|
-
guarded(frozen, () => fixCheck(built)),
|
|
86
|
-
{ escalations, sweepOnGreen: [ARCHITECTURE] },
|
|
87
|
-
)
|
|
88
|
-
}
|
|
72
|
+
else if (!green) await healthy(fix, { escalations })
|
|
89
73
|
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
90
74
|
if ((await qualityLap()) === "findings") {
|
|
91
|
-
if (await fixQualityFindings(escalations
|
|
92
|
-
await healthy(
|
|
75
|
+
if (await fixQualityFindings(escalations)) {
|
|
76
|
+
await healthy(fix, { escalations })
|
|
93
77
|
} else await repair()
|
|
94
78
|
}
|
|
95
|
-
return finish(escalations
|
|
79
|
+
return finish(escalations)
|
|
96
80
|
})
|
|
97
81
|
|
|
98
82
|
/**
|
|
@@ -105,7 +89,7 @@ export const handOffReview = (base: string, escalations: EscalationCount): Promi
|
|
|
105
89
|
file: REVIEW,
|
|
106
90
|
reviewing,
|
|
107
91
|
fixRisks,
|
|
108
|
-
keepGreen: () => healthy(
|
|
92
|
+
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
109
93
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
110
94
|
reviewMissing,
|
|
111
95
|
close: closeReview,
|
|
@@ -182,50 +182,11 @@ describe("architectureAuthorPrompt", () => {
|
|
|
182
182
|
})
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
describe("buildFixPrompt
|
|
186
|
-
|
|
187
|
-
ranges: [
|
|
188
|
-
{ pkg: "00-scenarios", from: "aaa1111", to: "bbb2222" },
|
|
189
|
-
{ pkg: "01-engine", from: "bbb2222", to: "ccc3333" },
|
|
190
|
-
],
|
|
191
|
-
scenarios: { added: ["tests/new.feature"], changed: ["tests/old.feature"] },
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
it("points at the architecture document", () => {
|
|
195
|
-
expect(renderText(() => buildFixPrompt(built))).toContain(".gtd/ARCHITECTURE.md")
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it("lists new scenario paths as likely missing wiring", () => {
|
|
199
|
-
const prompt = renderText(() => buildFixPrompt(built))
|
|
200
|
-
expect(prompt).toContain("tests/new.feature")
|
|
201
|
-
expect(prompt).toContain("missing wiring")
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it("lists changed or existing scenarios as regressions", () => {
|
|
205
|
-
const prompt = renderText(() => buildFixPrompt(built))
|
|
206
|
-
expect(prompt).toContain("regression")
|
|
207
|
-
expect(prompt).toContain("tests/old.feature")
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it("lists one range line per package", () => {
|
|
211
|
-
const prompt = renderText(() => buildFixPrompt(built))
|
|
212
|
-
expect(prompt).toContain("git log --oneline aaa1111..bbb2222 # 00-scenarios")
|
|
213
|
-
expect(prompt).toContain("git log --oneline bbb2222..ccc3333 # 01-engine")
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it("is today's prompt without a build context", () => {
|
|
185
|
+
describe("buildFixPrompt", () => {
|
|
186
|
+
it("starts each bullet on its own line", () => {
|
|
217
187
|
const prompt = renderText(() => buildFixPrompt())
|
|
218
|
-
expect(renderText(() => buildFixPrompt(undefined))).toBe(prompt)
|
|
219
188
|
expect(prompt).toMatch(/\n- Leave everything uncommitted — do not commit\n$/)
|
|
220
189
|
expect(prompt).toContain("writes its own\n- Leave")
|
|
221
|
-
expect(prompt).not.toContain("ARCHITECTURE")
|
|
222
|
-
expect(prompt).not.toContain("git log --oneline")
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it("starts each bullet on its own line", () => {
|
|
226
|
-
const prompt = renderText(() => buildFixPrompt(built))
|
|
227
|
-
expect(prompt).toMatch(/^- Read `\.gtd\/ARCHITECTURE\.md`/m)
|
|
228
|
-
expect(prompt).toMatch(/^- Leave everything uncommitted/m)
|
|
229
190
|
})
|
|
230
191
|
})
|
|
231
192
|
|
|
@@ -459,3 +420,24 @@ describe("deferNotesScript — the in-memory recognizer agrees with the real sh
|
|
|
459
420
|
expect(repo.readFile(".gtd/TODO.md")).toBe(real)
|
|
460
421
|
})
|
|
461
422
|
})
|
|
423
|
+
|
|
424
|
+
describe("buildReviewReviewingPrompt scenarios", () => {
|
|
425
|
+
it("names each removed assertion and asks for a Weakened: note", () => {
|
|
426
|
+
const prompt = renderText(() =>
|
|
427
|
+
buildReviewReviewingPrompt("base", undefined, [
|
|
428
|
+
{
|
|
429
|
+
path: "e2e/a.feature",
|
|
430
|
+
lines: [{ scenario: "Scenario: shown", line: "Then it is shown" }],
|
|
431
|
+
},
|
|
432
|
+
]),
|
|
433
|
+
)
|
|
434
|
+
expect(prompt).toContain("e2e/a.feature — Scenario: shown — Then it is shown")
|
|
435
|
+
expect(prompt).toContain("`Weakened:`")
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it("asks for no Weakened: note when nothing was removed", () => {
|
|
439
|
+
const prompt = renderText(() => buildReviewReviewingPrompt("base"))
|
|
440
|
+
expect(prompt).toContain("## Scenarios")
|
|
441
|
+
expect(prompt).not.toContain("Weakened:")
|
|
442
|
+
})
|
|
443
|
+
})
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type AgentOptions,
|
|
11
11
|
type SummaryContext,
|
|
12
12
|
} from "../../flows/index.js"
|
|
13
|
-
import type {
|
|
13
|
+
import type { RemovedAssertions } from "../../workflow-kit/index.js"
|
|
14
14
|
import {
|
|
15
15
|
accessPreamble,
|
|
16
16
|
skillsPreamble,
|
|
@@ -274,7 +274,8 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
274
274
|
- Every open point here is TECHNICAL — triage already resolved
|
|
275
275
|
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
276
276
|
yourself unless you genuinely cannot defend a default; a wrong
|
|
277
|
-
technical call is still caught by
|
|
277
|
+
technical call is still caught by each package's suite run and the
|
|
278
|
+
quality lap
|
|
278
279
|
- The narrow exception: a simplification that drops something
|
|
279
280
|
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
280
281
|
silent default
|
|
@@ -286,11 +287,13 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
286
287
|
only, as fenced TypeScript (or the repository's language)
|
|
287
288
|
3. \`## Call Stacks\` — seam level, entry point → module
|
|
288
289
|
boundary; one line per hop, never per line of code
|
|
289
|
-
4. \`## E2E Scenarios\` —
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
4. \`## E2E Scenarios\` — one line per user-visible behaviour,
|
|
291
|
+
\`- e2e: <path> — <behaviour> (covers <concern>)\`, in plain
|
|
292
|
+
words: the builder writes the scenario in the repository's
|
|
293
|
+
own e2e framework, so never spell out Gherkin here. With no
|
|
294
|
+
user-visible behaviour change, the literal line
|
|
295
|
+
\`No e2e change.\` plus a one-line reason — never empty or
|
|
296
|
+
missing
|
|
294
297
|
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
295
298
|
\`## Interfaces\`, each test a line
|
|
296
299
|
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
@@ -320,35 +323,31 @@ export const architectureDecomposePrompt = (): string =>
|
|
|
320
323
|
${stateFileRules}
|
|
321
324
|
- The only state files this turn touches are the package files
|
|
322
325
|
under \`.gtd/packages/\` — no other files for notes or output.
|
|
323
|
-
Never delete \`.gtd/ARCHITECTURE.md\`:
|
|
324
|
-
|
|
326
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: package files may
|
|
327
|
+
reference it, and it is removed with the last package
|
|
325
328
|
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
326
329
|
conversation, otherwise read it (the converged technical
|
|
327
330
|
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
328
331
|
and unit tests — this turn groups them into packages. A
|
|
329
332
|
\`## Merged Concerns\` heading there records merges, never a
|
|
330
333
|
concern of its own: write no package file for it
|
|
331
|
-
- A package is
|
|
332
|
-
green
|
|
333
|
-
|
|
334
|
-
same files stay in one package unless
|
|
335
|
-
an interface the earlier one creates.
|
|
336
|
-
|
|
337
|
-
interface-introducing packages first
|
|
334
|
+
- A package is one concern's slice, and each must leave the full
|
|
335
|
+
suite green — every package boundary costs a full run, so write
|
|
336
|
+
as few as the concerns allow. Keep the merge rule: concerns whose
|
|
337
|
+
footprints centre on the same files stay in one package unless
|
|
338
|
+
the later one only consumes an interface the earlier one creates.
|
|
339
|
+
Order them interface-introducing packages first
|
|
338
340
|
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
339
341
|
requirement(s) — both, independently, if merged — its tasks, and
|
|
340
342
|
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
341
343
|
relevant paths:
|
|
342
|
-
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
- the last numbered package is the wiring package: CLI entry,
|
|
350
|
-
workflow composition, config. A red e2e after it is a bug, not
|
|
351
|
-
missing work
|
|
344
|
+
- \`01-…\` onward — one vertical slice each: the unit tests,
|
|
345
|
+
the code, the wiring (CLI entry, workflow composition, config)
|
|
346
|
+
and every \`## E2E Scenarios\` entry its concern covers, so the
|
|
347
|
+
slice ends with its scenarios passing. A requirement no test
|
|
348
|
+
covers (docs, README, deletions, config) is a task of the slice
|
|
349
|
+
it belongs to; only one no slice owns becomes a chore package,
|
|
350
|
+
which declares no tests
|
|
352
351
|
- Every package with tests has a \`## Tests\` section, one line per
|
|
353
352
|
declared test, copied from the architecture entries:
|
|
354
353
|
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
@@ -398,10 +397,10 @@ export const packagesItemBuildingPrompt = (pkg: string): string =>
|
|
|
398
397
|
- Write the tests the package declares under \`## Tests\` first,
|
|
399
398
|
then the code that makes them pass; the turn is refused unless
|
|
400
399
|
every declared test is in your diff
|
|
401
|
-
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
400
|
+
- A declared e2e test is a scenario in the repository's own e2e
|
|
401
|
+
framework for the behaviour its \`## E2E Scenarios\` entry names.
|
|
402
|
+
Word it to fit the existing steps — reuse and generalise them
|
|
403
|
+
rather than adding one-off ones
|
|
405
404
|
- Implement every task the package describes, no more, no less,
|
|
406
405
|
fanning independent ones out to parallel subagents where your
|
|
407
406
|
harness supports it; leave other package files untouched
|
|
@@ -470,34 +469,10 @@ turn, or land untouched to give it one more attempt at the same
|
|
|
470
469
|
analysis.
|
|
471
470
|
`
|
|
472
471
|
|
|
473
|
-
|
|
474
|
-
export interface BuildContext {
|
|
475
|
-
readonly ranges: readonly { readonly pkg: string; readonly from: string; readonly to: string }[]
|
|
476
|
-
readonly scenarios: {
|
|
477
|
-
readonly added: readonly string[]
|
|
478
|
-
readonly changed: readonly string[]
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const buildContextPrompt = (built: BuildContext): string => {
|
|
483
|
-
const list = (paths: readonly string[]): string =>
|
|
484
|
-
paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n")
|
|
485
|
-
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
486
|
-
- A red scenario that is NEW in this plan likely means missing wiring
|
|
487
|
-
between packages; new scenarios:
|
|
488
|
-
${list(built.scenarios.added)}
|
|
489
|
-
- A red CHANGED or existing scenario or test is a regression a package
|
|
490
|
-
introduced; scenarios this plan changed:
|
|
491
|
-
${list(built.scenarios.changed)}
|
|
492
|
-
- Each package's commits, to bisect a break:
|
|
493
|
-
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
494
|
-
`
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export const buildFixPrompt = (built?: BuildContext): string =>
|
|
472
|
+
export const buildFixPrompt = (): string =>
|
|
498
473
|
`${stateFileRules}
|
|
499
474
|
${fixFeedbackPrompt}
|
|
500
|
-
|
|
475
|
+
- Leave everything uncommitted — do not commit
|
|
501
476
|
`
|
|
502
477
|
|
|
503
478
|
export const finisherSystem = (): string =>
|
|
@@ -888,7 +863,28 @@ actionability, and never dismiss a real note or edit as approval.
|
|
|
888
863
|
the sign-off
|
|
889
864
|
`
|
|
890
865
|
|
|
891
|
-
|
|
866
|
+
const scenariosPrompt = (removed: readonly RemovedAssertions[]): string =>
|
|
867
|
+
`- When a scenario in a \`.feature\` file was added or changed since the
|
|
868
|
+
base, the first chunk is \`## Scenarios\`: one pointer per such
|
|
869
|
+
scenario, its note saying in plain words what behaviour it now pins
|
|
870
|
+
down — the human signs off the scenarios here, nowhere else
|
|
871
|
+
${
|
|
872
|
+
removed.length === 0
|
|
873
|
+
? ""
|
|
874
|
+
: `- These assertions of scenarios that existed at the base are gone:
|
|
875
|
+
${removed.map(({ path, lines }) => lines.map((a) => ` ${path} — ${a.scenario} — ${a.line}\n`).join("")).join("")} Open the note of each scenario that lost one with \`Weakened:\`,
|
|
876
|
+
naming the lost line and whether \`.gtd/REQUIREMENTS.md\` asked
|
|
877
|
+
for it. A scenario that no longer exists has no pointer of its own:
|
|
878
|
+
put its \`Weakened:\` note on a pointer of the chunk covering its
|
|
879
|
+
removal
|
|
880
|
+
`
|
|
881
|
+
}`
|
|
882
|
+
|
|
883
|
+
export const buildReviewReviewingPrompt = (
|
|
884
|
+
base: string,
|
|
885
|
+
carry?: string,
|
|
886
|
+
removed: readonly RemovedAssertions[] = [],
|
|
887
|
+
): string =>
|
|
892
888
|
`${styleBlock}
|
|
893
889
|
|
|
894
890
|
${styleFormatContract}
|
|
@@ -931,7 +927,7 @@ review the changes:
|
|
|
931
927
|
A note sitting entirely on the line(s) beneath the pointer is
|
|
932
928
|
also valid. Either way, the note must never start with a bare \`./path\` token
|
|
933
929
|
— that parses as a second pointer, not a note
|
|
934
|
-
No diff is given — read the changes yourself. The range runs
|
|
930
|
+
${scenariosPrompt(removed)}No diff is given — read the changes yourself. The range runs
|
|
935
931
|
from \`${base}\` to the working tree (committed turns
|
|
936
932
|
plus anything pending); on a feedback round that's the previous
|
|
937
933
|
review's boundary, so it covers only what's new.
|
|
@@ -986,19 +982,3 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
|
986
982
|
Print the closing message and stop — this writes nothing itself.
|
|
987
983
|
`
|
|
988
984
|
}
|
|
989
|
-
|
|
990
|
-
export const scenarioWordingMessage = (at: string, drifted: readonly WordingDrift[]): string =>
|
|
991
|
-
`The e2e scenario wording was frozen when package 0 landed, and the last
|
|
992
|
-
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
993
|
-
|
|
994
|
-
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
995
|
-
|
|
996
|
-
To accept the change, land untouched.
|
|
997
|
-
|
|
998
|
-
To reject it, run
|
|
999
|
-
|
|
1000
|
-
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
1001
|
-
|
|
1002
|
-
and land. Restoring only some of the files is a partial accept: every file
|
|
1003
|
-
still differing from the frozen wording is accepted.
|
|
1004
|
-
`
|
|
@@ -20,7 +20,6 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
20
20
|
/** Environment settings. */
|
|
21
21
|
export const envDefaults: Readonly<Record<string, string>> = {
|
|
22
22
|
testCommand: "npm test",
|
|
23
|
-
fastTestCommand: "",
|
|
24
23
|
plannerModel: "smart",
|
|
25
24
|
coderModel: "base",
|
|
26
25
|
}
|