@pmelab/gtd 27.5.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 +9 -0
- package/dist/chunks/{reserved-DM5W_LAt.mjs → reserved-Df8GeSHw.mjs} +37 -9
- package/dist/chunks/{tail-6gvLXXcg.mjs → tail-DmIfEAI4.mjs} +1 -1
- package/dist/flows.mjs +17 -1
- package/dist/gtd.bundle.mjs +53 -8
- package/dist/testkit.mjs +37 -8
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +61 -10
- 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 +110 -16
- package/package.json +1 -1
- package/src/edge/Edge.test.ts +111 -0
- package/src/edge/Edge.ts +15 -3
- package/src/flags/index.test.ts +13 -0
- package/src/flags/index.ts +1 -0
- 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/workflows/feature/feature.ts +3 -1
- package/src/workflows/feature/risk-tier.feature +301 -0
- package/src/workflows/feature/seam.ts +5 -4
- 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/shared/options.test.ts +6 -1
- package/src/workflows/shared/text.test.ts +129 -0
- package/src/workflows/shared/text.ts +57 -9
- package/src/workflows/shared/vars.ts +2 -0
- package/src/workflows/shared/workflows.steps.ts +8 -0
package/src/replay/Replay.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ScopeOptions,
|
|
11
11
|
type StepRequest,
|
|
12
12
|
type Flow,
|
|
13
|
+
type Mark,
|
|
13
14
|
} from "../flows/index.js"
|
|
14
15
|
import {
|
|
15
16
|
headingSectionBodies,
|
|
@@ -114,12 +115,15 @@ export type ReplayOutcome =
|
|
|
114
115
|
readonly trace: readonly ReachedStep[]
|
|
115
116
|
/** The step the pending turn completed, when one was supplied. */
|
|
116
117
|
readonly landed: ReachedStep | undefined
|
|
118
|
+
/** Marks the flow made after the pending turn's step resolved; [] without a pending turn. */
|
|
119
|
+
readonly marks: readonly Mark[]
|
|
117
120
|
}
|
|
118
121
|
| {
|
|
119
122
|
readonly kind: "ended"
|
|
120
123
|
readonly via: "return" | "restart"
|
|
121
124
|
readonly trace: readonly ReachedStep[]
|
|
122
125
|
readonly landed: ReachedStep | undefined
|
|
126
|
+
readonly marks: readonly Mark[]
|
|
123
127
|
}
|
|
124
128
|
| { readonly kind: "divergence"; readonly message: string }
|
|
125
129
|
| { readonly kind: "refused"; readonly message: string }
|
|
@@ -292,6 +296,7 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
292
296
|
{ readonly hash: string; readonly before: TreeView; readonly after: TreeView }
|
|
293
297
|
>()
|
|
294
298
|
let landed: ReachedStep | undefined
|
|
299
|
+
const marks: Mark[] = []
|
|
295
300
|
let outcome: ReplayOutcome | undefined
|
|
296
301
|
let signal: (() => void) | undefined
|
|
297
302
|
const settled = new Promise<void>((resolve) => {
|
|
@@ -553,7 +558,7 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
553
558
|
const commit = commits[cursor]
|
|
554
559
|
if (commit !== undefined) return consumeCommit(step, commit)
|
|
555
560
|
if (input.pending === undefined || pendingUsed) {
|
|
556
|
-
return finish({ kind: "rest", rest: step, trace, landed })
|
|
561
|
+
return finish({ kind: "rest", rest: step, trace, landed, marks })
|
|
557
562
|
}
|
|
558
563
|
return landPending(step, input.pending)
|
|
559
564
|
}
|
|
@@ -598,7 +603,7 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
598
603
|
kind: "divergence",
|
|
599
604
|
message: `gtd: the flow ended, but commit ${short(commits[cursor]!.hash)} continues the episode — ${DIVERGED}`,
|
|
600
605
|
}
|
|
601
|
-
: { kind: "ended", via, trace, landed }
|
|
606
|
+
: { kind: "ended", via, trace, landed, marks }
|
|
602
607
|
|
|
603
608
|
const changesSince = (hash: string): readonly Change[] => {
|
|
604
609
|
const tree = treeAt(hash)
|
|
@@ -670,6 +675,9 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
|
|
|
670
675
|
.map((f) => f.message),
|
|
671
676
|
}))
|
|
672
677
|
}),
|
|
678
|
+
mark: (name, value) => {
|
|
679
|
+
if (pendingUsed) marks.push({ name, value })
|
|
680
|
+
},
|
|
673
681
|
kept: (name) => {
|
|
674
682
|
const tip = keptTips.get(scoped(name))
|
|
675
683
|
return tip === undefined
|
|
@@ -282,3 +282,57 @@ describe("isRecordableApproval", () => {
|
|
|
282
282
|
expect(isRecordableApproval(of("Ada", "a@x.io", 0))).toBe(false)
|
|
283
283
|
})
|
|
284
284
|
})
|
|
285
|
+
|
|
286
|
+
describe("Gtd-Mark", () => {
|
|
287
|
+
const body = (...lines: string[]) => `gtd(human): a → b\n\n${lines.join("\n")}`
|
|
288
|
+
|
|
289
|
+
it("round-trips in order, names repeating", () => {
|
|
290
|
+
const message = formatCommitMessage({
|
|
291
|
+
actor: "human",
|
|
292
|
+
from: "a",
|
|
293
|
+
to: "b",
|
|
294
|
+
marks: [
|
|
295
|
+
{ name: "risk-tier", value: "high" },
|
|
296
|
+
{ name: "production-signal", value: "error rate = 0" },
|
|
297
|
+
{ name: "production-signal", value: "p95" },
|
|
298
|
+
],
|
|
299
|
+
})
|
|
300
|
+
expect(parseCommitMessage(message).marks).toEqual([
|
|
301
|
+
{ name: "risk-tier", value: "high" },
|
|
302
|
+
{ name: "production-signal", value: "error rate = 0" },
|
|
303
|
+
{ name: "production-signal", value: "p95" },
|
|
304
|
+
])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("is written after Gtd-Judge and before Gtd-Approved, with no Gtd-Format bump", () => {
|
|
308
|
+
const lines = formatCommitMessage({
|
|
309
|
+
actor: "human",
|
|
310
|
+
to: "b",
|
|
311
|
+
judge: [{ id: "v", answer: "x", p: 1 }],
|
|
312
|
+
marks: [{ name: "risk-tier", value: "high" }],
|
|
313
|
+
approved: { step: { name: "s", occurrence: 1 }, name: "Ada", email: "a@x.io" },
|
|
314
|
+
}).split("\n")
|
|
315
|
+
const at = lines.findIndex((l) => l.startsWith("Gtd-Mark:"))
|
|
316
|
+
expect(lines[at - 1]).toMatch(/^Gtd-Judge:/)
|
|
317
|
+
expect(lines[at + 1]).toMatch(/^Gtd-Approved:/)
|
|
318
|
+
expect(lines).toContain("Gtd-Format: 1")
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("writes nothing without marks", () => {
|
|
322
|
+
expect(formatCommitMessage({ actor: "human", to: "b" })).not.toContain("Gtd-Mark")
|
|
323
|
+
expect(parseCommitMessage("gtd(human): a → b").marks).toEqual([])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it("skips a malformed line, a missing =, an empty value or a control character", () => {
|
|
327
|
+
const parsed = parseCommitMessage(
|
|
328
|
+
body(
|
|
329
|
+
"Gtd-Mark: no-equals",
|
|
330
|
+
"Gtd-Mark: Bad_Name=x",
|
|
331
|
+
"Gtd-Mark: empty=",
|
|
332
|
+
"Gtd-Mark: ctl=a\u001bb",
|
|
333
|
+
"Gtd-Mark: ok=fine",
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
expect(parsed.marks).toEqual([{ name: "ok", value: "fine" }])
|
|
337
|
+
})
|
|
338
|
+
})
|
package/src/replay/Trailers.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// written by gtd) is skipped, not fatal — a corrupt line must never make every
|
|
4
4
|
// command unusable.
|
|
5
5
|
|
|
6
|
+
import type { Mark } from "../flows/index.js"
|
|
6
7
|
import { HISTORY_FORMAT } from "./Format.js"
|
|
7
8
|
|
|
8
9
|
const TRANSITION_SEP = " → "
|
|
@@ -73,6 +74,8 @@ export interface CommitMessage {
|
|
|
73
74
|
/** Written only on a process's opening commit. */
|
|
74
75
|
readonly workflow: string | undefined
|
|
75
76
|
readonly judge: readonly JudgeVerdict[]
|
|
77
|
+
/** `Gtd-Mark`, in order; a malformed or control-character line is skipped. */
|
|
78
|
+
readonly marks: readonly Mark[]
|
|
76
79
|
readonly vars: Readonly<Record<string, string>>
|
|
77
80
|
/** Written only on a process's first commit. */
|
|
78
81
|
readonly config: Readonly<Record<string, string>>
|
|
@@ -141,6 +144,7 @@ export const isRecordableApproval = (approval: Approval): boolean => {
|
|
|
141
144
|
const back = decodeApproval(encodeApproval(approval))
|
|
142
145
|
return back !== undefined && encodeApproval(back) === encodeApproval(approval)
|
|
143
146
|
}
|
|
147
|
+
const MARK_RE = /^([a-z][a-z0-9-]*)=(.+)$/
|
|
144
148
|
const FORMAT_RE = /^[1-9][0-9]*$/
|
|
145
149
|
const HASH_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/
|
|
146
150
|
|
|
@@ -153,6 +157,7 @@ interface Trailers {
|
|
|
153
157
|
kept: string | undefined
|
|
154
158
|
approved: Approval | undefined
|
|
155
159
|
readonly judge: JudgeVerdict[]
|
|
160
|
+
readonly marks: Mark[]
|
|
156
161
|
readonly vars: Record<string, string>
|
|
157
162
|
readonly config: Record<string, string>
|
|
158
163
|
readonly cost: CostEntry[]
|
|
@@ -170,6 +175,11 @@ const TRAILER_READERS: Readonly<Record<string, (value: string, into: Trailers) =
|
|
|
170
175
|
const parsed = parseJson(value)
|
|
171
176
|
if (isVerdict(parsed)) into.judge.push(parsed)
|
|
172
177
|
},
|
|
178
|
+
"Gtd-Mark": (value, into) => {
|
|
179
|
+
const kv = MARK_RE.exec(value)
|
|
180
|
+
if (kv !== null && !CONTROL_CHARACTER.test(value))
|
|
181
|
+
into.marks.push({ name: kv[1]!, value: kv[2]! })
|
|
182
|
+
},
|
|
173
183
|
"Gtd-Var": (value, into) => {
|
|
174
184
|
const kv = VAR_RE.exec(value)
|
|
175
185
|
if (kv !== null) into.vars[kv[1]!] = kv[2]!
|
|
@@ -215,6 +225,7 @@ export const parseCommitMessage = (message: string): CommitMessage => {
|
|
|
215
225
|
kept: undefined,
|
|
216
226
|
approved: undefined,
|
|
217
227
|
judge: [],
|
|
228
|
+
marks: [],
|
|
218
229
|
vars: {},
|
|
219
230
|
config: {},
|
|
220
231
|
cost: [],
|
|
@@ -239,7 +250,9 @@ export interface CommitSpec {
|
|
|
239
250
|
readonly config?: Readonly<Record<string, string>>
|
|
240
251
|
readonly cost?: { readonly cost: number; readonly model?: string }
|
|
241
252
|
readonly judge?: readonly JudgeVerdict[]
|
|
242
|
-
/** Written after `Gtd-Judge`, before `Gtd-
|
|
253
|
+
/** Written after `Gtd-Judge`, before `Gtd-Approved`. */
|
|
254
|
+
readonly marks?: readonly Mark[]
|
|
255
|
+
/** Written after `Gtd-Mark`, before `Gtd-Branch`. */
|
|
243
256
|
readonly approved?: Approval
|
|
244
257
|
readonly branch?: string
|
|
245
258
|
readonly version?: string
|
|
@@ -263,6 +276,7 @@ export const formatCommitMessage = (spec: CommitSpec): string => {
|
|
|
263
276
|
)
|
|
264
277
|
}
|
|
265
278
|
for (const verdict of spec.judge ?? []) lines.push(`Gtd-Judge: ${JSON.stringify(verdict)}`)
|
|
279
|
+
for (const m of spec.marks ?? []) lines.push(`Gtd-Mark: ${m.name}=${m.value}`)
|
|
266
280
|
if (spec.approved !== undefined) {
|
|
267
281
|
lines.push(`Gtd-Approved: ${encodeApproval(spec.approved)}`)
|
|
268
282
|
}
|
|
@@ -18,6 +18,7 @@ import { packages } from "./packages.js"
|
|
|
18
18
|
import { architecture, design } from "./planning.js"
|
|
19
19
|
import { buildTail, type ReviewOutcome } from "./review.js"
|
|
20
20
|
import { seamBuild } from "./seam.js"
|
|
21
|
+
import { recordRisk, riskTierOn } from "./tier.js"
|
|
21
22
|
|
|
22
23
|
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
23
24
|
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
@@ -56,9 +57,10 @@ export const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
|
56
57
|
let base = firstBase
|
|
57
58
|
for (;;) {
|
|
58
59
|
await design(base)
|
|
60
|
+
const tier = riskTierOn() ? recordRisk() : undefined
|
|
59
61
|
// GTD_FLAG_NAIVE_PASS: the record is pinned at the process start, so a mid-process flip never moves a process.
|
|
60
62
|
if (vars.flag_naive_pass === "1") {
|
|
61
|
-
await seamBuild()
|
|
63
|
+
await seamBuild(tier)
|
|
62
64
|
} else {
|
|
63
65
|
await architecture()
|
|
64
66
|
await packages()
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_TIER
|
|
3
|
+
|
|
4
|
+
With GTD_FLAG_NAIVE_PASS and GTD_FLAG_RISK_TIER both on, design.triage asks
|
|
5
|
+
for a risk tier and production signals, and the design gate's exit lands them
|
|
6
|
+
as Gtd-Mark trailers: one `risk-tier=<low|high>`, one `production-signal`
|
|
7
|
+
per signal. A missing or unreadable tier is high. With the flag off, nothing
|
|
8
|
+
is asked and no commit carries a Gtd-Mark.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
14
|
+
|
|
15
|
+
Rule: with GTD_FLAG_RISK_TIER on
|
|
16
|
+
|
|
17
|
+
Background:
|
|
18
|
+
Given an environment variable "GTD_FLAG_RISK_TIER" set to "1"
|
|
19
|
+
And a file "src/calc.ts" with:
|
|
20
|
+
"""
|
|
21
|
+
export const add = (a: number, b: number) => a + b
|
|
22
|
+
"""
|
|
23
|
+
And a file "src/calc.test.ts" with:
|
|
24
|
+
"""
|
|
25
|
+
import { expect, it } from "vitest"
|
|
26
|
+
it("adds", () => expect(1 + 1).toBe(2))
|
|
27
|
+
"""
|
|
28
|
+
And the working tree is committed as "feat: add calculator"
|
|
29
|
+
And a file "TODO.md" with:
|
|
30
|
+
"""
|
|
31
|
+
# Add subtract
|
|
32
|
+
|
|
33
|
+
subtract(a, b) returns the difference.
|
|
34
|
+
"""
|
|
35
|
+
And step "unwind" changes nothing
|
|
36
|
+
And step "start-gate.check" changes nothing
|
|
37
|
+
|
|
38
|
+
Scenario: with both flags on the triage prompt asks for the tier and the signals
|
|
39
|
+
When the process is driven
|
|
40
|
+
Then the process rests at "design.triage"
|
|
41
|
+
And the resting step's content contains "## Risk Tier"
|
|
42
|
+
And the resting step's content contains "## Production Signals"
|
|
43
|
+
And the resting step's content contains "unsure means `high`"
|
|
44
|
+
|
|
45
|
+
Scenario: a low tier marks risk-tier=low on the design-exit commit
|
|
46
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
47
|
+
"""
|
|
48
|
+
# Requirements
|
|
49
|
+
|
|
50
|
+
## Risk Tier
|
|
51
|
+
|
|
52
|
+
low — trivial copy change
|
|
53
|
+
"""
|
|
54
|
+
And step "design.gate.answer" changes nothing
|
|
55
|
+
When the process is driven
|
|
56
|
+
Then the process rests at "seam.naive"
|
|
57
|
+
And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
|
|
58
|
+
And the last commit body contains "Gtd-Mark: risk-tier=low"
|
|
59
|
+
|
|
60
|
+
Scenario: a high tier marks the design-exit commit
|
|
61
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
62
|
+
"""
|
|
63
|
+
# Requirements
|
|
64
|
+
|
|
65
|
+
## Risk Tier
|
|
66
|
+
|
|
67
|
+
high — touches payments
|
|
68
|
+
"""
|
|
69
|
+
And step "design.gate.answer" changes nothing
|
|
70
|
+
When the process is driven
|
|
71
|
+
Then the process rests at "seam.naive"
|
|
72
|
+
And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
|
|
73
|
+
And the last commit body contains "Gtd-Mark: risk-tier=high"
|
|
74
|
+
|
|
75
|
+
Scenario Outline: a missing section or an unknown word counts as high
|
|
76
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
77
|
+
"""
|
|
78
|
+
# Requirements
|
|
79
|
+
|
|
80
|
+
<tier section>
|
|
81
|
+
## Concerns
|
|
82
|
+
|
|
83
|
+
### Subtract
|
|
84
|
+
"""
|
|
85
|
+
And step "design.gate.answer" changes nothing
|
|
86
|
+
When the process is driven
|
|
87
|
+
Then the process rests at "seam.naive"
|
|
88
|
+
And the last commit body contains "Gtd-Mark: risk-tier=high"
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
| tier section |
|
|
92
|
+
| |
|
|
93
|
+
| ## Risk Tier\n\nmedium — hmm |
|
|
94
|
+
| ```\n## Risk Tier\n\nlow\n```\n |
|
|
95
|
+
|
|
96
|
+
Scenario Outline: signals written at triage land as marks for either tier
|
|
97
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
98
|
+
"""
|
|
99
|
+
# Requirements
|
|
100
|
+
|
|
101
|
+
## Risk Tier
|
|
102
|
+
|
|
103
|
+
<tier> — reason
|
|
104
|
+
|
|
105
|
+
## Production Signals
|
|
106
|
+
|
|
107
|
+
- error rate on /subtract stays flat
|
|
108
|
+
- p95 latency
|
|
109
|
+
"""
|
|
110
|
+
And step "design.gate.answer" changes nothing
|
|
111
|
+
When the process is driven
|
|
112
|
+
Then the process rests at "seam.naive"
|
|
113
|
+
And the last commit body contains "Gtd-Mark: production-signal=error rate on /subtract stays flat"
|
|
114
|
+
And the last commit body contains "Gtd-Mark: production-signal=p95 latency"
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
| tier |
|
|
118
|
+
| low |
|
|
119
|
+
| high |
|
|
120
|
+
|
|
121
|
+
Scenario Outline: low and high processes produce the same commit subjects to implement.building
|
|
122
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
123
|
+
"""
|
|
124
|
+
# Requirements
|
|
125
|
+
|
|
126
|
+
## Risk Tier
|
|
127
|
+
|
|
128
|
+
<tier> — reason
|
|
129
|
+
"""
|
|
130
|
+
And step "design.gate.answer" changes nothing
|
|
131
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
132
|
+
"""
|
|
133
|
+
export const add = (a: number, b: number) => a + b
|
|
134
|
+
export const subtract = (a: number, b: number) => a - b
|
|
135
|
+
"""
|
|
136
|
+
And judge step "seam.judge" answers:
|
|
137
|
+
"""
|
|
138
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
139
|
+
"""
|
|
140
|
+
When the process is driven
|
|
141
|
+
Then the process rests at "implement.building"
|
|
142
|
+
And the commit subjects end with:
|
|
143
|
+
"""
|
|
144
|
+
gtd(agent): design.triage → design.gate.answer
|
|
145
|
+
gtd(human): design.gate.answer → seam.naive
|
|
146
|
+
gtd(agent): seam.naive → seam.judge
|
|
147
|
+
gtd(judge): seam.judge → implement.building
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
| tier |
|
|
152
|
+
| low |
|
|
153
|
+
| high |
|
|
154
|
+
|
|
155
|
+
Scenario Outline: the implementation prompt demands a flag for high and none for low
|
|
156
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
157
|
+
"""
|
|
158
|
+
# Requirements
|
|
159
|
+
|
|
160
|
+
## Risk Tier
|
|
161
|
+
|
|
162
|
+
<tier> — reason
|
|
163
|
+
"""
|
|
164
|
+
And step "design.gate.answer" changes nothing
|
|
165
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
166
|
+
"""
|
|
167
|
+
export const add = (a: number, b: number) => a + b
|
|
168
|
+
export const subtract = (a: number, b: number) => a - b
|
|
169
|
+
"""
|
|
170
|
+
And judge step "seam.judge" answers:
|
|
171
|
+
"""
|
|
172
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
173
|
+
"""
|
|
174
|
+
When the process is driven
|
|
175
|
+
Then the process rests at "implement.building"
|
|
176
|
+
And the resting step's content contains "<demand>"
|
|
177
|
+
And the resting step's content does not contain "<absent>"
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
| tier | demand | absent |
|
|
181
|
+
| high | Put the new behaviour behind a new release flag | Add no release flag |
|
|
182
|
+
| low | Add no release flag — this change is low risk tier | new release flag |
|
|
183
|
+
|
|
184
|
+
Scenario: the tier comes from the requirements the final design gate lap accepted
|
|
185
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
186
|
+
"""
|
|
187
|
+
# Requirements
|
|
188
|
+
|
|
189
|
+
## Risk Tier
|
|
190
|
+
|
|
191
|
+
low — trivial copy change
|
|
192
|
+
"""
|
|
193
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
194
|
+
"""
|
|
195
|
+
export const add = (a: number, b: number) => a + b
|
|
196
|
+
export const subtract = (a: number, b: number) => a - b
|
|
197
|
+
"""
|
|
198
|
+
And judge step "seam.judge" answers:
|
|
199
|
+
"""
|
|
200
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
201
|
+
"""
|
|
202
|
+
When the process is driven
|
|
203
|
+
Then the process rests at "design.gate.answer"
|
|
204
|
+
Given a file ".gtd/REQUIREMENTS.md" with:
|
|
205
|
+
"""
|
|
206
|
+
# Requirements
|
|
207
|
+
|
|
208
|
+
## Risk Tier
|
|
209
|
+
|
|
210
|
+
high — touches payments
|
|
211
|
+
"""
|
|
212
|
+
And step "design.triage" from now on changes nothing
|
|
213
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
214
|
+
"""
|
|
215
|
+
# Requirements
|
|
216
|
+
|
|
217
|
+
## Risk Tier
|
|
218
|
+
|
|
219
|
+
high — touches payments
|
|
220
|
+
|
|
221
|
+
## Concerns
|
|
222
|
+
|
|
223
|
+
### Subtract
|
|
224
|
+
"""
|
|
225
|
+
And step "design.gate.answer" changes nothing
|
|
226
|
+
When the process is driven
|
|
227
|
+
Then the process rests at "implement.building"
|
|
228
|
+
And the resting step's content contains "Put the new behaviour behind a new release flag"
|
|
229
|
+
And the resting step's content does not contain "Add no release flag"
|
|
230
|
+
|
|
231
|
+
Rule: with GTD_FLAG_RISK_TIER off
|
|
232
|
+
|
|
233
|
+
Background:
|
|
234
|
+
Given an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
235
|
+
And a file "src/calc.ts" with:
|
|
236
|
+
"""
|
|
237
|
+
export const add = (a: number, b: number) => a + b
|
|
238
|
+
"""
|
|
239
|
+
And a file "src/calc.test.ts" with:
|
|
240
|
+
"""
|
|
241
|
+
import { expect, it } from "vitest"
|
|
242
|
+
it("adds", () => expect(1 + 1).toBe(2))
|
|
243
|
+
"""
|
|
244
|
+
And the working tree is committed as "feat: add calculator"
|
|
245
|
+
And a file "TODO.md" with:
|
|
246
|
+
"""
|
|
247
|
+
# Add subtract
|
|
248
|
+
|
|
249
|
+
subtract(a, b) returns the difference.
|
|
250
|
+
"""
|
|
251
|
+
And step "unwind" changes nothing
|
|
252
|
+
And step "start-gate.check" changes nothing
|
|
253
|
+
|
|
254
|
+
Scenario: the flag-off twin asks for no tier and marks no commit
|
|
255
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
256
|
+
"""
|
|
257
|
+
# Requirements
|
|
258
|
+
|
|
259
|
+
## Risk Tier
|
|
260
|
+
|
|
261
|
+
low — trivial
|
|
262
|
+
|
|
263
|
+
## Production Signals
|
|
264
|
+
|
|
265
|
+
- error rate stays flat
|
|
266
|
+
"""
|
|
267
|
+
And step "design.gate.answer" changes nothing
|
|
268
|
+
When the process is driven
|
|
269
|
+
Then the process rests at "seam.naive"
|
|
270
|
+
And no commit body contains "Gtd-Mark"
|
|
271
|
+
|
|
272
|
+
Scenario: the flag-off twin's triage prompt has no tier section
|
|
273
|
+
When the process is driven
|
|
274
|
+
Then the process rests at "design.triage"
|
|
275
|
+
And the resting step's content does not contain "## Risk Tier"
|
|
276
|
+
And the resting step's content does not contain "## Production Signals"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
Scenario: the flag-off twin keeps today's flag demand
|
|
280
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
281
|
+
"""
|
|
282
|
+
# Requirements
|
|
283
|
+
|
|
284
|
+
## Risk Tier
|
|
285
|
+
|
|
286
|
+
low — trivial
|
|
287
|
+
"""
|
|
288
|
+
And step "design.gate.answer" changes nothing
|
|
289
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
290
|
+
"""
|
|
291
|
+
export const add = (a: number, b: number) => a + b
|
|
292
|
+
export const subtract = (a: number, b: number) => a - b
|
|
293
|
+
"""
|
|
294
|
+
And judge step "seam.judge" answers:
|
|
295
|
+
"""
|
|
296
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
297
|
+
"""
|
|
298
|
+
When the process is driven
|
|
299
|
+
Then the process rests at "implement.building"
|
|
300
|
+
And the resting step's content contains "Put the new behaviour behind a new release flag"
|
|
301
|
+
And the resting step's content does not contain "Add no release flag"
|
|
@@ -16,6 +16,7 @@ import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
|
16
16
|
import * as t from "../shared/text.js"
|
|
17
17
|
import { defaults } from "../shared/vars.js"
|
|
18
18
|
import { refactorFirst } from "./refactor.js"
|
|
19
|
+
import type { RiskTier } from "./tier.js"
|
|
19
20
|
|
|
20
21
|
/** Naive-pass retries after a `reject`; the next `reject` rests at `seam.steer`. */
|
|
21
22
|
const NAIVE_RETRIES = 2
|
|
@@ -39,9 +40,9 @@ export const naivePass = (rejection?: t.SeamRejection): Promise<void> =>
|
|
|
39
40
|
system: t.builderSystem(),
|
|
40
41
|
})
|
|
41
42
|
|
|
42
|
-
export const implement = (reference: string): Promise<void> =>
|
|
43
|
+
export const implement = (reference: string, tier?: RiskTier): Promise<void> =>
|
|
43
44
|
scope("implement", async () => {
|
|
44
|
-
await t.agentWithSkills("building", t.implementBuildingPrompt(reference), {
|
|
45
|
+
await t.agentWithSkills("building", t.implementBuildingPrompt(reference, tier), {
|
|
45
46
|
label: "Implementing",
|
|
46
47
|
model: coder(),
|
|
47
48
|
system: t.builderSystem(),
|
|
@@ -94,7 +95,7 @@ const decide = (): Promise<Decision> =>
|
|
|
94
95
|
}
|
|
95
96
|
})
|
|
96
97
|
|
|
97
|
-
export const seamBuild = async (): Promise<void> => {
|
|
98
|
+
export const seamBuild = async (tier?: RiskTier): Promise<void> => {
|
|
98
99
|
const { reference, shape } = await decide()
|
|
99
100
|
if (shape !== undefined && !(await refactorFirst(shape, { reference }))) {
|
|
100
101
|
// Unreadable strays were never judged, and no refactor runs: a person reads the diff.
|
|
@@ -106,5 +107,5 @@ export const seamBuild = async (): Promise<void> => {
|
|
|
106
107
|
}),
|
|
107
108
|
)
|
|
108
109
|
}
|
|
109
|
-
await implement(reference)
|
|
110
|
+
await implement(reference, tier)
|
|
110
111
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { human, run } from "../../flows/index.js"
|
|
2
2
|
import * as t from "../shared/text.js"
|
|
3
|
+
import { riskTierOn } from "./tier.js"
|
|
3
4
|
import { ARCHITECTURE, coder, FEEDBACK, planner, REQUIREMENTS, REVIEW } from "../shared/steps.js"
|
|
4
5
|
|
|
5
6
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
6
7
|
export const triage = (base: string): Promise<void> =>
|
|
7
|
-
t.agentWithSkills("triage", t.designTriagePrompt(base), {
|
|
8
|
+
t.agentWithSkills("triage", t.designTriagePrompt(base, riskTierOn()), {
|
|
8
9
|
label: "Triaging the change",
|
|
9
10
|
file: REQUIREMENTS,
|
|
10
11
|
mode: "qa",
|
|
@@ -30,7 +31,7 @@ export const decompose = (): Promise<void> =>
|
|
|
30
31
|
|
|
31
32
|
export const answerProductQuestions = (): Promise<void> =>
|
|
32
33
|
human("gate.answer", {
|
|
33
|
-
message: t.designGateAnswerMessage(),
|
|
34
|
+
message: t.designGateAnswerMessage(riskTierOn()),
|
|
34
35
|
label: "Awaiting your product answers",
|
|
35
36
|
file: REQUIREMENTS,
|
|
36
37
|
mode: "qa",
|