@pmelab/gtd 42.0.0 → 44.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 +96 -48
- package/claude/hooks/sketch.ts +1 -1
- package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-DJk_6-ju.mjs} +75 -46
- package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
- package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
- package/dist/chunks/{tail-D2o2h5cc.mjs → tail-5w5mxGTq.mjs} +3 -3
- 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 +7 -290
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +27 -7
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +287 -161
- 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/FORMATS.md +4 -4
- 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/coverage.ts +1 -1
- package/src/workflows/feature/feature.test.ts +8 -0
- package/src/workflows/feature/feature.ts +2 -2
- package/src/workflows/feature/index.ts +1 -1
- package/src/workflows/feature/lens-review.feature +16 -4
- package/src/workflows/feature/refactor.ts +1 -1
- package/src/workflows/feature/review-answers.feature +179 -0
- package/src/workflows/feature/seam.ts +1 -1
- package/src/workflows/feature/steps.test.ts +2 -76
- package/src/workflows/feature/steps.ts +5 -48
- package/src/workflows/fix/fix.feature +20 -21
- package/src/workflows/index.ts +1 -1
- package/src/workflows/review/review-door.feature +149 -0
- package/src/workflows/review/review.test.ts +1 -73
- package/src/workflows/review/review.ts +34 -6
- 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/{feature → shared}/review.test.ts +9 -61
- package/src/workflows/{feature → shared}/review.ts +40 -48
- package/src/workflows/shared/skills.test.ts +2 -0
- package/src/workflows/shared/skills.ts +2 -0
- package/src/workflows/shared/steps.test.ts +64 -2
- package/src/workflows/shared/steps.ts +40 -1
- package/src/workflows/shared/tail.ts +2 -2
- package/src/workflows/shared/text.test.ts +40 -89
- package/src/workflows/shared/text.ts +63 -54
|
@@ -5,7 +5,6 @@ import { join } from "node:path"
|
|
|
5
5
|
import { afterEach, describe, expect, it } from "vitest"
|
|
6
6
|
import {
|
|
7
7
|
buildReviewTriageMessage,
|
|
8
|
-
deferNotesScript,
|
|
9
8
|
buildReviewReviewingPrompt,
|
|
10
9
|
agentWithSkills,
|
|
11
10
|
buildFixPrompt,
|
|
@@ -32,8 +31,7 @@ import {
|
|
|
32
31
|
withSkills,
|
|
33
32
|
composedReview,
|
|
34
33
|
} from "./text.js"
|
|
35
|
-
import { steeringFormatFor } from "../../steering/index.js"
|
|
36
|
-
import { applyEmittedScript, InMemRepo } from "../../testkit/index.js"
|
|
34
|
+
import { steeringFormatFor, unansweredNotes } from "../../steering/index.js"
|
|
37
35
|
import { captureStep, renderText } from "./text.fixture.js"
|
|
38
36
|
|
|
39
37
|
describe("withSkills", () => {
|
|
@@ -829,53 +827,6 @@ describe("buildReviewReviewingPrompt risk marking", () => {
|
|
|
829
827
|
})
|
|
830
828
|
})
|
|
831
829
|
|
|
832
|
-
describe("deferNotesScript", () => {
|
|
833
|
-
const run = (notes: Parameters<typeof deferNotesScript>[0], existing?: string): string => {
|
|
834
|
-
const dir = mkdtempSync(join(tmpdir(), "defer-"))
|
|
835
|
-
mkdirSync(join(dir, ".gtd"))
|
|
836
|
-
if (existing !== undefined) writeFileSync(join(dir, ".gtd/TODO.md"), existing)
|
|
837
|
-
execFileSync("sh", ["-c", deferNotesScript(notes, "abcdef1234567890")], { cwd: dir })
|
|
838
|
-
return readFileSync(join(dir, ".gtd/TODO.md"), "utf8")
|
|
839
|
-
}
|
|
840
|
-
const notes = [{ id: "note-1", anchor: "a.ts:3", text: "plain", label: "could: tiny" }]
|
|
841
|
-
|
|
842
|
-
it("creates the file with one section headed by the round's short sha", () => {
|
|
843
|
-
expect(run(notes)).toBe(
|
|
844
|
-
"## Deferred review notes (abcdef1)\n\n- note-1 — a.ts:3 (could: tiny): plain\n",
|
|
845
|
-
)
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
it("appends after a blank line, never truncating", () => {
|
|
849
|
-
const out = run(notes, "# Sketch\n\nkeep me\n")
|
|
850
|
-
expect(out.startsWith("# Sketch\n\nkeep me\n\n## Deferred review notes (abcdef1)\n")).toBe(true)
|
|
851
|
-
})
|
|
852
|
-
|
|
853
|
-
it("separates from a file lacking a trailing newline", () => {
|
|
854
|
-
expect(run(notes, "keep")).toContain("keep\n\n## Deferred")
|
|
855
|
-
})
|
|
856
|
-
|
|
857
|
-
it("lands quotes, dollars and backticks verbatim", () => {
|
|
858
|
-
const text = 'it\'s $HOME `x` "q" \\n'
|
|
859
|
-
const out = run([{ id: "note-1", anchor: "a", text }])
|
|
860
|
-
expect(out).toContain(text)
|
|
861
|
-
})
|
|
862
|
-
|
|
863
|
-
it("is an oxfmt fixed point, for a note longer than one line", () => {
|
|
864
|
-
const long = [
|
|
865
|
-
{
|
|
866
|
-
id: "note-3",
|
|
867
|
-
anchor: "Parser accepts CRLF ./src/parse.ts#30-31",
|
|
868
|
-
text: "rename the helper to something clearer please",
|
|
869
|
-
label: "could",
|
|
870
|
-
},
|
|
871
|
-
]
|
|
872
|
-
const dir = mkdtempSync(join(tmpdir(), "fmt-"))
|
|
873
|
-
const file = join(dir, "TODO.md")
|
|
874
|
-
writeFileSync(file, run(long, "# Sketch\n"))
|
|
875
|
-
execFileSync("npx", ["oxfmt", "--check", file], { cwd: process.cwd() })
|
|
876
|
-
})
|
|
877
|
-
})
|
|
878
|
-
|
|
879
830
|
describe("severity wording", () => {
|
|
880
831
|
it("triage message names the four severity ids", () => {
|
|
881
832
|
const m = renderText(() => buildReviewTriageMessage())
|
|
@@ -888,44 +839,6 @@ describe("severity wording", () => {
|
|
|
888
839
|
expect(p).toContain("capture order")
|
|
889
840
|
expect(p).toContain("(tier: reason)")
|
|
890
841
|
})
|
|
891
|
-
|
|
892
|
-
it("design triage parks deferred review notes", () => {
|
|
893
|
-
expect(renderText(() => designTriagePrompt("base"))).toContain("## Deferred review notes")
|
|
894
|
-
})
|
|
895
|
-
})
|
|
896
|
-
|
|
897
|
-
describe("deferNotesScript — the in-memory recognizer agrees with the real sh script", () => {
|
|
898
|
-
const notes = [
|
|
899
|
-
{ id: "note-1", anchor: "a.ts:3", text: 'it\'s $HOME `x` "q" 100%', label: "could" },
|
|
900
|
-
{
|
|
901
|
-
id: "note-2",
|
|
902
|
-
anchor: "Parser accepts CRLF ./src/parse.ts#30-31",
|
|
903
|
-
text: "rename the helper to something clearer please",
|
|
904
|
-
label: "could",
|
|
905
|
-
},
|
|
906
|
-
]
|
|
907
|
-
const cases: [string, string | undefined][] = [
|
|
908
|
-
["no file", undefined],
|
|
909
|
-
["an empty file", ""],
|
|
910
|
-
["a file without a trailing newline", "keep"],
|
|
911
|
-
["a file with a trailing newline", "# Sketch\n\nkeep\n"],
|
|
912
|
-
]
|
|
913
|
-
|
|
914
|
-
it.each(cases)("%s", (_name, existing) => {
|
|
915
|
-
const script = deferNotesScript(notes, "abcdef1234567890")
|
|
916
|
-
const dir = mkdtempSync(join(tmpdir(), "defer-contract-"))
|
|
917
|
-
if (existing !== undefined) {
|
|
918
|
-
mkdirSync(join(dir, ".gtd"))
|
|
919
|
-
writeFileSync(join(dir, ".gtd/TODO.md"), existing)
|
|
920
|
-
}
|
|
921
|
-
execFileSync("sh", ["-c", script], { cwd: dir })
|
|
922
|
-
const real = readFileSync(join(dir, ".gtd/TODO.md"), "utf8")
|
|
923
|
-
|
|
924
|
-
const repo = new InMemRepo()
|
|
925
|
-
if (existing !== undefined) repo.writeFile(".gtd/TODO.md", existing)
|
|
926
|
-
expect(applyEmittedScript(repo, new Map(), script).ok).toBe(true)
|
|
927
|
-
expect(repo.readFile(".gtd/TODO.md")).toBe(real)
|
|
928
|
-
})
|
|
929
842
|
})
|
|
930
843
|
|
|
931
844
|
describe("buildReviewReviewingPrompt scenarios", () => {
|
|
@@ -1319,6 +1232,15 @@ it("asks the refactor review, and only it, to flag a test-file hunk that is more
|
|
|
1319
1232
|
expect(first).not.toContain("give any test-file hunk its own chunk")
|
|
1320
1233
|
})
|
|
1321
1234
|
|
|
1235
|
+
describe("buildReviewCollectingPrompt answer boxes", () => {
|
|
1236
|
+
it("a hand-edit round folds fix and own answers, other rounds leave them to gtd", () => {
|
|
1237
|
+
const edits = renderText(() => buildReviewCollectingPrompt("cap", undefined, "edits"))
|
|
1238
|
+
expect(edits).toContain("A hand-edit round routes no answer box itself")
|
|
1239
|
+
const triaged = renderText(() => buildReviewCollectingPrompt("cap", undefined, "triaged"))
|
|
1240
|
+
expect(triaged).toContain("Never fold a note's ticked answer box")
|
|
1241
|
+
})
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1322
1244
|
describe("composedReview", () => {
|
|
1323
1245
|
const BASE = "0123456789abcdef0123456789abcdef01234567"
|
|
1324
1246
|
|
|
@@ -1380,6 +1302,35 @@ describe("composedReview", () => {
|
|
|
1380
1302
|
|
|
1381
1303
|
it("names the nit fix on the review after it", () => {
|
|
1382
1304
|
const doc = composedReview(BASE, { notes: [], fixed: [], noise: [], nitFix: "abcdef0123" })
|
|
1383
|
-
expect(doc).toContain(
|
|
1305
|
+
expect(doc).toContain(
|
|
1306
|
+
"## Fixed from your notes\n\nYour nits and fix-as-suggested notes were worked on in abcdef0;",
|
|
1307
|
+
)
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
it("ends every note, and only a note, in its three answer boxes", () => {
|
|
1311
|
+
const doc = composedReview(BASE, {
|
|
1312
|
+
notes: [{ title: "a", body: "- ends in a list" }],
|
|
1313
|
+
fixed: [{ title: "f", commit: "fedcba9876543210" }],
|
|
1314
|
+
noise: [],
|
|
1315
|
+
})
|
|
1316
|
+
expect(doc).toContain(
|
|
1317
|
+
"## a\n\n- ends in a list\n\nTick exactly one:\n\n- [ ] fix as suggested\n- [ ] won't fix\n- [ ] _your answer_\n\n## Fixed automatically",
|
|
1318
|
+
)
|
|
1319
|
+
expect(doc.match(/fix as suggested/g)).toHaveLength(1)
|
|
1320
|
+
expect(unansweredNotes(doc)).toEqual([{ title: "a", line: 5, reason: "tick exactly one box" }])
|
|
1321
|
+
})
|
|
1322
|
+
|
|
1323
|
+
it("leaves out the notes already answered", () => {
|
|
1324
|
+
const doc = composedReview(BASE, {
|
|
1325
|
+
notes: [
|
|
1326
|
+
{ title: "a", body: "1" },
|
|
1327
|
+
{ title: "a", body: "2" },
|
|
1328
|
+
],
|
|
1329
|
+
fixed: [],
|
|
1330
|
+
noise: [],
|
|
1331
|
+
answered: new Set(["a"]),
|
|
1332
|
+
})
|
|
1333
|
+
expect(doc).not.toContain("## a\n")
|
|
1334
|
+
expect(doc).toContain("## a (2)\n\n2")
|
|
1384
1335
|
})
|
|
1385
1336
|
})
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
type JudgeQuestion,
|
|
13
13
|
type SummaryContext,
|
|
14
14
|
} from "../../flows/index.js"
|
|
15
|
+
import {
|
|
16
|
+
ANSWER_FIX,
|
|
17
|
+
ANSWER_LEAD,
|
|
18
|
+
ANSWER_WONT_FIX,
|
|
19
|
+
FREE_TEXT_PLACEHOLDER,
|
|
20
|
+
} from "../../steering/index.js"
|
|
15
21
|
import { escapeRegExp } from "./registry.js"
|
|
16
22
|
import { defaultFlagRegistry } from "./vars.js"
|
|
17
23
|
import type { CollectRound, RemovedAssertions, Requested } from "../../workflow-kit/index.js"
|
|
@@ -116,6 +122,8 @@ export interface ComposedReview {
|
|
|
116
122
|
readonly weakened?: readonly RemovedAssertions[]
|
|
117
123
|
/** The commit that fixed the human's nits, on the review that follows it. */
|
|
118
124
|
readonly nitFix?: string | undefined
|
|
125
|
+
/** Titles of notes the human already answered, left out. */
|
|
126
|
+
readonly answered?: ReadonlySet<string> | undefined
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
const SUMMARY_TITLES = [
|
|
@@ -133,9 +141,9 @@ const unboxed = (body: string): string =>
|
|
|
133
141
|
const short = (hash: string): string => hash.slice(0, 7)
|
|
134
142
|
|
|
135
143
|
/**
|
|
136
|
-
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note,
|
|
137
|
-
*
|
|
138
|
-
* matched to their chunk by title.
|
|
144
|
+
* \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
|
|
145
|
+
* answer boxes, then the fixed and noise lists. Chunk titles are made unique —
|
|
146
|
+
* notes and answers are matched to their chunk by title.
|
|
139
147
|
*/
|
|
140
148
|
export const composedReview = (base: string, report: ComposedReview): string => {
|
|
141
149
|
// Summary titles are taken first, so no note can claim one.
|
|
@@ -147,6 +155,11 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
147
155
|
return name
|
|
148
156
|
}
|
|
149
157
|
const chunk = (title: string, body: string): string => `## ${title}\n\n${body.trim()}`
|
|
158
|
+
// The lead-in line keeps a body that ends in a list from absorbing the boxes.
|
|
159
|
+
const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`
|
|
160
|
+
const notes = report.notes
|
|
161
|
+
.map((n) => ({ ...n, title: unique(n.title) }))
|
|
162
|
+
.filter((n) => report.answered?.has(n.title) !== true)
|
|
150
163
|
const weakened = (report.weakened ?? []).flatMap(({ path, lines }) =>
|
|
151
164
|
lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`),
|
|
152
165
|
)
|
|
@@ -156,7 +169,7 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
156
169
|
: [
|
|
157
170
|
chunk(
|
|
158
171
|
"Fixed from your notes",
|
|
159
|
-
`Your nits were
|
|
172
|
+
`Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`,
|
|
160
173
|
),
|
|
161
174
|
]),
|
|
162
175
|
...(weakened.length === 0
|
|
@@ -167,14 +180,16 @@ export const composedReview = (base: string, report: ComposedReview): string =>
|
|
|
167
180
|
`These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`,
|
|
168
181
|
),
|
|
169
182
|
]),
|
|
170
|
-
...
|
|
183
|
+
...notes.map((n) =>
|
|
171
184
|
chunk(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
n.title,
|
|
186
|
+
`${
|
|
187
|
+
n.unresolved === true
|
|
188
|
+
? `_The fixer left this unresolved._\n\n${unboxed(n.body)}`
|
|
189
|
+
: n.unclear === true
|
|
190
|
+
? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}`
|
|
191
|
+
: unboxed(n.body) || "_No detail given._"
|
|
192
|
+
}\n\n${answerBoxes}`,
|
|
178
193
|
),
|
|
179
194
|
),
|
|
180
195
|
...(report.fixed.length === 0
|
|
@@ -288,10 +303,6 @@ ${stateFileRules}
|
|
|
288
303
|
${footnoteFoldIn}${codeThreadReplies()}
|
|
289
304
|
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
290
305
|
— no other files for notes or output
|
|
291
|
-
- Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
|
|
292
|
-
that the start diff does not touch are parked by review, never folded;
|
|
293
|
-
the human's edits to such a line (or its removal) are input like any
|
|
294
|
-
other part of the start diff
|
|
295
306
|
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
296
307
|
this process — the human's input, folded into the concerns
|
|
297
308
|
below like any other part of the start diff; never a state
|
|
@@ -680,8 +691,12 @@ export const buildReviewAwaitReviewMessage = (base: string): string =>
|
|
|
680
691
|
per finding that needs your judgement — it questions a criterion or the
|
|
681
692
|
approach, or the fixer could not resolve it — then, for reference only, what
|
|
682
693
|
was **fixed automatically** (with the fixing commit) and what was **dropped
|
|
683
|
-
as noise**.
|
|
684
|
-
|
|
694
|
+
as noise**. Each finding chunk ends in three boxes — tick exactly one:
|
|
695
|
+
\`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
|
|
696
|
+
recorded in the commit), or write your own answer over \`_your answer_\` and
|
|
697
|
+
tick it (judged like a note). Every landing here is refused until each
|
|
698
|
+
finding holds exactly one tick. The fixed and noise lists need no answer;
|
|
699
|
+
disagreeing with a fix or a drop means writing a note of your own.
|
|
685
700
|
|
|
686
701
|
Review the diff yourself, with whatever tool you like — gtd checks
|
|
687
702
|
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
@@ -691,8 +706,8 @@ working tree:
|
|
|
691
706
|
git diff ${base}
|
|
692
707
|
When you've been through the whole diff, run \`gtd land\`:
|
|
693
708
|
|
|
694
|
-
- **Sign off** — leave no comment — no note
|
|
695
|
-
\`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
709
|
+
- **Sign off** — answer every finding, leave no other comment — no note
|
|
710
|
+
in \`.gtd/REVIEW.md\`, no code edit — to close the process.
|
|
696
711
|
Every turn commit stays on the
|
|
697
712
|
branch; run \`gtd summary\` afterward for a closing-message
|
|
698
713
|
prompt.
|
|
@@ -860,6 +875,17 @@ The reviewer's risks are:
|
|
|
860
875
|
|
|
861
876
|
${risks.length === 0 ? "(none)\n" : notesCapture(risks)}`
|
|
862
877
|
|
|
878
|
+
export const reviewFeedbackFixPrompt = (): string =>
|
|
879
|
+
`${stateFileRules}
|
|
880
|
+
- Read \`.gtd/REQUIREMENTS.md\` — the concerns the human's review asked for,
|
|
881
|
+
one \`## \` heading each, in build order
|
|
882
|
+
- Build every concern in this one turn; an answered question is context,
|
|
883
|
+
not a concern
|
|
884
|
+
- Where a change is behavioural, add a test that fails without it
|
|
885
|
+
- Leave \`.gtd/REQUIREMENTS.md\` untouched — the next step removes it
|
|
886
|
+
- Leave everything uncommitted and finish your turn
|
|
887
|
+
`
|
|
888
|
+
|
|
863
889
|
export const buildReviewReviewMissingMessage = (commit: string): string =>
|
|
864
890
|
`The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
|
|
865
891
|
nothing to sign off on.
|
|
@@ -894,34 +920,6 @@ const wrapItem = (item: string): string[] => {
|
|
|
894
920
|
return [...out, line]
|
|
895
921
|
}
|
|
896
922
|
|
|
897
|
-
/** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
|
|
898
|
-
export const deferNotesScript = (notes: readonly NoteInput[], round: string): string => {
|
|
899
|
-
const lines = [
|
|
900
|
-
`## Deferred review notes (${round.slice(0, 7)})`,
|
|
901
|
-
"",
|
|
902
|
-
...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`)),
|
|
903
|
-
]
|
|
904
|
-
return `mkdir -p .gtd
|
|
905
|
-
if [ -s .gtd/TODO.md ]; then
|
|
906
|
-
[ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
|
|
907
|
-
printf '\\n' >> .gtd/TODO.md
|
|
908
|
-
fi
|
|
909
|
-
printf '%s\\n' ${lines.map(quote).join(" ")} >> .gtd/TODO.md
|
|
910
|
-
`
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/** The gate right after a deferral: landing clean signs off without these notes. */
|
|
914
|
-
export const buildReviewAwaitAfterDeferMessage = (
|
|
915
|
-
base: string,
|
|
916
|
-
deferred: readonly NoteInput[],
|
|
917
|
-
): string =>
|
|
918
|
-
`${buildReviewAwaitReviewMessage(base)}
|
|
919
|
-
Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
|
|
920
|
-
clean signs off without them:
|
|
921
|
-
|
|
922
|
-
${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
|
|
923
|
-
`
|
|
924
|
-
|
|
925
923
|
export const buildReviewTriageMessage = (): string =>
|
|
926
924
|
`Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
927
925
|
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
@@ -932,12 +930,14 @@ default; a skipped judgment never dismisses a note).
|
|
|
932
930
|
|
|
933
931
|
Also judge each note's severity, one id per note: \`note-<n>.security\`,
|
|
934
932
|
\`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
|
|
935
|
-
\`note-<n>.newConcept\`.
|
|
933
|
+
\`note-<n>.newConcept\`. An own answer under a finding's boxes is judged
|
|
934
|
+
the same way, keyed \`answer-<n>\`.
|
|
936
935
|
`
|
|
937
936
|
const closedFold = (round: CollectRound, target: string): string =>
|
|
938
937
|
round === "edits"
|
|
939
938
|
? `- Fold every hand-edit and every note the human left that round into
|
|
940
|
-
\`${target}\`
|
|
939
|
+
\`${target}\` — a note ticked \`fix as suggested\` or answered in its
|
|
940
|
+
own words included; one ticked \`won't fix\` is dropped
|
|
941
941
|
`
|
|
942
942
|
: round === "notes"
|
|
943
943
|
? `- Fold each note that asks for a change into \`${target}\`
|
|
@@ -954,6 +954,7 @@ export const buildReviewCollectingPrompt = (
|
|
|
954
954
|
/** `"reply"` is the in-gate reply turn; any other round is closed and folds what that round left. */
|
|
955
955
|
readonly round: CollectRound
|
|
956
956
|
},
|
|
957
|
+
round?: CollectRound,
|
|
957
958
|
): string => {
|
|
958
959
|
const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md"
|
|
959
960
|
const fence = refactor
|
|
@@ -1038,12 +1039,20 @@ ${closedFold(refactor.round, target)}`
|
|
|
1038
1039
|
: `- This turn writes \`${target}\` (the folded concerns) and
|
|
1039
1040
|
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
1040
1041
|
in the tree) — you classify, you do not build
|
|
1041
|
-
- Fold every concluded thread
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1042
|
+
- Fold every concluded thread and hand-edit — and each \`edit\` note
|
|
1043
|
+
the capture lists, no other note — into \`${target}\` and
|
|
1044
|
+
delete the folded threads from \`.gtd/REVIEW.md\`; append one \`- A:\`
|
|
1045
|
+
reply to each thread whose last entry is a \`- H:\` question
|
|
1046
|
+
${
|
|
1047
|
+
round === "edits"
|
|
1048
|
+
? `- A hand-edit round routes no answer box itself: fold each note
|
|
1049
|
+
ticked \`fix as suggested\` or answered in its own words, too; drop one
|
|
1050
|
+
ticked \`won't fix\`
|
|
1046
1051
|
`
|
|
1052
|
+
: `- Never fold a note's ticked answer box the capture does not list: gtd
|
|
1053
|
+
routes those itself
|
|
1054
|
+
`
|
|
1055
|
+
}`
|
|
1047
1056
|
}- When the capture names an answering commit, carry each answered
|
|
1048
1057
|
question into \`${target}\`'s \`## Answered Questions\` as
|
|
1049
1058
|
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|