@pmelab/gtd 27.4.0 → 27.6.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 +17 -0
- package/dist/chunks/{Schema-CyImoHTj.mjs → Schema-DO5ttJaI.mjs} +1 -1
- package/dist/chunks/{reserved-BPdVZkgv.mjs → reserved-Df8GeSHw.mjs} +97 -8
- package/dist/chunks/{tail-iLAMoigl.mjs → tail-DmIfEAI4.mjs} +1 -1
- package/dist/flows.mjs +20 -2
- package/dist/gtd.bundle.mjs +97 -17
- package/dist/testkit.mjs +80 -18
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +193 -66
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +447 -114
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +49 -1
- package/src/cli/Cli.ts +20 -3
- package/src/edge/Edge.test.ts +156 -1
- package/src/edge/Edge.ts +30 -7
- package/src/flags/index.test.ts +27 -1
- package/src/flags/index.ts +15 -3
- package/src/flows/helpers.ts +7 -3
- package/src/flows/runtime.test.ts +42 -0
- package/src/flows/runtime.ts +36 -0
- package/src/replay/Replay.test.ts +43 -0
- package/src/replay/Replay.ts +10 -2
- package/src/replay/Trailers.test.ts +54 -0
- package/src/replay/Trailers.ts +15 -1
- package/src/replay/judgeDiffShape.test.ts +43 -0
- package/src/step/planStart.ts +3 -2
- package/src/testkit/TestRepo.ts +3 -0
- package/src/testkit/steps.ts +18 -0
- package/src/workflow/index.ts +8 -1
- package/src/workflow/vars.test.ts +23 -1
- package/src/workflow/vars.ts +24 -0
- package/src/workflows/feature/feature.ts +11 -2
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/risk-tier.feature +301 -0
- package/src/workflows/feature/seam-pin.feature +52 -0
- package/src/workflows/feature/seam.feature +387 -0
- package/src/workflows/feature/seam.test.ts +88 -0
- package/src/workflows/feature/seam.ts +111 -0
- package/src/workflows/feature/steps.ts +3 -2
- package/src/workflows/feature/tier.test.ts +164 -0
- package/src/workflows/feature/tier.ts +74 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/options.test.ts +17 -1
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/skills.test.ts +20 -2
- package/src/workflows/shared/skills.ts +6 -0
- package/src/workflows/shared/text.test.ts +188 -0
- package/src/workflows/shared/text.ts +139 -5
- package/src/workflows/shared/vars.ts +6 -0
- package/src/workflows/shared/workflows.steps.ts +47 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import { installContext, type FlowContext } from "../../flows/index.js"
|
|
3
|
+
import { fixtureContext } from "../shared/text.fixture.js"
|
|
4
|
+
import { productionSignals, readRiskTier, recordRisk, riskTierOn } from "./tier.js"
|
|
5
|
+
|
|
6
|
+
afterEach(() => installContext(undefined))
|
|
7
|
+
|
|
8
|
+
const withVars = (vars: Record<string, string>) => installContext(fixtureContext({ vars }))
|
|
9
|
+
|
|
10
|
+
describe("riskTierOn", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
[{ flag_naive_pass: "1", flag_risk_tier: "1" }, true],
|
|
13
|
+
[{ flag_naive_pass: "1", flag_risk_tier: "0" }, false],
|
|
14
|
+
[{ flag_naive_pass: "0", flag_risk_tier: "1" }, false],
|
|
15
|
+
[{ flag_naive_pass: "0", flag_risk_tier: "0" }, false],
|
|
16
|
+
[{ flag_naive_pass: "1" }, false],
|
|
17
|
+
])("%j is %s", (vars, expected) => {
|
|
18
|
+
withVars(vars)
|
|
19
|
+
expect(riskTierOn()).toBe(expected)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("readRiskTier", () => {
|
|
24
|
+
const doc = (body: string) =>
|
|
25
|
+
`# Requirements\n\n## Risk Tier\n\n${body}\n\n## Answered Questions\n`
|
|
26
|
+
|
|
27
|
+
it.each(["low", "`low`", "**Low** — trivial copy", "LOW - copy", "_low_ — x"])(
|
|
28
|
+
"reads %j as low",
|
|
29
|
+
(body) => expect(readRiskTier(doc(body))).toBe("low"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
it.each(["high", "medium", "lower", "", " \n\n"])("reads %j as high", (body) =>
|
|
33
|
+
expect(readRiskTier(doc(body))).toBe("high"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
it("reads a missing section and undefined as high", () => {
|
|
37
|
+
expect(readRiskTier("# Requirements\n\n## Other\n\nlow\n")).toBe("high")
|
|
38
|
+
expect(readRiskTier(undefined)).toBe("high")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("takes the first non-blank line only and stops at the next section", () => {
|
|
42
|
+
expect(readRiskTier("## Risk Tier\n\nhigh — why\nlow\n")).toBe("high")
|
|
43
|
+
expect(readRiskTier("## Risk Tier\n\n## Other\n\nlow\n")).toBe("high")
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("readRiskTier fences", () => {
|
|
48
|
+
it("ignores a backtick-fenced example above the real section", () => {
|
|
49
|
+
const text = "```\n## Risk Tier\n\nlow\n```\n\n## Risk Tier\n\nhigh\n"
|
|
50
|
+
expect(readRiskTier(text)).toBe("high")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("reads a fenced example with no real section as high", () => {
|
|
54
|
+
expect(readRiskTier("```md\n## Risk Tier\n\nlow\n```\n")).toBe("high")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("keeps the signals section open across a fence and skips fenced items", () => {
|
|
58
|
+
const signals = "## Production Signals\n- before\n```\n# x\n- fake\n```\n- after\n"
|
|
59
|
+
expect(productionSignals(signals)).toEqual(["before", "after"])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("ignores a tilde-fenced example above the real section", () => {
|
|
63
|
+
const text = "~~~\n## Risk Tier\n\nlow\n~~~\n\n## Risk Tier\n\nhigh\n"
|
|
64
|
+
expect(readRiskTier(text)).toBe("high")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("does not read a fence opener's info string as the tier", () => {
|
|
68
|
+
expect(readRiskTier("## Risk Tier\n\n```low\nhigh\n```\n")).toBe("high")
|
|
69
|
+
expect(readRiskTier("## Risk Tier\n\n~~~low\nlow\n~~~\n")).toBe("high")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("fails closed on an unclosed fence before the real section", () => {
|
|
73
|
+
expect(readRiskTier("```\nstray\n\n## Risk Tier\n\nlow\n")).toBe("high")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("does not close a 4-backtick fence with 3 backticks", () => {
|
|
77
|
+
const text = "````\n```\n## Risk Tier\n\nlow\n````\n"
|
|
78
|
+
expect(readRiskTier(text)).toBe("high")
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe("readRiskTier fence closers", () => {
|
|
83
|
+
it.each([
|
|
84
|
+
["a tilde line does not close a backtick fence", "```\n~~~\n## Risk Tier\n\nlow\n```\n"],
|
|
85
|
+
["a closer with an info string does not close", "```\n```js\n## Risk Tier\n\nlow\n```\n"],
|
|
86
|
+
])("%s", (_, text) => {
|
|
87
|
+
expect(readRiskTier(text)).toBe("high")
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe("productionSignals", () => {
|
|
92
|
+
it("lists top-level items only, skipping blanks and indented lines", () => {
|
|
93
|
+
const text = `## Production Signals
|
|
94
|
+
|
|
95
|
+
- error rate on /pay
|
|
96
|
+
not a signal
|
|
97
|
+
- nested detail
|
|
98
|
+
-
|
|
99
|
+
- spaced out\t signal
|
|
100
|
+
* star item
|
|
101
|
+
|
|
102
|
+
## Answered Questions
|
|
103
|
+
|
|
104
|
+
- not a signal
|
|
105
|
+
`
|
|
106
|
+
expect(productionSignals(text)).toEqual([
|
|
107
|
+
"error rate on /pay",
|
|
108
|
+
"spaced out signal",
|
|
109
|
+
"star item",
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("skips a fenced Production Signals example", () => {
|
|
114
|
+
const text = "```\n## Production Signals\n- fake\n```\n\n## Production Signals\n- real\n"
|
|
115
|
+
expect(productionSignals(text)).toEqual(["real"])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("drops control characters", () => {
|
|
119
|
+
expect(productionSignals("## Production Signals\n- a\u001bb\u0085c\n")).toEqual(["abc"])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("is empty for a missing section or undefined", () => {
|
|
123
|
+
expect(productionSignals("## Risk Tier\nlow\n")).toEqual([])
|
|
124
|
+
expect(productionSignals(undefined)).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("recordRisk", () => {
|
|
129
|
+
const record = (requirements: string | undefined) => {
|
|
130
|
+
const marks: [string, string][] = []
|
|
131
|
+
installContext({
|
|
132
|
+
...fixtureContext({ read: () => requirements }),
|
|
133
|
+
mark: (name: string, value: string) => marks.push([name, value]),
|
|
134
|
+
} as unknown as FlowContext)
|
|
135
|
+
return { tier: recordRisk(), marks }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it("marks the tier first, then one production-signal each", () => {
|
|
139
|
+
const { tier, marks } = record(
|
|
140
|
+
"## Risk Tier\nhigh — payments\n\n## Production Signals\n- a\n- b\n",
|
|
141
|
+
)
|
|
142
|
+
expect(tier).toBe("high")
|
|
143
|
+
expect(marks).toEqual([
|
|
144
|
+
["risk-tier", "high"],
|
|
145
|
+
["production-signal", "a"],
|
|
146
|
+
["production-signal", "b"],
|
|
147
|
+
])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("marks risk-tier=low too", () => {
|
|
151
|
+
const { tier, marks } = record("## Risk Tier\nlow — copy\n\n## Production Signals\n- a\n")
|
|
152
|
+
expect(tier).toBe("low")
|
|
153
|
+
expect(marks).toEqual([
|
|
154
|
+
["risk-tier", "low"],
|
|
155
|
+
["production-signal", "a"],
|
|
156
|
+
])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("treats a missing file as high", () => {
|
|
160
|
+
const { tier, marks } = record(undefined)
|
|
161
|
+
expect(tier).toBe("high")
|
|
162
|
+
expect(marks).toEqual([["risk-tier", "high"]])
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mark, read, vars } from "../../flows/index.js"
|
|
2
|
+
import { REQUIREMENTS } from "../shared/steps.js"
|
|
3
|
+
|
|
4
|
+
export type RiskTier = "low" | "high"
|
|
5
|
+
|
|
6
|
+
/** GTD_FLAG_NAIVE_PASS, GTD_FLAG_RISK_TIER: both pinned at process start, so a mid-process flip never moves a process. */
|
|
7
|
+
export const riskTierOn = (): boolean => vars.flag_naive_pass === "1" && vars.flag_risk_tier === "1"
|
|
8
|
+
|
|
9
|
+
// An unclosed fence runs to end of file, so the tier reads `high` (fail-closed). A backtick
|
|
10
|
+
// opener with a backtick in its info string still counts as a fence: deviates from CommonMark,
|
|
11
|
+
// worst case is `high`.
|
|
12
|
+
const markFenced = (text: string): readonly { line: string; fenced: boolean }[] => {
|
|
13
|
+
let fence: string | undefined // the opener run, e.g. "````"
|
|
14
|
+
return text.split(/\r?\n/).map((line) => {
|
|
15
|
+
const run = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1]
|
|
16
|
+
const fenced = fence !== undefined || run !== undefined
|
|
17
|
+
if (fence === undefined) fence = run
|
|
18
|
+
else if (
|
|
19
|
+
run !== undefined &&
|
|
20
|
+
run[0] === fence[0] &&
|
|
21
|
+
run.length >= fence.length &&
|
|
22
|
+
/^ {0,3}(`+|~+)[ \t]*$/.test(line)
|
|
23
|
+
)
|
|
24
|
+
fence = undefined
|
|
25
|
+
return { line, fenced }
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sectionLines = (
|
|
30
|
+
text: string | undefined,
|
|
31
|
+
title: string,
|
|
32
|
+
): readonly { line: string; fenced: boolean }[] => {
|
|
33
|
+
if (text === undefined) return []
|
|
34
|
+
const heading = new RegExp(`^##[ \\t]+${title}[ \\t]*$`, "i")
|
|
35
|
+
const lines = markFenced(text)
|
|
36
|
+
const start = lines.findIndex((l) => !l.fenced && heading.test(l.line))
|
|
37
|
+
if (start < 0) return []
|
|
38
|
+
const rest = lines.slice(start + 1)
|
|
39
|
+
const end = rest.findIndex((l) => !l.fenced && /^#{1,2}[ \t]/.test(l.line))
|
|
40
|
+
return end < 0 ? rest : rest.slice(0, end)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Anything but a legible `low` is `high`: under-tiering is the risky direction. */
|
|
44
|
+
export const readRiskTier = (requirements: string | undefined): RiskTier => {
|
|
45
|
+
const first = sectionLines(requirements, "Risk Tier").find(
|
|
46
|
+
(l) => !l.fenced && l.line.trim() !== "",
|
|
47
|
+
)?.line
|
|
48
|
+
const word = first?.trim().split(/\s+/)[0]?.replace(/[`*_]/g, "").toLowerCase()
|
|
49
|
+
return word === "low" ? "low" : "high"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line no-control-regex -- the control range IS the point
|
|
53
|
+
const CONTROL = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/g
|
|
54
|
+
|
|
55
|
+
const tidy = (text: string): string => text.replace(/\s+/g, " ").replace(CONTROL, "").trim()
|
|
56
|
+
|
|
57
|
+
export const productionSignals = (requirements: string | undefined): readonly string[] => {
|
|
58
|
+
const items: string[] = []
|
|
59
|
+
for (const { line, fenced } of sectionLines(requirements, "Production Signals")) {
|
|
60
|
+
if (fenced) continue
|
|
61
|
+
const top = /^[-*+][ \t]+(.*)$/.exec(line)
|
|
62
|
+
if (top !== null) items.push(top[1]!)
|
|
63
|
+
}
|
|
64
|
+
return items.map(tidy).filter((item) => item !== "")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The tier is always marked, first, so the last `risk-tier` mark opens the current lap's marks. */
|
|
68
|
+
export const recordRisk = (): RiskTier => {
|
|
69
|
+
const requirements = read(REQUIREMENTS)
|
|
70
|
+
const tier = readRiskTier(requirements)
|
|
71
|
+
mark("risk-tier", tier)
|
|
72
|
+
for (const signal of productionSignals(requirements)) mark("production-signal", signal)
|
|
73
|
+
return tier
|
|
74
|
+
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from "./feature/steps.js"
|
|
|
23
23
|
export * from "./feature/planning.js"
|
|
24
24
|
export * from "./feature/packages.js"
|
|
25
25
|
export * from "./feature/refactor.js"
|
|
26
|
+
export * from "./feature/seam.js"
|
|
26
27
|
export { buildTail, type ReviewOutcome } from "./feature/review.js"
|
|
27
28
|
export * from "./feature/feature.js"
|
|
28
29
|
export { base, review, reviewFlow } from "./review/review.js"
|
|
@@ -9,10 +9,26 @@ const { flagShipped } = options
|
|
|
9
9
|
describe("the shipped workflow factories", () => {
|
|
10
10
|
it("take exactly the settings the shipped workflows declare as options", () => {
|
|
11
11
|
expect(Object.keys(ShippedOptions.fields).sort()).toEqual(
|
|
12
|
-
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
12
|
+
[...Object.keys(defaults), ...Object.keys(envDefaults)]
|
|
13
|
+
.filter((name) => name !== "flag_naive_pass" && name !== "flag_risk_tier")
|
|
14
|
+
.sort(),
|
|
13
15
|
)
|
|
14
16
|
})
|
|
15
17
|
|
|
18
|
+
it("declare seamAllow and flag_naive_pass defaults; flag_naive_pass is no option", () => {
|
|
19
|
+
expect(defaults.flag_naive_pass).toBe("0")
|
|
20
|
+
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_naive_pass")
|
|
21
|
+
expect(configure(feature({ seamAllow: "docs/**" }))).toMatchObject({
|
|
22
|
+
ok: true,
|
|
23
|
+
build: { defaults: { seamAllow: "docs/**", flag_naive_pass: "0" } },
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("declare flag_risk_tier off by default; it is no factory option", () => {
|
|
28
|
+
expect(defaults.flag_risk_tier).toBe("0")
|
|
29
|
+
expect(Object.keys(ShippedOptions.fields)).not.toContain("flag_risk_tier")
|
|
30
|
+
})
|
|
31
|
+
|
|
16
32
|
it("lay each option over the shipped setting of its own kind", () => {
|
|
17
33
|
const result = configure(feature({ qualityReviews: "correctness", testCommand: "npm run ci" }))
|
|
18
34
|
expect(result).toMatchObject({
|
|
@@ -29,6 +29,10 @@ describe("the bundled workflow's skills export", () => {
|
|
|
29
29
|
"refactor",
|
|
30
30
|
"refactor.fix.suite",
|
|
31
31
|
"refactor.health",
|
|
32
|
+
"seam",
|
|
33
|
+
"implement",
|
|
34
|
+
"implement.fix.suite",
|
|
35
|
+
"implement.health",
|
|
32
36
|
"build.quality.correctness",
|
|
33
37
|
"build.quality.owasp-security",
|
|
34
38
|
"build.quality.ponytail-review",
|
|
@@ -39,8 +43,18 @@ describe("the bundled workflow's skills export", () => {
|
|
|
39
43
|
)
|
|
40
44
|
})
|
|
41
45
|
|
|
42
|
-
it("keys no group scope", () => {
|
|
43
|
-
for (const group of [
|
|
46
|
+
it("keys no group scope or non-agent step", () => {
|
|
47
|
+
for (const group of [
|
|
48
|
+
"packages.item.fix",
|
|
49
|
+
"build.fix",
|
|
50
|
+
"build.review.fix",
|
|
51
|
+
"build.quality",
|
|
52
|
+
"implement.fix",
|
|
53
|
+
"seam.judge",
|
|
54
|
+
"seam.approve",
|
|
55
|
+
"seam.steer",
|
|
56
|
+
"seam.by-hand",
|
|
57
|
+
]) {
|
|
44
58
|
expect(bundled).not.toHaveProperty(group)
|
|
45
59
|
}
|
|
46
60
|
})
|
|
@@ -63,6 +77,10 @@ describe("the bundled workflow's skills export", () => {
|
|
|
63
77
|
refactor: ["ponytail"],
|
|
64
78
|
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
65
79
|
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
80
|
+
seam: ["incremental-implementation"],
|
|
81
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
82
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
83
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
66
84
|
})
|
|
67
85
|
})
|
|
68
86
|
|
|
@@ -29,6 +29,12 @@ export const skills = (
|
|
|
29
29
|
refactor: ["ponytail"],
|
|
30
30
|
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
31
31
|
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
32
|
+
// Declared whether or not `naive_pass` is on, so a config keyed on them keeps
|
|
33
|
+
// loading when the flag flips off.
|
|
34
|
+
seam: ["incremental-implementation"],
|
|
35
|
+
implement: ["test-driven-development", "incremental-implementation"],
|
|
36
|
+
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
37
|
+
"implement.health": ["debugging-and-error-recovery"],
|
|
32
38
|
...lensScopes(
|
|
33
39
|
"build.quality",
|
|
34
40
|
lensesOf(vars.qualityReviews ?? ""),
|
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
buildReviewCollectingPrompt,
|
|
18
18
|
designGateAnswerMessage,
|
|
19
19
|
designTriagePrompt,
|
|
20
|
+
implementBuildingPrompt,
|
|
21
|
+
implementFixPrompt,
|
|
22
|
+
naivePassPrompt,
|
|
23
|
+
seamByHandMessage,
|
|
24
|
+
seamSteerMessage,
|
|
20
25
|
summaryPrompt,
|
|
21
26
|
withSkills,
|
|
22
27
|
} from "./text.js"
|
|
@@ -105,6 +110,33 @@ describe("agentWithSkills access preamble", () => {
|
|
|
105
110
|
})
|
|
106
111
|
})
|
|
107
112
|
|
|
113
|
+
describe("the risk tier prompt and gate line", () => {
|
|
114
|
+
const triage = (riskTier: boolean) => renderText(() => designTriagePrompt("base", riskTier))
|
|
115
|
+
const gate = (riskTier: boolean) => renderText(() => designGateAnswerMessage(riskTier))
|
|
116
|
+
|
|
117
|
+
it("asks for the tier and the signals, before the answered questions, unsure meaning high", () => {
|
|
118
|
+
const prompt = triage(true)
|
|
119
|
+
expect(prompt).toContain("`## Risk Tier`")
|
|
120
|
+
expect(prompt).toContain("`## Production Signals`")
|
|
121
|
+
expect(prompt).toContain("BEFORE `## Answered Questions`")
|
|
122
|
+
expect(prompt).toContain("unsure means `high`")
|
|
123
|
+
expect(prompt).toContain("Whichever lap this is")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("only appends the tier section when on", () => {
|
|
127
|
+
expect(triage(false)).not.toContain("Risk Tier")
|
|
128
|
+
expect(triage(true).startsWith(triage(false))).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("says how to override the tier at the gate when on", () => {
|
|
132
|
+
expect(gate(true)).toContain("edit the first word under `## Risk Tier`")
|
|
133
|
+
expect(gate(true)).toContain("approve the next lap with no further change")
|
|
134
|
+
expect(gate(true)).toContain("means `high`")
|
|
135
|
+
expect(gate(false)).not.toContain("Risk Tier")
|
|
136
|
+
expect(gate(true).startsWith(gate(false))).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
108
140
|
describe("the shared open-question instruction", () => {
|
|
109
141
|
it("leaves the option count to the agent, floored at two real options", () => {
|
|
110
142
|
const prompt = renderText(() => designTriagePrompt("base"))
|
|
@@ -241,11 +273,103 @@ describe("summaryPrompt", () => {
|
|
|
241
273
|
{ model: "smart", cost: 5 },
|
|
242
274
|
{ model: "base", cost: 7 },
|
|
243
275
|
],
|
|
276
|
+
marks: [],
|
|
244
277
|
vars: {},
|
|
245
278
|
env: {},
|
|
246
279
|
})
|
|
247
280
|
expect(prompt).toContain("Token cost: 12\n- smart: 5\n- base: 7\n\nPrint the closing message")
|
|
248
281
|
})
|
|
282
|
+
|
|
283
|
+
const base = {
|
|
284
|
+
entryCommit: "e",
|
|
285
|
+
processBase: "b",
|
|
286
|
+
processTip: "t",
|
|
287
|
+
humanCommits: [],
|
|
288
|
+
processCost: 0,
|
|
289
|
+
processCostByModel: [],
|
|
290
|
+
marks: [],
|
|
291
|
+
vars: {},
|
|
292
|
+
env: {},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
it("with no marks leaves today's text", () => {
|
|
296
|
+
expect(summaryPrompt(base)).toContain("Token cost: 0\n\nPrint the closing message")
|
|
297
|
+
expect(summaryPrompt(base)).not.toContain("Risk tier")
|
|
298
|
+
expect(summaryPrompt(base)).not.toContain("Production signals")
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it("adds the advisory risk line once for the last lap's high tier", () => {
|
|
302
|
+
const prompt = summaryPrompt({
|
|
303
|
+
...base,
|
|
304
|
+
marks: [
|
|
305
|
+
{ name: "risk-tier", value: "low" },
|
|
306
|
+
{ name: "risk-tier", value: "high" },
|
|
307
|
+
],
|
|
308
|
+
})
|
|
309
|
+
expect(prompt).toContain("Risk tier: high — this merge needs a human reviewer (advisory")
|
|
310
|
+
expect(prompt.split("Risk tier: high").length).toBe(2)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("reads only the last lap: a lowered tier and a dropped signal leave no trace", () => {
|
|
314
|
+
const prompt = summaryPrompt({
|
|
315
|
+
...base,
|
|
316
|
+
marks: [
|
|
317
|
+
{ name: "risk-tier", value: "high" },
|
|
318
|
+
{ name: "production-signal", value: "error rate" },
|
|
319
|
+
{ name: "risk-tier", value: "low" },
|
|
320
|
+
{ name: "production-signal", value: "p95" },
|
|
321
|
+
],
|
|
322
|
+
})
|
|
323
|
+
expect(prompt).not.toContain("Risk tier")
|
|
324
|
+
expect(prompt).not.toContain("error rate")
|
|
325
|
+
expect(prompt).toContain("- `p95`\n")
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it("treats a flag-on process with no tier mark as high", () => {
|
|
329
|
+
const prompt = summaryPrompt({
|
|
330
|
+
...base,
|
|
331
|
+
vars: { flag_risk_tier: "1", flag_naive_pass: "1" },
|
|
332
|
+
})
|
|
333
|
+
expect(prompt).toContain("Risk tier: high")
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it("marks no tier when the risk-tier flag is on without naive-pass", () => {
|
|
337
|
+
const prompt = summaryPrompt({ ...base, vars: { flag_risk_tier: "1", flag_naive_pass: "0" } })
|
|
338
|
+
expect(prompt).not.toContain("Risk tier")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const signal = (value: string) => ({ name: "production-signal", value })
|
|
342
|
+
|
|
343
|
+
it("caps the listed signals at 10 and fences them as not instructions", () => {
|
|
344
|
+
const prompt = summaryPrompt({
|
|
345
|
+
...base,
|
|
346
|
+
marks: [
|
|
347
|
+
{ name: "risk-tier", value: "low" },
|
|
348
|
+
...Array.from({ length: 12 }, (_, i) => signal(`s${i}`)),
|
|
349
|
+
],
|
|
350
|
+
})
|
|
351
|
+
expect(prompt).toContain("not instructions")
|
|
352
|
+
for (let i = 0; i < 10; i++) expect(prompt).toContain(`- \`s${i}\`\n`)
|
|
353
|
+
expect(prompt).not.toContain("`s10`")
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it("cuts a long signal to exactly 200 chars", () => {
|
|
357
|
+
const prompt = summaryPrompt({
|
|
358
|
+
...base,
|
|
359
|
+
marks: [{ name: "risk-tier", value: "low" }, signal("s0"), signal("x".repeat(300))],
|
|
360
|
+
})
|
|
361
|
+
expect(prompt).toContain(`- \`${"x".repeat(200)}\`\n`)
|
|
362
|
+
expect(prompt).not.toContain("x".repeat(201))
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it("lists a repeated signal once, in first-occurrence order", () => {
|
|
366
|
+
const prompt = summaryPrompt({
|
|
367
|
+
...base,
|
|
368
|
+
marks: [{ name: "risk-tier", value: "low" }, signal("s0"), signal("s1"), signal("s0")],
|
|
369
|
+
})
|
|
370
|
+
expect(prompt).toContain("- `s0`\n- `s1`\n")
|
|
371
|
+
expect(prompt.split("- `s0`").length).toBe(2)
|
|
372
|
+
})
|
|
249
373
|
})
|
|
250
374
|
|
|
251
375
|
describe("the thread rules", () => {
|
|
@@ -441,3 +565,67 @@ describe("buildReviewReviewingPrompt scenarios", () => {
|
|
|
441
565
|
expect(prompt).not.toContain("Weakened:")
|
|
442
566
|
})
|
|
443
567
|
})
|
|
568
|
+
|
|
569
|
+
describe("the seam phase's texts", () => {
|
|
570
|
+
const rejection = { strays: ["src/flows/helpers.ts", "src/flows/runtime.ts"], p: 0.85 }
|
|
571
|
+
|
|
572
|
+
it("asks the first naive pass for the quickest working change, with no rejection", () => {
|
|
573
|
+
const prompt = renderText(() => naivePassPrompt())
|
|
574
|
+
expect(prompt).toContain(".gtd/REQUIREMENTS.md")
|
|
575
|
+
expect(prompt).not.toContain("rejected")
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it("names the reject verdict, its p, every stray and the criterion on a retry", () => {
|
|
579
|
+
const prompt = renderText(() => naivePassPrompt(rejection))
|
|
580
|
+
expect(prompt).toContain("reject")
|
|
581
|
+
expect(prompt).toContain("0.85")
|
|
582
|
+
expect(prompt).toContain("`src/flows/helpers.ts`")
|
|
583
|
+
expect(prompt).toContain("`src/flows/runtime.ts`")
|
|
584
|
+
expect(prompt).toContain("solved the task the wrong way")
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it("cites the reference as reading material only and asks for a new release flag", () => {
|
|
588
|
+
const prompt = renderText(() => implementBuildingPrompt("abc123"))
|
|
589
|
+
expect(prompt).toContain("git show abc123")
|
|
590
|
+
expect(prompt).toContain("never apply")
|
|
591
|
+
expect(prompt).toContain("release flag")
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it("high and undefined tiers carry the flag bullet; low says to add none", () => {
|
|
595
|
+
const base = renderText(() => implementBuildingPrompt("abc123"))
|
|
596
|
+
expect(renderText(() => implementBuildingPrompt("abc123", "high"))).toBe(base)
|
|
597
|
+
expect(base).toContain("new release flag")
|
|
598
|
+
const low = renderText(() => implementBuildingPrompt("abc123", "low"))
|
|
599
|
+
expect(low).toContain("Add no release flag — this change is low risk tier")
|
|
600
|
+
expect(low).not.toContain("new release flag")
|
|
601
|
+
expect(low).toContain("git show abc123")
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it("keeps a fix turn to the feedback file's rules", () => {
|
|
605
|
+
expect(renderText(() => implementFixPrompt())).toContain(".gtd/FEEDBACK.md")
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
it("names both steer answers and the overrule risk, and lists every stray", () => {
|
|
609
|
+
const message = renderText(() => seamSteerMessage(rejection))
|
|
610
|
+
expect(message).toContain("edit `.gtd/REQUIREMENTS.md`")
|
|
611
|
+
expect(message).toContain("clean")
|
|
612
|
+
expect(message).toContain("Overrule")
|
|
613
|
+
expect(message).toContain("unaddressed")
|
|
614
|
+
expect(message).toContain("src/flows/helpers.ts")
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it("names every unreadable path for a by-hand landing", () => {
|
|
618
|
+
const message = renderText(() =>
|
|
619
|
+
seamByHandMessage(["vendor/a.bin", "src/flows/helpers.ts"], ["vendor/a.bin"], "abc123"),
|
|
620
|
+
)
|
|
621
|
+
expect(message).toContain("`vendor/a.bin` (unreadable)")
|
|
622
|
+
expect(message).toContain("`src/flows/helpers.ts`\n")
|
|
623
|
+
expect(message).toContain("git show abc123 --")
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it("keeps a stray path with a newline on one line", () => {
|
|
627
|
+
const message = renderText(() => seamSteerMessage({ strays: ["a\nb.ts"], p: 0.9 }))
|
|
628
|
+
expect(message).toContain('`"a\\nb.ts"`')
|
|
629
|
+
expect(message).not.toContain("a\nb.ts")
|
|
630
|
+
})
|
|
631
|
+
})
|