@pmelab/gtd 15.6.0 → 15.7.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/README.md +37 -8
- package/dist/gtd.bundle.mjs +849 -212
- package/package.json +1 -1
- package/src/flows/helpers.ts +46 -0
- package/src/flows/runtime.ts +30 -0
- package/src/flows/scripts.test.ts +16 -0
- package/src/flows/scripts.ts +2 -1
- package/src/workflows/planning.ts +17 -11
- package/src/workflows/prose.ts +33 -7
- package/src/workflows/review.ts +103 -28
- package/src/workflows/text.fixture.ts +6 -1
- package/src/workflows/text.test.ts +68 -0
- package/src/workflows/text.ts +84 -38
- package/src/workflows/threads.test.ts +129 -0
- package/src/workflows/unified.ts +3 -2
package/src/workflows/text.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
agent,
|
|
3
|
+
codeThreads,
|
|
4
|
+
head,
|
|
5
|
+
start,
|
|
6
|
+
vars,
|
|
7
|
+
type AgentOptions,
|
|
8
|
+
type SummaryContext,
|
|
9
|
+
} from "../flows/index.js"
|
|
2
10
|
import {
|
|
3
11
|
skillsPreamble,
|
|
4
12
|
styleBlock,
|
|
@@ -112,13 +120,26 @@ What each change does next (then run \`gtd land\`):
|
|
|
112
120
|
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**review-gate.check**).
|
|
113
121
|
`
|
|
114
122
|
|
|
123
|
+
/** A bullet naming every code thread waiting on the agent; empty when none do. */
|
|
124
|
+
const codeThreadReplies = (): string => {
|
|
125
|
+
const waiting = codeThreads().filter((t) => t.waitingOn === "agent")
|
|
126
|
+
if (waiting.length === 0) return ""
|
|
127
|
+
const list = waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")
|
|
128
|
+
return `
|
|
129
|
+
- Code threads waiting on you (a comment run opening with \`H:\` in a changed
|
|
130
|
+
file) — append exactly one \`A:\` comment line, same token and
|
|
131
|
+
indentation, directly below the thread's last line; or fold a
|
|
132
|
+
concluded thread in and delete its lines:
|
|
133
|
+
${list}`
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
export const designTriagePrompt = (base: string): string =>
|
|
116
137
|
`${styleBlock}
|
|
117
138
|
|
|
118
139
|
${styleFormatContract}
|
|
119
140
|
|
|
120
141
|
${stateFileRules}
|
|
121
|
-
${footnoteFoldIn}
|
|
142
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
122
143
|
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
123
144
|
— no other files for notes or output
|
|
124
145
|
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
@@ -195,27 +216,30 @@ export const designSystem = (): string =>
|
|
|
195
216
|
${agentConduct}`
|
|
196
217
|
|
|
197
218
|
export const designGateAnswerMessage = (): string =>
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
|
|
219
|
+
`This gate stops on every process, even with no open question, so
|
|
220
|
+
you can discuss \`.gtd/REQUIREMENTS.md\`; changing nothing and re-running
|
|
221
|
+
accepts it as written — unless a thread is open (see below).
|
|
201
222
|
|
|
202
223
|
\`.gtd/REQUIREMENTS.md\` holds the concerns under
|
|
203
224
|
development. Each open question under \`## Open Questions\` offers
|
|
204
|
-
a few options plus a \`- [ ] _your answer_\` slot.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
226
|
+
box (\`- [x]\`) per question; for your own answer, replace
|
|
227
|
+
\`_your answer_\` with your text and tick that line. A round that
|
|
228
|
+
only leaves notes or thread replies needs no tick. Stepping is
|
|
229
|
+
refused while a question is unanswered, unless the round adds a
|
|
230
|
+
thread for the agent to answer.
|
|
210
231
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
232
|
+
Moving on is refused while any thread is open — its last entry is
|
|
233
|
+
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
234
|
+
round returns to this same gate so you can read the agent's answer.
|
|
235
|
+
|
|
236
|
+
You can also leave a footnote alongside an answer:
|
|
214
237
|
|
|
215
238
|
${footnoteRules}
|
|
216
239
|
What each change does next (then run \`gtd land\`):
|
|
217
|
-
- **Accept as-is** — change nothing and re-run
|
|
218
|
-
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
240
|
+
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
241
|
+
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
242
|
+
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
219
243
|
`
|
|
220
244
|
|
|
221
245
|
export const architectureAuthorPrompt = (): string =>
|
|
@@ -224,7 +248,7 @@ export const architectureAuthorPrompt = (): string =>
|
|
|
224
248
|
${styleFormatContract}
|
|
225
249
|
|
|
226
250
|
${stateFileRules}
|
|
227
|
-
${footnoteFoldIn}
|
|
251
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
228
252
|
- The only state files this turn touches are
|
|
229
253
|
\`.gtd/ARCHITECTURE.md\` (write it) and \`.gtd/REQUIREMENTS.md\`
|
|
230
254
|
(delete once folded in) — no other files for notes or output
|
|
@@ -309,27 +333,30 @@ ${stateFileRules}
|
|
|
309
333
|
`
|
|
310
334
|
|
|
311
335
|
export const architectureGateAnswerMessage = (): string =>
|
|
312
|
-
`
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
`This gate stops on every process, even with no open question, so
|
|
337
|
+
you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
|
|
338
|
+
accepts it as written — unless a thread is open (see below).
|
|
315
339
|
|
|
316
340
|
\`.gtd/ARCHITECTURE.md\` holds the technical plan under
|
|
317
341
|
development. Each open question under \`## Open Questions\` offers
|
|
318
|
-
a few options plus a \`- [ ] _your answer_\` slot.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
342
|
+
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
343
|
+
box (\`- [x]\`) per question; for your own answer, replace
|
|
344
|
+
\`_your answer_\` with your text and tick that line. A round that
|
|
345
|
+
only leaves notes or thread replies needs no tick. Stepping is
|
|
346
|
+
refused while a question is unanswered, unless the round adds a
|
|
347
|
+
thread for the agent to answer.
|
|
348
|
+
|
|
349
|
+
Moving on is refused while any thread is open — its last entry is
|
|
350
|
+
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
351
|
+
round returns to this same gate so you can read the agent's answer.
|
|
324
352
|
|
|
325
|
-
You can also leave a footnote alongside an answer
|
|
326
|
-
substitutes for ticking a box, which is still required before
|
|
327
|
-
stepping is allowed:
|
|
353
|
+
You can also leave a footnote alongside an answer:
|
|
328
354
|
|
|
329
355
|
${footnoteRules}
|
|
330
356
|
What each change does next (then run \`gtd land\`):
|
|
331
|
-
- **Accept as-is** — change nothing and re-run
|
|
332
|
-
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
357
|
+
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
358
|
+
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
359
|
+
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
333
360
|
`
|
|
334
361
|
|
|
335
362
|
export const packagesItemBuildingPrompt = (pkg: string): string =>
|
|
@@ -535,6 +562,11 @@ When you've been through the whole diff, run \`gtd land\`:
|
|
|
535
562
|
whatever the boxes say. Every turn commit stays on the
|
|
536
563
|
branch; run \`gtd summary\` afterward for a closing-message
|
|
537
564
|
prompt.
|
|
565
|
+
- **Ask a question** — start a thread (\`- H: <question>\`) on a
|
|
566
|
+
footnote. The agent answers inside \`.gtd/REVIEW.md\` and the process
|
|
567
|
+
rests at this same gate again — no revert, no development lap. A
|
|
568
|
+
round that also leaves notes or edits folds those in the same turn
|
|
569
|
+
and answers the thread.
|
|
538
570
|
- **Request changes** — leave a comment: a note on a
|
|
539
571
|
\`.gtd/REVIEW.md\` line, a footnote anchored to a hunk, or a
|
|
540
572
|
direct code edit — to send a FULL development lap
|
|
@@ -549,6 +581,9 @@ When you've been through the whole diff, run \`gtd land\`:
|
|
|
549
581
|
genuinely non-actionable comment (an approving remark with no code
|
|
550
582
|
edit) skips the lap and signs off straight away.
|
|
551
583
|
|
|
584
|
+
Every landing here is refused while a thread is open (its last entry
|
|
585
|
+
is the agent's): reply with a conclusion, or delete the thread.
|
|
586
|
+
|
|
552
587
|
A footnote works the same way here as a line note:
|
|
553
588
|
|
|
554
589
|
${footnoteRules}
|
|
@@ -597,9 +632,16 @@ ${styleFormatContract}
|
|
|
597
632
|
You are judging and classifying a round of review feedback.
|
|
598
633
|
|
|
599
634
|
${stateFileRules}
|
|
600
|
-
${footnoteFoldIn}
|
|
601
|
-
-
|
|
602
|
-
|
|
635
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
636
|
+
- This turn writes \`.gtd/REQUIREMENTS.md\` (the folded concerns) and
|
|
637
|
+
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
638
|
+
in the tree) — you classify, you do not build
|
|
639
|
+
- Fold every concluded thread, note, ticked answer and hand-edit into
|
|
640
|
+
\`.gtd/REQUIREMENTS.md\` and delete the folded threads from
|
|
641
|
+
\`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
|
|
642
|
+
whose last entry is a \`- H:\` question
|
|
643
|
+
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix
|
|
644
|
+
what it reports
|
|
603
645
|
|
|
604
646
|
The raw review material is:
|
|
605
647
|
|
|
@@ -612,10 +654,14 @@ The round is actionable if any of these hold:
|
|
|
612
654
|
|
|
613
655
|
- The human left a note on \`.gtd/REVIEW.md\`. A note is a mandatory
|
|
614
656
|
concern below
|
|
615
|
-
- The human added a code comment this round
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
657
|
+
- The human added a code comment this round. A comment run whose
|
|
658
|
+
first line starts \`H:\` is a THREAD: when its last entry is an
|
|
659
|
+
\`H:\` question, write exactly one \`A:\` comment line, same token
|
|
660
|
+
and indentation, directly below it — no fold-in; a concluded
|
|
661
|
+
thread is folded into the requirements and its comment lines
|
|
662
|
+
deleted. Any other comment, even a plain-prose one, is a one-shot
|
|
663
|
+
concern — describe it, and note the comment line itself is
|
|
664
|
+
transient: it must not survive the lap that satisfies it
|
|
619
665
|
- The human hand-edited non-comment code this round — no longer
|
|
620
666
|
a committed intent to build on, but a sketch like the entry
|
|
621
667
|
commit's own diff. Describe what it was reaching for; expect
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
hasThreadFor,
|
|
4
|
+
installContext,
|
|
5
|
+
requireReplies,
|
|
6
|
+
requireThreadsClosed,
|
|
7
|
+
type CodeThreadInfo,
|
|
8
|
+
type ThreadInfo,
|
|
9
|
+
} from "../flows/index.js"
|
|
10
|
+
|
|
11
|
+
const withThreads = (
|
|
12
|
+
list: readonly ThreadInfo[],
|
|
13
|
+
run: () => void,
|
|
14
|
+
code: readonly CodeThreadInfo[] = [],
|
|
15
|
+
): void => {
|
|
16
|
+
installContext({
|
|
17
|
+
refuse: (message: string): never => {
|
|
18
|
+
throw new Error(message)
|
|
19
|
+
},
|
|
20
|
+
read: () => "doc",
|
|
21
|
+
threads: () => list,
|
|
22
|
+
codeThreads: () => code,
|
|
23
|
+
} as never)
|
|
24
|
+
run()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => installContext(undefined))
|
|
28
|
+
|
|
29
|
+
const open: ThreadInfo = { name: "a", line: 3, waitingOn: "human", faults: [] }
|
|
30
|
+
const asked: ThreadInfo = { name: "b", line: 9, waitingOn: "agent", faults: [] }
|
|
31
|
+
const faulty: ThreadInfo = {
|
|
32
|
+
...asked,
|
|
33
|
+
name: "c",
|
|
34
|
+
line: 14,
|
|
35
|
+
faults: ['Footnote thread "[^c]": two consecutive "H:" entries — entries must alternate'],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("requireThreadsClosed", () => {
|
|
39
|
+
it("refuses while any thread waits on the human, naming each", () => {
|
|
40
|
+
withThreads([open, asked, { name: "c", line: 12, waitingOn: "human", faults: [] }], () => {
|
|
41
|
+
expect(() => requireThreadsClosed("f.md")).toThrow(/f\.md:3: \[\^a\][\s\S]*f\.md:12: \[\^c\]/)
|
|
42
|
+
expect(() => requireThreadsClosed("f.md")).toThrow(/reply with a conclusion, or delete/)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("passes with no open thread", () => {
|
|
47
|
+
withThreads([asked], () => expect(() => requireThreadsClosed("f.md")).not.toThrow())
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("refuses a thread with a syntax fault even when it waits on the agent", () => {
|
|
51
|
+
withThreads([faulty], () => {
|
|
52
|
+
expect(() => requireThreadsClosed("f.md")).toThrow(/f\.md:14: \[\^c\][\s\S]*two consecutive/)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe("requireReplies", () => {
|
|
58
|
+
it("refuses while any thread waits on the agent, naming each", () => {
|
|
59
|
+
withThreads([open, asked], () => {
|
|
60
|
+
expect(() => requireReplies("f.md")).toThrow(/f\.md:9: \[\^b\]/)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("passes when every thread has the agent's reply", () => {
|
|
65
|
+
withThreads([open], () => expect(() => requireReplies("f.md")).not.toThrow())
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("refuses a thread with a syntax fault even when it waits on the human", () => {
|
|
69
|
+
withThreads([{ ...faulty, waitingOn: "human" }], () => {
|
|
70
|
+
expect(() => requireReplies("f.md")).toThrow(/f\.md:14: \[\^c\][\s\S]*two consecutive/)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const codeOpen: CodeThreadInfo = {
|
|
76
|
+
path: "src/a.ts",
|
|
77
|
+
line: 7,
|
|
78
|
+
waitingOn: "human",
|
|
79
|
+
first: "why?",
|
|
80
|
+
faults: [],
|
|
81
|
+
}
|
|
82
|
+
const codeAsked: CodeThreadInfo = { ...codeOpen, line: 20, waitingOn: "agent", first: "how?" }
|
|
83
|
+
const codeFaulty: CodeThreadInfo = {
|
|
84
|
+
...codeOpen,
|
|
85
|
+
line: 30,
|
|
86
|
+
first: "x",
|
|
87
|
+
faults: ['Code thread at src/a.ts:30: two consecutive "H:" entries'],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("code threads at the gates", () => {
|
|
91
|
+
it("an open code thread refuses landing, naming path:line and first entry", () => {
|
|
92
|
+
withThreads(
|
|
93
|
+
[],
|
|
94
|
+
() => expect(() => requireThreadsClosed("f.md")).toThrow(/src\/a\.ts:7: why\?/),
|
|
95
|
+
[codeOpen, codeAsked],
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("a code thread waiting on the agent does not refuse landing", () => {
|
|
100
|
+
withThreads([], () => expect(() => requireThreadsClosed("f.md")).not.toThrow(), [codeAsked])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("a code thread waiting on the agent refuses an agent turn, naming path:line", () => {
|
|
104
|
+
withThreads(
|
|
105
|
+
[],
|
|
106
|
+
() => {
|
|
107
|
+
expect(() => requireReplies("f.md")).toThrow(/src\/a\.ts:20: how\?/)
|
|
108
|
+
expect(() => requireReplies("f.md")).toThrow(/one "A:" comment line/)
|
|
109
|
+
},
|
|
110
|
+
[codeOpen, codeAsked],
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("a fault refuses both ways", () => {
|
|
115
|
+
withThreads([], () => expect(() => requireThreadsClosed("f.md")).toThrow(/src\/a\.ts:30/), [
|
|
116
|
+
codeFaulty,
|
|
117
|
+
])
|
|
118
|
+
withThreads([], () => expect(() => requireReplies("f.md")).toThrow(/two consecutive/), [
|
|
119
|
+
codeFaulty,
|
|
120
|
+
])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("hasThreadFor combines footnote and code threads", () => {
|
|
124
|
+
withThreads([], () => expect(hasThreadFor("agent", "f.md")).toBe(true), [codeAsked])
|
|
125
|
+
withThreads([asked], () => expect(hasThreadFor("agent", "f.md")).toBe(true))
|
|
126
|
+
withThreads([open], () => expect(hasThreadFor("agent", "f.md")).toBe(false), [codeOpen])
|
|
127
|
+
withThreads([open], () => expect(hasThreadFor("human")).toBe(false))
|
|
128
|
+
})
|
|
129
|
+
})
|
package/src/workflows/unified.ts
CHANGED
|
@@ -30,6 +30,7 @@ import * as t from "./text.js"
|
|
|
30
30
|
// Every part is exported for other workflows to compose; see the modules
|
|
31
31
|
// re-exported below.
|
|
32
32
|
|
|
33
|
+
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
33
34
|
export { defaults } from "./vars.js"
|
|
34
35
|
export { agentWithSkills } from "./text.js"
|
|
35
36
|
export * from "./steps.js"
|
|
@@ -53,10 +54,10 @@ export const unwind = (): Promise<void> => {
|
|
|
53
54
|
export const reUnwind = async (
|
|
54
55
|
feedback: Extract<ReviewOutcome, { verdict: "feedback" }>,
|
|
55
56
|
): Promise<void> => {
|
|
56
|
-
const { base, edited } = feedback
|
|
57
|
+
const { base, restoreFrom, edited } = feedback
|
|
57
58
|
const restore = edited.filter((c) => c.status !== "added").map((c) => c.path)
|
|
58
59
|
const remove = edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
59
|
-
await run("re-unwind", restoreScript(base, { restore, remove }), {
|
|
60
|
+
await run("re-unwind", restoreScript(base, { restore, remove }, restoreFrom), {
|
|
60
61
|
label: "Re-unwinding your review edit",
|
|
61
62
|
file: REVIEW,
|
|
62
63
|
base,
|