@pmelab/gtd 42.0.0 → 43.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 +79 -35
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-COEMFEpy.mjs} +35 -46
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-C_xn8U7U.mjs} +1 -1
- package/dist/gtd.bundle.mjs +488 -30
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +447 -27
- package/dist/web/main.js +42 -13
- package/dist/workflow-kit.mjs +61 -25
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +31 -42
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +217 -121
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +33 -1
- package/src/cli/Cli.ts +70 -6
- package/src/cli/History.ts +15 -0
- package/src/cli/Judges.test.ts +373 -0
- package/src/cli/Judges.ts +624 -0
- package/src/cli/Metrics.ts +3 -16
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +9 -0
- package/src/steering/index.ts +12 -1
- package/src/steering/review.test.ts +107 -0
- package/src/steering/review.ts +159 -9
- package/src/testkit/EmittedScriptRecognizer.ts +0 -14
- package/src/testkit/steps.ts +4 -0
- package/src/workflow-kit/review.test.ts +170 -26
- package/src/workflow-kit/review.ts +95 -19
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/review.test.ts +5 -53
- package/src/workflows/feature/review.ts +31 -42
- package/src/workflows/feature/steps.test.ts +0 -12
- package/src/workflows/feature/steps.ts +3 -10
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
- package/src/workflows/shared/access.test.ts +2 -11
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/shared/text.test.ts +40 -89
- package/src/workflows/shared/text.ts +52 -54
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: every note in the review is answered with one tick
|
|
3
|
+
|
|
4
|
+
Each note gtd writes to `.gtd/REVIEW.md` ends in three boxes — fix as
|
|
5
|
+
suggested, won't fix, or the human's own answer written over the
|
|
6
|
+
placeholder. The gate refuses to land until every note holds exactly one
|
|
7
|
+
tick. A fix goes to the nit batch, a won't-fix is dropped and recorded in
|
|
8
|
+
the gate's commit, an own answer is judged like any other note. Nothing is
|
|
9
|
+
deferred to `.gtd/TODO.md`.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
13
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
14
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
15
|
+
And a file "src/calc.ts" with:
|
|
16
|
+
"""
|
|
17
|
+
export const add = (a: number, b: number) => a + b
|
|
18
|
+
"""
|
|
19
|
+
And the working tree is committed as "feat: add calculator"
|
|
20
|
+
And a file "TODO.md" with:
|
|
21
|
+
"""
|
|
22
|
+
# Add subtract
|
|
23
|
+
|
|
24
|
+
subtract(a, b) returns the difference.
|
|
25
|
+
"""
|
|
26
|
+
And step "unwind" changes nothing
|
|
27
|
+
And step "start-gate.check" changes nothing
|
|
28
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
29
|
+
"""
|
|
30
|
+
# Requirements
|
|
31
|
+
|
|
32
|
+
## Concerns
|
|
33
|
+
|
|
34
|
+
### Subtract
|
|
35
|
+
|
|
36
|
+
- `subtract(a, b)` returns `a - b`.
|
|
37
|
+
|
|
38
|
+
## Acceptance Criteria
|
|
39
|
+
|
|
40
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
41
|
+
"""
|
|
42
|
+
And step "design.gate.answer" changes nothing
|
|
43
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
44
|
+
"""
|
|
45
|
+
it("[AC-1] subtracts", () => {})
|
|
46
|
+
"""
|
|
47
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
48
|
+
"""
|
|
49
|
+
export const add = (a: number, b: number) => a + b
|
|
50
|
+
export const subtract = (a: number, b: number) => a - b
|
|
51
|
+
"""
|
|
52
|
+
And step "seam.check" changes nothing
|
|
53
|
+
And step "implement.applying" runs its script
|
|
54
|
+
And step "implement.health.check" changes nothing
|
|
55
|
+
And step "implement.closing" runs its script
|
|
56
|
+
And judge step "implement.coverage" answers:
|
|
57
|
+
"""
|
|
58
|
+
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
59
|
+
"""
|
|
60
|
+
And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
61
|
+
"""
|
|
62
|
+
# Quality
|
|
63
|
+
|
|
64
|
+
## subtract has no negative test
|
|
65
|
+
|
|
66
|
+
Add a test subtracting a larger number.
|
|
67
|
+
|
|
68
|
+
## add could be a method
|
|
69
|
+
|
|
70
|
+
A class would read better.
|
|
71
|
+
|
|
72
|
+
## AC-1 ignores floating point
|
|
73
|
+
|
|
74
|
+
Should subtract round? The criterion does not say.
|
|
75
|
+
"""
|
|
76
|
+
And step "build.quality.sorting" runs its script
|
|
77
|
+
And step "build.review.composing" runs its script
|
|
78
|
+
And judge step "build.quality.triage" answers:
|
|
79
|
+
"""
|
|
80
|
+
[{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"design","p":0.9}]
|
|
81
|
+
"""
|
|
82
|
+
And step "build.review.closing" runs its script
|
|
83
|
+
|
|
84
|
+
Scenario: the note ends in its three answer boxes
|
|
85
|
+
When the process is driven
|
|
86
|
+
Then the process rests at "build.review.await-review"
|
|
87
|
+
And ".gtd/REVIEW.md" contains:
|
|
88
|
+
"""
|
|
89
|
+
## AC-1 ignores floating point
|
|
90
|
+
|
|
91
|
+
Should subtract round? The criterion does not say.
|
|
92
|
+
|
|
93
|
+
Tick exactly one:
|
|
94
|
+
|
|
95
|
+
- [ ] fix as suggested
|
|
96
|
+
- [ ] won't fix
|
|
97
|
+
- [ ] _your answer_
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
Scenario: a sign-off with the note unanswered is refused
|
|
101
|
+
When the process is driven
|
|
102
|
+
Then landing is refused with "answer-completeness: 1 note(s) in .gtd/REVIEW.md not answered"
|
|
103
|
+
And the process rests at "build.review.await-review"
|
|
104
|
+
|
|
105
|
+
Scenario: two ticks on one note are refused
|
|
106
|
+
When the process is driven
|
|
107
|
+
And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
|
|
108
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
109
|
+
Then landing is refused with "tick exactly one box per note"
|
|
110
|
+
|
|
111
|
+
Scenario: a ticked answer still holding the placeholder is refused
|
|
112
|
+
When the process is driven
|
|
113
|
+
And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] _your answer_"
|
|
114
|
+
Then landing is refused with "answer-completeness"
|
|
115
|
+
|
|
116
|
+
Scenario: won't fix drops the note, records it, and signs off
|
|
117
|
+
When the process is driven
|
|
118
|
+
And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
|
|
119
|
+
And gtd lands
|
|
120
|
+
And the process is driven
|
|
121
|
+
Then the process rests at "idle"
|
|
122
|
+
And the commit "gtd(human): build.review.await-review → build.review.closing" has a body containing "Gtd-Mark: wontfix=AC-1 ignores floating point"
|
|
123
|
+
And ".gtd/TODO.md" does not exist
|
|
124
|
+
And no commit body contains "build.review.triage"
|
|
125
|
+
|
|
126
|
+
Scenario: fix as suggested goes to the nit batch unjudged, and the note is not asked again
|
|
127
|
+
Given step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
|
|
128
|
+
"""
|
|
129
|
+
export const add = (a: number, b: number) => a + b
|
|
130
|
+
export const subtract = (a: number, b: number) => Math.round(a - b)
|
|
131
|
+
"""
|
|
132
|
+
And step "build.review.fix.nits.fixing" is prompted with "Should subtract round?"
|
|
133
|
+
And step "build.health.check" changes nothing
|
|
134
|
+
When the process is driven
|
|
135
|
+
And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
|
|
136
|
+
And gtd lands
|
|
137
|
+
And the process is driven
|
|
138
|
+
Then the process rests at "build.review.await-review"
|
|
139
|
+
And no commit body contains "build.review.triage"
|
|
140
|
+
And ".gtd/REVIEW.md" contains "## Fixed from your notes"
|
|
141
|
+
And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
|
|
142
|
+
And ".gtd/TODO.md" does not exist
|
|
143
|
+
When gtd lands
|
|
144
|
+
And the process is driven
|
|
145
|
+
Then the process rests at "idle"
|
|
146
|
+
|
|
147
|
+
Scenario: an own answer is judged like a note
|
|
148
|
+
Given judge step "build.review.triage" answers:
|
|
149
|
+
"""
|
|
150
|
+
[{"id":"answer-1","answer":"nit","p":0.9}]
|
|
151
|
+
"""
|
|
152
|
+
And step "build.review.fix.nits.fixing" is prompted with "round half up"
|
|
153
|
+
And step "build.health.check" changes nothing
|
|
154
|
+
And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
|
|
155
|
+
"""
|
|
156
|
+
export const add = (a: number, b: number) => a + b
|
|
157
|
+
export const subtract = (a: number, b: number) => Math.round(a - b)
|
|
158
|
+
"""
|
|
159
|
+
When the process is driven
|
|
160
|
+
And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] round half up"
|
|
161
|
+
And gtd lands
|
|
162
|
+
And the process is driven
|
|
163
|
+
Then the process rests at "build.review.await-review"
|
|
164
|
+
And the commit subjects end with:
|
|
165
|
+
"""
|
|
166
|
+
gtd(human): build.review.await-review → build.review.triage
|
|
167
|
+
gtd(judge): build.review.triage → build.review.fix.nits.fixing
|
|
168
|
+
gtd(agent): build.review.fix.nits.fixing → build.health.check
|
|
169
|
+
gtd(check): build.health.check → build.review.closing
|
|
170
|
+
gtd(check): build.review.closing → build.review.composing
|
|
171
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
172
|
+
"""
|
|
173
|
+
And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
|
|
174
|
+
|
|
175
|
+
Scenario: deleting a note's boxes instead of answering is refused
|
|
176
|
+
When the process is driven
|
|
177
|
+
And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with ""
|
|
178
|
+
And the text "Tick exactly one:" in ".gtd/REVIEW.md" is replaced with ""
|
|
179
|
+
Then landing is refused with "its answer boxes were deleted"
|
|
@@ -675,7 +675,7 @@ describe("severity routing", () => {
|
|
|
675
675
|
expect(run.prompts.get("review.collecting")).toContain("(critical: forced by `!`)")
|
|
676
676
|
})
|
|
677
677
|
|
|
678
|
-
it("a confident could edit
|
|
678
|
+
it("a confident could edit is still re-planned, labelled could", async () => {
|
|
679
679
|
const run = await drive({
|
|
680
680
|
notes: ["add — quote", "sub", "mul"],
|
|
681
681
|
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
@@ -684,9 +684,10 @@ describe("severity routing", () => {
|
|
|
684
684
|
"review.composing",
|
|
685
685
|
"review.await-review",
|
|
686
686
|
"review.triage",
|
|
687
|
-
"review.
|
|
688
|
-
"review.
|
|
687
|
+
"review.closing",
|
|
688
|
+
"review.collecting",
|
|
689
689
|
])
|
|
690
|
+
expect(run.prompts.get("review.collecting")).toContain("(could)")
|
|
690
691
|
})
|
|
691
692
|
|
|
692
693
|
it("severity answers on a nit leave it a nit", async () => {
|
|
@@ -730,68 +731,19 @@ describe("severity routing", () => {
|
|
|
730
731
|
expect(run.judged).toContain("note-1.security")
|
|
731
732
|
})
|
|
732
733
|
|
|
733
|
-
it("the gate after a deferral accepts a clean land and lists the deferred notes", async () => {
|
|
734
|
-
const run = await drive({
|
|
735
|
-
notes: ["add — quote", "sub", "mul"],
|
|
736
|
-
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
737
|
-
})
|
|
738
|
-
expect(run.gates).toEqual([true, true])
|
|
739
|
-
expect(run.gateMessages[1]).toContain("NOT acted on")
|
|
740
|
-
expect(run.gateMessages[1]).toContain("note-1")
|
|
741
|
-
expect(run.gateMessages[0]).not.toContain("NOT acted on")
|
|
742
|
-
})
|
|
743
|
-
|
|
744
734
|
it("the first round's gate accepts a clean land", async () => {
|
|
745
735
|
const run = await drive({ notes: ["add", "sub", "mul"] })
|
|
746
736
|
expect(run.gates.length).toBeGreaterThan(0)
|
|
747
737
|
expect(run.gates.every((g) => g)).toBe(true)
|
|
748
738
|
})
|
|
749
739
|
|
|
750
|
-
it("a later gate still accepts a clean land after a deferral round with a nit", async () => {
|
|
751
|
-
const run = await drive({
|
|
752
|
-
notes: ["add — quote", "sub — typo", "mul"],
|
|
753
|
-
answers: {
|
|
754
|
-
"note-1": verdict("edit"),
|
|
755
|
-
...sev("note-1", could),
|
|
756
|
-
"note-2": verdict("nit"),
|
|
757
|
-
},
|
|
758
|
-
reviewDocs: [undefined, doc(["add", "sub"])],
|
|
759
|
-
})
|
|
760
|
-
expect(run.log).toContain("review.defer")
|
|
761
|
-
expect(run.log.at(-1)).toBe("review.await-review")
|
|
762
|
-
expect(run.gates).toEqual([true, true])
|
|
763
|
-
})
|
|
764
|
-
|
|
765
|
-
it("a question and a could note in one round: the gate's base is the answering commit", async () => {
|
|
766
|
-
const run = await drive({
|
|
767
|
-
notes: ["add — why?", "sub — quote", "mul"],
|
|
768
|
-
answers: {
|
|
769
|
-
"note-1": verdict("question"),
|
|
770
|
-
"note-2": verdict("edit"),
|
|
771
|
-
...sev("note-2", could),
|
|
772
|
-
},
|
|
773
|
-
passSecondGate: true,
|
|
774
|
-
})
|
|
775
|
-
expect(run.log).toEqual([
|
|
776
|
-
"review.composing",
|
|
777
|
-
"review.await-review",
|
|
778
|
-
"review.triage",
|
|
779
|
-
"review.answer-review-questions",
|
|
780
|
-
"review.defer",
|
|
781
|
-
"review.await-review",
|
|
782
|
-
])
|
|
783
|
-
// c4 is the answering commit, not the defer commit (c5) or the round (c2)
|
|
784
|
-
expect(run.since.at(-1)).toBe("c4")
|
|
785
|
-
})
|
|
786
|
-
|
|
787
740
|
it("a blank reviewSeverityMinP makes every confident could critical", async () => {
|
|
788
741
|
const run = await drive({
|
|
789
742
|
notes: ["add — quote", "sub", "mul"],
|
|
790
743
|
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
791
744
|
vars: { reviewSeverityMinP: "" },
|
|
792
745
|
})
|
|
793
|
-
expect(run.
|
|
794
|
-
expect(run.log).not.toContain("review.defer")
|
|
746
|
+
expect(run.prompts.get("review.collecting")).toContain("(critical: judge unsure)")
|
|
795
747
|
})
|
|
796
748
|
})
|
|
797
749
|
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../shared/steps.js"
|
|
30
30
|
import { closeReview, isOptionalObject, tail, type TailOptions } from "../shared/tail.js"
|
|
31
31
|
import * as t from "../shared/text.js"
|
|
32
|
-
import { answerReviewQuestions, collecting,
|
|
32
|
+
import { answerReviewQuestions, collecting, fixNits } from "./steps.js"
|
|
33
33
|
|
|
34
34
|
export type { ReviewOutcome } from "../../workflow-kit/index.js"
|
|
35
35
|
|
|
@@ -46,6 +46,12 @@ const verdictQuestion = (note: ReviewNote): JudgeQuestion => ({
|
|
|
46
46
|
|
|
47
47
|
type Tier = "critical" | "should" | "could"
|
|
48
48
|
|
|
49
|
+
const TIER_ORDER: Readonly<Record<Tier, number>> = { critical: 0, should: 1, could: 2 }
|
|
50
|
+
|
|
51
|
+
/** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
|
|
52
|
+
const captureKey = (s: Severity | undefined): number =>
|
|
53
|
+
s === undefined ? 2 : TIER_ORDER[s.tier] * 3 + s.rank
|
|
54
|
+
|
|
49
55
|
const SEVERITY_ANSWERS: Readonly<Record<string, readonly string[]>> = {
|
|
50
56
|
security: ["yes", "no"],
|
|
51
57
|
reversibility: ["one-way", "two-way"],
|
|
@@ -60,10 +66,7 @@ interface Severity {
|
|
|
60
66
|
readonly rank: 0 | 1 | 2
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
/**
|
|
64
|
-
* Deferring is the risky direction: anything unanswered, unrecognised, cut or
|
|
65
|
-
* below `minP` is `critical`, never `could`.
|
|
66
|
-
*/
|
|
69
|
+
/** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
|
|
67
70
|
export const severityOf = (note: ReviewNote, judgment: Judgment, minP: number): Severity => {
|
|
68
71
|
const ans = (q: string) => judgment.answers[`${note.id}.${q}`]
|
|
69
72
|
const a = (q: string): string | undefined => ans(q)?.answer
|
|
@@ -125,7 +128,7 @@ const severityQuestions = (note: ReviewNote): JudgeQuestion[] => [
|
|
|
125
128
|
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
126
129
|
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
127
130
|
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
128
|
-
* non-`edit` note are ignored
|
|
131
|
+
* non-`edit` note are ignored; on an `edit` they order the collecting turn.
|
|
129
132
|
*/
|
|
130
133
|
const triage =
|
|
131
134
|
(severities: Map<string, Severity>) =>
|
|
@@ -155,9 +158,8 @@ const triage =
|
|
|
155
158
|
? undefined
|
|
156
159
|
: VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP))
|
|
157
160
|
if (verdict !== undefined && !truncated.includes(n.id)) return [n.id, verdict]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return [n.id, severity.tier === "could" ? "defer" : "edit"]
|
|
161
|
+
severities.set(n.id, severityOf(n, judgment, severityMinP))
|
|
162
|
+
return [n.id, "edit"]
|
|
161
163
|
}),
|
|
162
164
|
)
|
|
163
165
|
}
|
|
@@ -167,13 +169,15 @@ export const isCode = (path: string): boolean => path !== ".gtd" && !path.starts
|
|
|
167
169
|
/**
|
|
168
170
|
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
169
171
|
* turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
|
|
170
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept
|
|
172
|
+
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
173
|
+
* a fresh one leaves out the notes the human already answered.
|
|
171
174
|
*/
|
|
172
175
|
const composing = (
|
|
173
176
|
base: string,
|
|
174
177
|
report: LensReport,
|
|
175
178
|
carry?: string,
|
|
176
179
|
nitFix?: string,
|
|
180
|
+
answered: ReadonlySet<string> = new Set(),
|
|
177
181
|
): Promise<void> =>
|
|
178
182
|
run(
|
|
179
183
|
"review.composing",
|
|
@@ -184,6 +188,7 @@ const composing = (
|
|
|
184
188
|
...report,
|
|
185
189
|
weakened: kit.removedAssertions(changesSince(base)),
|
|
186
190
|
nitFix,
|
|
191
|
+
answered,
|
|
187
192
|
}),
|
|
188
193
|
[QUALITY],
|
|
189
194
|
)
|
|
@@ -193,9 +198,10 @@ const composing = (
|
|
|
193
198
|
|
|
194
199
|
/**
|
|
195
200
|
* The quality lap's notes reach a human over everything since `base`, who
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
201
|
+
* answers each one — fix as suggested, won't fix, or their own answer — and
|
|
202
|
+
* may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
|
|
203
|
+
* answer or comment is judged: edits go to another planning lap, questions
|
|
204
|
+
* are answered in place, nits fixed in one batch, praise dropped.
|
|
199
205
|
*/
|
|
200
206
|
export const review = (
|
|
201
207
|
base: string,
|
|
@@ -210,42 +216,26 @@ export const review = (
|
|
|
210
216
|
"gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
|
|
211
217
|
}
|
|
212
218
|
const severities = new Map<string, Severity>()
|
|
213
|
-
// Armed by a defer-only round to pick the next gate's message; a fresh
|
|
214
|
-
// review (`reviewing`) disarms it.
|
|
215
|
-
let deferred: readonly t.NoteInput[] = []
|
|
216
219
|
// Set by a nit fix, so the fresh review that follows names its commit.
|
|
217
220
|
let nitFix: string | undefined
|
|
221
|
+
// Every note the human answered in this review, left out of a fresh one.
|
|
222
|
+
const answered = new Set<string>()
|
|
218
223
|
return kit.review(base, {
|
|
219
224
|
file: REVIEW,
|
|
220
225
|
feedback: feedback.file,
|
|
221
226
|
isCode,
|
|
222
227
|
reviewing: (b, carry) => {
|
|
223
|
-
deferred = []
|
|
224
228
|
const fixedAt = nitFix
|
|
225
229
|
nitFix = undefined
|
|
226
|
-
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt)
|
|
230
|
+
return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered)
|
|
227
231
|
},
|
|
228
232
|
fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
|
|
229
233
|
keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
|
|
230
|
-
awaitReview: (b) =>
|
|
231
|
-
const notes = deferred
|
|
232
|
-
deferred = []
|
|
233
|
-
return notes.length > 0
|
|
234
|
-
? awaitReview(b, t.buildReviewAwaitAfterDeferMessage(b, notes))
|
|
235
|
-
: awaitReview(b)
|
|
236
|
-
},
|
|
234
|
+
awaitReview: (b) => awaitReview(b),
|
|
237
235
|
reviewMissing,
|
|
238
236
|
collecting: feedback.collecting,
|
|
239
237
|
triage: triage(severities),
|
|
240
|
-
|
|
241
|
-
deferred = notes.map((n) => ({
|
|
242
|
-
id: n.id,
|
|
243
|
-
anchor: n.anchor,
|
|
244
|
-
text: n.text,
|
|
245
|
-
label: severities.get(n.id)!.reason,
|
|
246
|
-
}))
|
|
247
|
-
return deferNotes(deferred, head())
|
|
248
|
-
},
|
|
238
|
+
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
249
239
|
answerQuestions: answerReviewQuestions,
|
|
250
240
|
fixNits: async (notes) => {
|
|
251
241
|
await guarded((n: typeof notes) => fixNits(n, fixAccess, rules), guard)(notes)
|
|
@@ -259,14 +249,13 @@ export const review = (
|
|
|
259
249
|
t.reviewEditNotesCapture(
|
|
260
250
|
round,
|
|
261
251
|
edits
|
|
262
|
-
.map((n, i) => ({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
252
|
+
.map((n, i) => ({
|
|
253
|
+
n,
|
|
254
|
+
i,
|
|
255
|
+
s: severities.get(n.id),
|
|
256
|
+
key: captureKey(severities.get(n.id)),
|
|
257
|
+
}))
|
|
258
|
+
.sort((x, y) => x.key - y.key || x.i - y.i)
|
|
270
259
|
.map(({ n, s }) => ({
|
|
271
260
|
id: n.id,
|
|
272
261
|
anchor: n.anchor,
|
|
@@ -111,18 +111,6 @@ describe("the feature workflow's own steps declare skills — a step's rendered
|
|
|
111
111
|
expect(scopes).toEqual([{ name: "review.fix.nits", access }])
|
|
112
112
|
expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
|
|
113
113
|
})
|
|
114
|
-
|
|
115
|
-
it("deferNotes rests at build.review.defer as a script writing .gtd/TODO.md", async () => {
|
|
116
|
-
const request = await capture(
|
|
117
|
-
() =>
|
|
118
|
-
steps.deferNotes([{ id: "note-1", anchor: "a", text: "x", label: "could" }], "abcdef123"),
|
|
119
|
-
"build",
|
|
120
|
-
)
|
|
121
|
-
if (request.kind !== "run") throw new Error("unreachable")
|
|
122
|
-
expect(request.name).toBe("review.defer")
|
|
123
|
-
expect(request.body).toContain("## Deferred review notes (abcdef1)")
|
|
124
|
-
expect(request.options).toMatchObject({ file: ".gtd/TODO.md" })
|
|
125
|
-
})
|
|
126
114
|
})
|
|
127
115
|
|
|
128
116
|
describe("the feature workflow's planning steps run on the planner model", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { human,
|
|
1
|
+
import { human, scope, type ScopeAccess } from "../../flows/index.js"
|
|
2
2
|
import type { CollectRound } from "../../workflow-kit/index.js"
|
|
3
3
|
import * as t from "../shared/text.js"
|
|
4
4
|
import { riskTierOn } from "./tier.js"
|
|
@@ -52,8 +52,8 @@ export const fixNits = (
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
55
|
-
export const collecting = (capture: string): Promise<void> =>
|
|
56
|
-
t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
|
|
55
|
+
export const collecting = (capture: string, round?: CollectRound): Promise<void> =>
|
|
56
|
+
t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture, undefined, round), {
|
|
57
57
|
label: "Collecting your feedback",
|
|
58
58
|
file: REQUIREMENTS,
|
|
59
59
|
mode: "qa",
|
|
@@ -79,10 +79,3 @@ export const refactorCollecting = (
|
|
|
79
79
|
allowEmpty: true,
|
|
80
80
|
},
|
|
81
81
|
)
|
|
82
|
-
|
|
83
|
-
/** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
|
|
84
|
-
export const deferNotes = (notes: readonly t.NoteInput[], round: string): Promise<void> =>
|
|
85
|
-
run("review.defer", t.deferNotesScript(notes, round), {
|
|
86
|
-
label: "Deferring could-notes to TODO.md",
|
|
87
|
-
file: ".gtd/TODO.md",
|
|
88
|
-
})
|
|
@@ -45,11 +45,12 @@ You are judging and classifying a round of review feedback.
|
|
|
45
45
|
- This turn writes `.gtd/REQUIREMENTS.md` (the folded concerns) and
|
|
46
46
|
replies inside `.gtd/REVIEW.md` (thread replies only; the file stays
|
|
47
47
|
in the tree) — you classify, you do not build
|
|
48
|
-
- Fold every concluded thread
|
|
49
|
-
|
|
50
|
-
`.gtd/
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
- Fold every concluded thread and hand-edit — and each `edit` note
|
|
49
|
+
the capture lists, no other note — into `.gtd/REQUIREMENTS.md` and
|
|
50
|
+
delete the folded threads from `.gtd/REVIEW.md`; append one `- A:`
|
|
51
|
+
reply to each thread whose last entry is a `- H:` question
|
|
52
|
+
- Never fold a note's ticked answer box the capture does not list: gtd
|
|
53
|
+
routes those itself
|
|
53
54
|
- When the capture names an answering commit, carry each answered
|
|
54
55
|
question into `.gtd/REQUIREMENTS.md`'s `## Answered Questions` as
|
|
55
56
|
`### <the note>` plus its answer — the lap replaces `.gtd/REVIEW.md`
|
|
@@ -11,7 +11,6 @@ describe("the bundled workflow's access export", () => {
|
|
|
11
11
|
expect(bundled).toEqual({
|
|
12
12
|
design: { write: [] },
|
|
13
13
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
14
|
-
"build.review.defer": { write: [".gtd/TODO.md"] },
|
|
15
14
|
"build.review.fix.nits": {},
|
|
16
15
|
"build.review.fix.risks": {},
|
|
17
16
|
"build.quality.correctness": { write: [] },
|
|
@@ -21,7 +20,6 @@ describe("the bundled workflow's access export", () => {
|
|
|
21
20
|
"build.quality.conventions": { write: [] },
|
|
22
21
|
"build.quality.spec-challenge": { write: [] },
|
|
23
22
|
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
24
|
-
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
25
23
|
"refactor.build.quality.correctness": { write: [] },
|
|
26
24
|
"refactor.build.quality.ponytail-review": { write: [] },
|
|
27
25
|
"refactor.build.quality.conventions": { write: [] },
|
|
@@ -32,7 +30,6 @@ describe("the bundled workflow's access export", () => {
|
|
|
32
30
|
expect(bundled).not.toHaveProperty(["refactor.build.review.fix.nits"])
|
|
33
31
|
expect(bundled).not.toHaveProperty(["refactor.build.review.fix.risks"])
|
|
34
32
|
expect(bundled["refactor.build.review"]).toEqual({ write: [".gtd/REVIEW.md"] })
|
|
35
|
-
expect(bundled["refactor.build.review.defer"]).toEqual({ write: [".gtd/TODO.md"] })
|
|
36
33
|
})
|
|
37
34
|
|
|
38
35
|
it("keys one lens scope per qualityReviews entry", () => {
|
|
@@ -54,17 +51,11 @@ describe("the bundled workflow's access export", () => {
|
|
|
54
51
|
"refactor.build.quality.correctness",
|
|
55
52
|
"refactor.build.quality.my-lens",
|
|
56
53
|
"refactor.build.review",
|
|
57
|
-
"refactor.build.review.defer",
|
|
58
54
|
])
|
|
59
55
|
})
|
|
60
56
|
|
|
61
|
-
it("keys only turn
|
|
62
|
-
|
|
63
|
-
const known = [
|
|
64
|
-
...Object.keys(workflow.skills(defaults)),
|
|
65
|
-
"build.review.defer",
|
|
66
|
-
"refactor.build.review.defer",
|
|
67
|
-
]
|
|
57
|
+
it("keys only turn scopes", () => {
|
|
58
|
+
const known = Object.keys(workflow.skills(defaults))
|
|
68
59
|
for (const key of Object.keys(bundled)) expect(known).toContain(key)
|
|
69
60
|
})
|
|
70
61
|
})
|
|
@@ -13,12 +13,10 @@ export const access = (
|
|
|
13
13
|
): Readonly<Record<string, ScopeAccess>> => ({
|
|
14
14
|
design: { write: [] },
|
|
15
15
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
16
|
-
"build.review.defer": { write: [".gtd/TODO.md"] },
|
|
17
16
|
// `{}` reopens what `build.review` restricted: the fixes edit code.
|
|
18
17
|
"build.review.fix.nits": {},
|
|
19
18
|
"build.review.fix.risks": {},
|
|
20
19
|
"refactor.build.review": { write: [".gtd/REVIEW.md"] },
|
|
21
|
-
"refactor.build.review.defer": { write: [".gtd/TODO.md"] },
|
|
22
20
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
|
|
23
21
|
...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({
|
|
24
22
|
write: [],
|