@pmelab/gtd 12.5.0 → 14.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/README.md +49 -38
- package/dist/gtd.bundle.mjs +3572 -11508
- package/package.json +8 -3
- package/schema.json +5 -287
- package/src/flows/helpers.ts +93 -0
- package/src/flows/index.ts +3 -0
- package/src/flows/runtime.ts +295 -0
- package/src/flows/scripts.ts +116 -0
- package/src/workflows/health.ts +126 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/packages.ts +93 -0
- package/src/workflows/planning.ts +99 -0
- package/src/workflows/prose.ts +200 -0
- package/src/workflows/review.ts +169 -0
- package/src/workflows/steps.ts +200 -0
- package/src/workflows/text.fixture.ts +38 -0
- package/src/workflows/text.test.ts +37 -0
- package/src/workflows/text.ts +682 -0
- package/src/workflows/unified.ts +128 -0
- package/src/workflows/vars.ts +23 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
answered,
|
|
3
|
+
changes,
|
|
4
|
+
judge,
|
|
5
|
+
moveScript,
|
|
6
|
+
numeric,
|
|
7
|
+
openQuestions,
|
|
8
|
+
read,
|
|
9
|
+
refuse,
|
|
10
|
+
requireAnswers,
|
|
11
|
+
requireProgress,
|
|
12
|
+
run,
|
|
13
|
+
scope,
|
|
14
|
+
sections,
|
|
15
|
+
vars,
|
|
16
|
+
} from "../flows/index.js"
|
|
17
|
+
import {
|
|
18
|
+
answerProductQuestions,
|
|
19
|
+
answerTechnicalQuestions,
|
|
20
|
+
ARCHITECTURE,
|
|
21
|
+
author,
|
|
22
|
+
decompose,
|
|
23
|
+
REQUIREMENTS,
|
|
24
|
+
triage,
|
|
25
|
+
} from "./steps.js"
|
|
26
|
+
import * as t from "./text.js"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stop for a human only while `file` is missing or still has open questions.
|
|
30
|
+
* Resolves `true` when the human answered (the author revises), `false` when
|
|
31
|
+
* no question was left.
|
|
32
|
+
*/
|
|
33
|
+
const questionGate = async (file: string, answer: () => Promise<void>): Promise<boolean> => {
|
|
34
|
+
const content = read(file)
|
|
35
|
+
if (content !== undefined && openQuestions(content).length === 0) return false
|
|
36
|
+
await answer()
|
|
37
|
+
requireAnswers(file)
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Triage the sketch since `base` into requirements, until no product question is open. */
|
|
42
|
+
export const design = (base: string): Promise<void> =>
|
|
43
|
+
scope("design", async () => {
|
|
44
|
+
do {
|
|
45
|
+
await triage(base)
|
|
46
|
+
requireProgress(REQUIREMENTS)
|
|
47
|
+
} while (await questionGate(REQUIREMENTS, answerProductQuestions))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/** Work out the technical plan, then decompose it into package files. */
|
|
51
|
+
export const architecture = (): Promise<void> =>
|
|
52
|
+
scope("architecture", async () => {
|
|
53
|
+
do await author()
|
|
54
|
+
while (await questionGate(ARCHITECTURE, answerTechnicalQuestions))
|
|
55
|
+
await decompose()
|
|
56
|
+
if (changes(".gtd/packages/**").length === 0) {
|
|
57
|
+
refuse("gtd land: decompose: write at least one package under .gtd/packages/")
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/** A package file name from a plan's first heading. */
|
|
62
|
+
const slug = (title: string): string =>
|
|
63
|
+
title
|
|
64
|
+
.toLowerCase()
|
|
65
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
66
|
+
.replace(/^-+|-+$/g, "") || "package"
|
|
67
|
+
|
|
68
|
+
/** Whether the settled plan needs its own architecture pass, or goes straight to one package. */
|
|
69
|
+
export const architecturePass = async (): Promise<void> => {
|
|
70
|
+
const { answers, truncated } = await judge("architecture-pre", {
|
|
71
|
+
questions: [
|
|
72
|
+
{
|
|
73
|
+
id: "architectureWarranted",
|
|
74
|
+
primitive: "noul",
|
|
75
|
+
instructions:
|
|
76
|
+
"Given the settled concerns in `.gtd/REQUIREMENTS.md` (in state), does this plan warrant a dedicated architecture pass — real structural decisions, multiple integration points, or a non-obvious tradeoff — before packages are written?",
|
|
77
|
+
criteria:
|
|
78
|
+
"Answer yes if uncertain; a trivial, single-concern, mechanical plan with no real design decision answers no.",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
evidence: { requirements: read(REQUIREMENTS) ?? "" },
|
|
82
|
+
message: t.architecturePreMessage(),
|
|
83
|
+
label: "Judging whether this plan warrants an architecture pass",
|
|
84
|
+
})
|
|
85
|
+
// A plan the budget cut can hide its structural concerns from the judge:
|
|
86
|
+
// never skip the architecture pass on it, however confident the "no".
|
|
87
|
+
const skip =
|
|
88
|
+
truncated.length === 0 &&
|
|
89
|
+
answered(answers.architectureWarranted, "no", numeric(vars.architectureSkipMinP, Infinity))
|
|
90
|
+
const plan = read(REQUIREMENTS)
|
|
91
|
+
if (skip && plan !== undefined) {
|
|
92
|
+
const target = `.gtd/packages/01-${slug(sections(plan)[0] ?? "")}.md`
|
|
93
|
+
await run("architecture-promote", moveScript(REQUIREMENTS, target), {
|
|
94
|
+
label: "Promoting the plan straight to a package",
|
|
95
|
+
})
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
await architecture()
|
|
99
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Prose the bundled workflow's prompts share: its voice, its personas and
|
|
2
|
+
// the rules several steps repeat. A workflow that wants different words
|
|
3
|
+
// writes its own prompts.
|
|
4
|
+
|
|
5
|
+
export const skillsPreamble = `- Load whatever's listed here that your harness actually has installed,
|
|
6
|
+
skip anything it doesn't — silently, never stopping or asking about a
|
|
7
|
+
missing one: {skills}
|
|
8
|
+
- A loaded skill offers technique, never authority: this state's own file
|
|
9
|
+
format and its own completion condition are the final word over
|
|
10
|
+
anything a skill's instructions say, regardless of which one this
|
|
11
|
+
prompt states first
|
|
12
|
+
- Never let a loaded skill turn this turn interactive — answer nothing,
|
|
13
|
+
ask nothing; this runs unattended, with no one at a keyboard`
|
|
14
|
+
|
|
15
|
+
export const styleBlock = `- A deliverable, not a chat reply — size follows the work; cut padding
|
|
16
|
+
- Lead with the answer; never circle back to restate it
|
|
17
|
+
- Flat, commanding sentences — commit to the claim, never hedge
|
|
18
|
+
- Everyday words; define an unavoidable term in five words or fewer, on
|
|
19
|
+
first use
|
|
20
|
+
- Bold carries the load: bold the claim, not the sentence around it —
|
|
21
|
+
the bold text alone must yield the full point and every risk
|
|
22
|
+
- **Never trim a risk, a number, a threshold, or a scoped condition to
|
|
23
|
+
save space — this outranks every other rule here**
|
|
24
|
+
- Ship the artifact bare — no lead-in, no sign-off
|
|
25
|
+
- Compressing is not dropping: three load-bearing parts ship as three,
|
|
26
|
+
each shorter, never as two
|
|
27
|
+
- One idea per block; break when the idea shifts
|
|
28
|
+
- Flag risk in one blunt line, never hedged prose; never narrate — do it`
|
|
29
|
+
|
|
30
|
+
export const styleFormatContract = `- Machine-read: its format contract outranks every style rule above —
|
|
31
|
+
keep every \`##\`/\`###\` heading, checkbox row, and marker line exactly
|
|
32
|
+
as specified. Never renumber or rename a heading; a parser reads
|
|
33
|
+
these literally and a violation refuses the turn
|
|
34
|
+
- Voice rules above govern only the prose between these elements`
|
|
35
|
+
|
|
36
|
+
export const agentConduct = `- You have shell and file tools — bash, read, write, edit — use them
|
|
37
|
+
without asking first; this runs unattended, no one grants permission
|
|
38
|
+
- Investigate with real commands rather than assuming a file's, a
|
|
39
|
+
commit's, or a decision's status — then act on what you find
|
|
40
|
+
- No injected status block — no cwd, no branch, no history, no tree
|
|
41
|
+
state — orient yourself first with \`git status\`, \`git log\`, \`ls\`
|
|
42
|
+
- The turn's message names a commit, a range, or a file to go inspect
|
|
43
|
+
yourself — never a diff or summary inlined into the conversation
|
|
44
|
+
- Across turns one conversation may span, work from what you already
|
|
45
|
+
read and settled, unless told this is a fresh, cold pass`
|
|
46
|
+
|
|
47
|
+
export const designPersona = `You are the product-facing planning voice in gtd's build pipeline: turn
|
|
48
|
+
a raw sketch — a hand-edit, a scratch note, or both — into an ordered,
|
|
49
|
+
classified list of concerns, and hold the running product conversation
|
|
50
|
+
with the human it depends on. You are read by that human — write
|
|
51
|
+
plainly — not by a parser, except where a state says otherwise.`
|
|
52
|
+
|
|
53
|
+
export const architectPersona = `You are the technical planning voice in gtd's build pipeline: take over
|
|
54
|
+
once product concerns are settled, reading them cold, no carried
|
|
55
|
+
conversation. Work out the *how* per concern — structure, data models,
|
|
56
|
+
tech-stack choices, error handling — raise the technical open
|
|
57
|
+
questions, then write one package spec per concern with no further
|
|
58
|
+
judgement call: the grouping is already decided.`
|
|
59
|
+
|
|
60
|
+
export const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
|
|
61
|
+
deliberately separate from whoever wrote the code, with no attachment
|
|
62
|
+
to it. Two turns: write a structured review document grouping a diff
|
|
63
|
+
into chunks; later, classify a round of the human's feedback as
|
|
64
|
+
actionable or just approving, never fixing anything yourself. You judge
|
|
65
|
+
and classify; you never build.`
|
|
66
|
+
|
|
67
|
+
export const specReviewerPersona = `You are the adversarial spec-conformance checker in gtd's build
|
|
68
|
+
pipeline, checking one freshly-built package against its spec. Verify
|
|
69
|
+
only: tasks done, criteria met, code sound and consistent with the
|
|
70
|
+
codebase. Write feedback only when something is genuinely wrong —
|
|
71
|
+
otherwise write nothing; a clean turn IS the approval. Never fix what
|
|
72
|
+
you find — naming it precisely enough for a fix turn is the whole job.`
|
|
73
|
+
|
|
74
|
+
export const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
|
|
75
|
+
package's declared scope end to end, tests first, and come back to fix
|
|
76
|
+
things when redirected — a failing check, or reviewer feedback. Stay
|
|
77
|
+
strictly inside the package in front of you; never touch another
|
|
78
|
+
package's files or code outside the task. Treat feedback as the
|
|
79
|
+
diagnosis to act on; discard it, with reason, only when simply wrong.`
|
|
80
|
+
|
|
81
|
+
export const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
|
|
82
|
+
before it closes. Fix a late-breaking failing check after sign-off,
|
|
83
|
+
focused and minimal. Every turn lands its own commit — nothing here
|
|
84
|
+
gets squashed away.`
|
|
85
|
+
|
|
86
|
+
export const escalationPersona = `You are the diagnosing voice in gtd's build pipeline, called in once a
|
|
87
|
+
check has stayed red past repeated fix attempts. Read the failing
|
|
88
|
+
output, the previous round's, and the code those attempts touched, then
|
|
89
|
+
write down what is failing, why the earlier attempts didn't resolve it,
|
|
90
|
+
and concrete approaches worth trying next. Never fix the code yourself —
|
|
91
|
+
naming the problem precisely enough for the next fix turn is the whole
|
|
92
|
+
job.`
|
|
93
|
+
|
|
94
|
+
export const stateFileRules = `- You are an autonomous coding agent
|
|
95
|
+
- This workflow's own state files are its private scratchpad, never
|
|
96
|
+
project code or documentation`
|
|
97
|
+
|
|
98
|
+
export const questionBar = `- The goal is shared understanding, not a quota or an empty section: a
|
|
99
|
+
question exists to close a gap between what the human wants and
|
|
100
|
+
what the agent is about to build. Ask whenever that gap is open —
|
|
101
|
+
even when the point looks cheap to undo, settled-looking, or
|
|
102
|
+
narrow. This goal outranks what follows; the conditions below are
|
|
103
|
+
signals a gap is real, strong evidence to ask, never permission
|
|
104
|
+
withheld
|
|
105
|
+
- Before writing \`## Open Questions\`, walk every concern you grouped and
|
|
106
|
+
collect every point above the bar into that ONE section — a question
|
|
107
|
+
held back for a later lap is a bug. One lap is the target; a second is
|
|
108
|
+
the exception
|
|
109
|
+
- Raise first, narrow second: only once every concern above the bar is
|
|
110
|
+
raised into \`## Open Questions\`, walk that same set once more and
|
|
111
|
+
answer the ones a confident default settles, moving each into
|
|
112
|
+
\`## Answered Questions\` with its answer. This pass narrows what's
|
|
113
|
+
already raised — it is never license to raise less, and skipping the
|
|
114
|
+
raise to answer straight through is the same bug as holding a
|
|
115
|
+
question back for a later lap
|
|
116
|
+
- Treat each of these as strong evidence a gap is real: it's a genuine
|
|
117
|
+
fork with divergent outcomes (user-visible for product, materially
|
|
118
|
+
different builds for technical); the diff and history don't already
|
|
119
|
+
settle it (treat an explicit \`.gtd/TODO.md\` directive, a committed
|
|
120
|
+
hand-edit, or — on a loop-back lap — the human's own review-round
|
|
121
|
+
edit/note as settled); or it would be expensive to undo once
|
|
122
|
+
packages are written
|
|
123
|
+
- Where no gap in shared understanding exists, decide it yourself:
|
|
124
|
+
record it under \`## Answered Questions\` as
|
|
125
|
+
\`### <the point phrased as a question>\`
|
|
126
|
+
plus a one-line rationale in prose, no checkboxes. That heading
|
|
127
|
+
always comes last, after every other \`##\` section
|
|
128
|
+
- Above the bar, write it under \`## Open Questions\` — always the first
|
|
129
|
+
\`##\` section — as \`### <question>\` plus a checkbox list: two
|
|
130
|
+
concrete options and a free-text slot, all unticked:
|
|
131
|
+
|
|
132
|
+
### <the question>
|
|
133
|
+
|
|
134
|
+
- [ ] <first option — a concrete answer, a few words of rationale>
|
|
135
|
+
- [ ] <second option>
|
|
136
|
+
- [ ] _your answer_
|
|
137
|
+
|
|
138
|
+
- Never tick a box yourself — the human ticks exactly one per question`
|
|
139
|
+
|
|
140
|
+
export const questionBarReturn = `- This lap continues the same goal as the first: close the gap
|
|
141
|
+
between what the human wants and what gets built. Folding in
|
|
142
|
+
answers and deciding what's left is that same goal continued, not
|
|
143
|
+
a different job
|
|
144
|
+
- The human answered — a ticked box, a free-text answer in place of
|
|
145
|
+
\`_your answer_\`, a deleted question, or a deleted \`## Open Questions\`
|
|
146
|
+
section are all answers. Fold each resolved answer into its concern's
|
|
147
|
+
prose, then move the question into \`## Answered Questions\` as
|
|
148
|
+
\`### <question>\` with the resolved answer in plain prose — no
|
|
149
|
+
checkboxes
|
|
150
|
+
- \`## Answered Questions\` is always the last \`##\` section — a moved
|
|
151
|
+
question lands there, never wherever \`## Open Questions\` used to sit
|
|
152
|
+
- Never re-raise a deleted question, and never re-open a settled
|
|
153
|
+
\`## Answered Questions\` entry
|
|
154
|
+
- An answer may earn a follow-up: if it opens a genuinely new fork above
|
|
155
|
+
the bar — one the answer itself created — raise it as a fresh \`##
|
|
156
|
+
Open Questions\` entry on this same lap. Never restate a question
|
|
157
|
+
already asked, and never treat this as licence to re-open a question
|
|
158
|
+
already settled under \`## Answered Questions\`
|
|
159
|
+
- Recognise a silent lap from \`## Open Questions\` still present with
|
|
160
|
+
nothing ticked and nothing else changed — the human's way of saying
|
|
161
|
+
the gap is already closed. That lap ends the questions, whatever the
|
|
162
|
+
goal says: decide every remaining question yourself, move each to
|
|
163
|
+
\`## Answered Questions\` with a one-line rationale, raise nothing new,
|
|
164
|
+
and leave no \`## Open Questions\` section behind`
|
|
165
|
+
|
|
166
|
+
export const fixFeedbackPrompt = `- The only state file this turn writes is \`.gtd/FEEDBACK.md\` — no other
|
|
167
|
+
files for notes or output
|
|
168
|
+
- Read \`.gtd/FEEDBACK.md\` (the failing test output) and fix the code so
|
|
169
|
+
the suite passes
|
|
170
|
+
- When \`.gtd/ESCALATION.md\` is present, it is a human-reviewed analysis
|
|
171
|
+
of why earlier attempts failed — read it and treat it as the primary
|
|
172
|
+
instruction for this turn. Never edit or delete it yourself, even once
|
|
173
|
+
you believe you've resolved it: only a genuinely green check retires
|
|
174
|
+
it, so an attempt that turns out to be wrong still leaves the next
|
|
175
|
+
turn's instruction in place. Its absence is the ordinary case: an
|
|
176
|
+
unremarkable red round with nothing to escalate yet
|
|
177
|
+
- Delete \`.gtd/FEEDBACK.md\` either way — once the suite passes, or once
|
|
178
|
+
you have established the feedback was wrong; leaving it in place is
|
|
179
|
+
never the right end state, the next check writes its own`
|
|
180
|
+
|
|
181
|
+
export const footnoteRules = `- Leave a footnote anywhere: mark the exact spot with \`[^name]\` (any
|
|
182
|
+
name, no whitespace or \`]\`), then define it below as \`[^name]:
|
|
183
|
+
explain what you mean\` — indent a longer comment's continuation
|
|
184
|
+
lines. (The literal words "your comment" are this format's seeded
|
|
185
|
+
placeholder body — a definition still holding them exactly is
|
|
186
|
+
flagged as unfilled, so write your own words there)
|
|
187
|
+
- A footnote is a comment on that exact spot — the hunk, line, or
|
|
188
|
+
paragraph it marks — never a whole-file remark
|
|
189
|
+
- \`name\` is yours to pick; only a definition's name must be unique in
|
|
190
|
+
this document — the same name may mark more than one spot`
|
|
191
|
+
|
|
192
|
+
export const footnoteFoldIn = `- A footnote (\`[^name]\` plus its \`[^name]:\` definition) is a comment on
|
|
193
|
+
its exact anchor — fold it in as a mandatory concern described
|
|
194
|
+
against that anchor's hunk or paragraph, never flattened into a
|
|
195
|
+
whole-file remark
|
|
196
|
+
- DELETE it in this same turn — marker and definition together — the
|
|
197
|
+
way a transient hand-written code comment is already treated. Never
|
|
198
|
+
re-read a footnote already acted on
|
|
199
|
+
- A footnote is human input only — you reply in prose; never write one
|
|
200
|
+
yourself`
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
answered,
|
|
3
|
+
changes,
|
|
4
|
+
codeChanges,
|
|
5
|
+
head,
|
|
6
|
+
judge,
|
|
7
|
+
numeric,
|
|
8
|
+
read,
|
|
9
|
+
refuse,
|
|
10
|
+
removeScript,
|
|
11
|
+
run,
|
|
12
|
+
scope,
|
|
13
|
+
sectionBodies,
|
|
14
|
+
vars,
|
|
15
|
+
wrote,
|
|
16
|
+
type Change,
|
|
17
|
+
type JudgeQuestion,
|
|
18
|
+
} from "../flows/index.js"
|
|
19
|
+
import { escalation, FIX_CAP, healthy, type EscalationCount } from "./health.js"
|
|
20
|
+
import {
|
|
21
|
+
awaitReview,
|
|
22
|
+
collecting,
|
|
23
|
+
fix,
|
|
24
|
+
fixQuality,
|
|
25
|
+
QUALITY,
|
|
26
|
+
REQUIREMENTS,
|
|
27
|
+
REVIEW,
|
|
28
|
+
reviewing,
|
|
29
|
+
reviewMissing,
|
|
30
|
+
reviewQuality,
|
|
31
|
+
} from "./steps.js"
|
|
32
|
+
import * as t from "./text.js"
|
|
33
|
+
|
|
34
|
+
/** The skills the quality lap reviews with, one turn each: the `qualityReviews` var. */
|
|
35
|
+
export const qualityLenses = (): readonly string[] =>
|
|
36
|
+
(vars.qualityReviews ?? "")
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((lens) => lens.trim())
|
|
39
|
+
.filter((lens) => lens !== "")
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One review turn per lens over the whole change, each appending what it
|
|
43
|
+
* finds blocking to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
44
|
+
* has any.
|
|
45
|
+
*/
|
|
46
|
+
export const qualityLap = async (): Promise<"clean" | "findings"> => {
|
|
47
|
+
for (const lens of qualityLenses()) await reviewQuality(lens)
|
|
48
|
+
return (read(QUALITY) ?? "").length > 0 ? "findings" : "clean"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
52
|
+
export const fixQualityFindings = async (escalations: EscalationCount): Promise<boolean> => {
|
|
53
|
+
for (let turns = 0; turns < FIX_CAP; turns++) {
|
|
54
|
+
await fixQuality()
|
|
55
|
+
if (changes(QUALITY).some((c) => c.status === "deleted")) return true
|
|
56
|
+
}
|
|
57
|
+
await escalation(escalations)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How a review round ended. `feedback` carries what the human edited, for the re-unwind to undo. */
|
|
62
|
+
export type ReviewOutcome =
|
|
63
|
+
| { readonly verdict: "signoff" }
|
|
64
|
+
| {
|
|
65
|
+
readonly verdict: "feedback"
|
|
66
|
+
/** The commit the human's review edit landed as. */
|
|
67
|
+
readonly base: string
|
|
68
|
+
/** The code paths that edit changed, with their content before it. */
|
|
69
|
+
readonly edited: readonly Change[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One noul per `## ` chunk of `.gtd/REVIEW.md`: is its note actionable? */
|
|
73
|
+
const triageQuestions = (chunks: readonly string[]): JudgeQuestion[] =>
|
|
74
|
+
chunks.map((title, i) => ({
|
|
75
|
+
id: `chunk-${i + 1}`,
|
|
76
|
+
primitive: "noul",
|
|
77
|
+
instructions: `Is the note under review chunk "${title}" (in .gtd/REVIEW.md) actionable — anything beyond an approving remark with no code edit?`,
|
|
78
|
+
criteria:
|
|
79
|
+
"A concrete request, a question, a code comment, or a hand-edit under this chunk answers yes. No note, or a purely approving remark, answers no.",
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
/** A note-only round: judge whether any chunk of `review` asks for something. */
|
|
83
|
+
const actionable = async (review: string): Promise<boolean> => {
|
|
84
|
+
const found = sectionBodies(review)
|
|
85
|
+
const chunks = found.map((section) => section.title)
|
|
86
|
+
const evidence = Object.fromEntries(found.map(({ body }, i) => [`chunk-${i + 1}`, body]))
|
|
87
|
+
const { answers, truncated } = await judge("review.triage", {
|
|
88
|
+
questions: triageQuestions(chunks),
|
|
89
|
+
evidence,
|
|
90
|
+
message: t.buildReviewTriageMessage(),
|
|
91
|
+
label: "Judging feedback actionability",
|
|
92
|
+
})
|
|
93
|
+
// Dismissing a note is the risky direction, so only a confident "no" on
|
|
94
|
+
// uncut evidence dismisses a chunk; a blank floor dismisses nothing.
|
|
95
|
+
const minP = numeric(vars.reviewNoteActionable, Infinity)
|
|
96
|
+
return (
|
|
97
|
+
chunks.length === 0 ||
|
|
98
|
+
chunks.some((_, i) => {
|
|
99
|
+
const id = `chunk-${i + 1}`
|
|
100
|
+
return truncated.includes(id) || !answered(answers[id], "no", minP)
|
|
101
|
+
})
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const collect = async (capture: string): Promise<"signoff" | "feedback"> => {
|
|
106
|
+
await collecting(capture)
|
|
107
|
+
if (wrote(REQUIREMENTS)) return "feedback"
|
|
108
|
+
if (changes().length === 0) return "signoff"
|
|
109
|
+
return refuse(
|
|
110
|
+
"gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
116
|
+
* reviews and signs off or comments, and a comment is classified into
|
|
117
|
+
* requirements for another lap.
|
|
118
|
+
*/
|
|
119
|
+
export const review = async (base: string): Promise<ReviewOutcome> => {
|
|
120
|
+
for (;;) {
|
|
121
|
+
await reviewing(base)
|
|
122
|
+
await awaitReview(base)
|
|
123
|
+
if (changes(REVIEW).some((c) => c.status === "deleted")) {
|
|
124
|
+
refuse(
|
|
125
|
+
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const edited = codeChanges()
|
|
129
|
+
const round = head()
|
|
130
|
+
const record = read(REVIEW)
|
|
131
|
+
if (record === undefined) {
|
|
132
|
+
await reviewMissing(round)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
// The review gate clears every tick before its commit, so a changed
|
|
136
|
+
// REVIEW.md is a note, never a tick.
|
|
137
|
+
const noted = changes(REVIEW).length > 0
|
|
138
|
+
const outcome = (verdict: "signoff" | "feedback"): ReviewOutcome =>
|
|
139
|
+
verdict === "signoff" ? { verdict } : { verdict, base: round, edited }
|
|
140
|
+
await run("review.closing", removeScript([REVIEW]), {
|
|
141
|
+
label: "Closing the review",
|
|
142
|
+
base: round,
|
|
143
|
+
})
|
|
144
|
+
if (edited.length > 0) return outcome(await collect(t.reviewEditsCapture(round)))
|
|
145
|
+
if (!noted || !(await actionable(record))) return { verdict: "signoff" }
|
|
146
|
+
return outcome(await collect(t.reviewNotesCapture(round)))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
151
|
+
export const buildTail = (fixFirst: boolean, base: string): Promise<ReviewOutcome> =>
|
|
152
|
+
scope("build", async () => {
|
|
153
|
+
const escalations: EscalationCount = { rounds: 0 }
|
|
154
|
+
let redFirst = fixFirst
|
|
155
|
+
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
156
|
+
let lapped = false
|
|
157
|
+
for (;;) {
|
|
158
|
+
if (redFirst) {
|
|
159
|
+
await fix()
|
|
160
|
+
await healthy(fix, { fixesSoFar: 1, escalations })
|
|
161
|
+
redFirst = false
|
|
162
|
+
}
|
|
163
|
+
const lap = lapped ? "clean" : await qualityLap()
|
|
164
|
+
lapped = true
|
|
165
|
+
if (lap === "clean") return review(base)
|
|
166
|
+
if (await fixQualityFindings(escalations)) await healthy(fix, { escalations })
|
|
167
|
+
else redFirst = true
|
|
168
|
+
}
|
|
169
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { agent, human, vars } from "../flows/index.js"
|
|
2
|
+
import * as t from "./text.js"
|
|
3
|
+
|
|
4
|
+
// The bundled workflow's single steps. A step's name is relative to the
|
|
5
|
+
// scope its caller runs it in — `triage` inside `scope("design")` is
|
|
6
|
+
// `design.triage` — and the full name is public API: renaming it strands
|
|
7
|
+
// every process resting on it.
|
|
8
|
+
|
|
9
|
+
export const FEEDBACK = ".gtd/FEEDBACK.md"
|
|
10
|
+
export const ESCALATION = ".gtd/ESCALATION.md"
|
|
11
|
+
export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
|
|
12
|
+
export const ARCHITECTURE = ".gtd/ARCHITECTURE.md"
|
|
13
|
+
export const REVIEW = ".gtd/REVIEW.md"
|
|
14
|
+
export const QUALITY = ".gtd/QUALITY.md"
|
|
15
|
+
export const SPEC_FEEDBACK = ".gtd/SPEC_FEEDBACK.md"
|
|
16
|
+
|
|
17
|
+
const planner = (): string => vars.plannerModel ?? ""
|
|
18
|
+
const coder = (): string => vars.coderModel ?? ""
|
|
19
|
+
|
|
20
|
+
// ── Planning ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
23
|
+
export const triage = (base: string): Promise<void> =>
|
|
24
|
+
agent("triage", t.withSkills(vars.triageSkills, t.designTriagePrompt(base)), {
|
|
25
|
+
label: "Triaging the change",
|
|
26
|
+
file: REQUIREMENTS,
|
|
27
|
+
mode: "qa",
|
|
28
|
+
model: planner(),
|
|
29
|
+
system: t.designSystem(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const author = (): Promise<void> =>
|
|
33
|
+
agent("author", t.withSkills(vars.architectureSkills, t.architectureAuthorPrompt()), {
|
|
34
|
+
label: "Refining the technical plan",
|
|
35
|
+
file: ARCHITECTURE,
|
|
36
|
+
mode: "qa",
|
|
37
|
+
model: planner(),
|
|
38
|
+
system: t.architectSystem(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const decompose = (): Promise<void> =>
|
|
42
|
+
agent("decompose", t.withSkills(vars.decomposeSkills, t.architectureDecomposePrompt()), {
|
|
43
|
+
label: "Decomposing into packages",
|
|
44
|
+
model: planner(),
|
|
45
|
+
system: t.architectSystem(),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export const answerProductQuestions = (): Promise<void> =>
|
|
49
|
+
human("gate.answer", {
|
|
50
|
+
message: t.designGateAnswerMessage(),
|
|
51
|
+
label: "Awaiting your product answers",
|
|
52
|
+
file: REQUIREMENTS,
|
|
53
|
+
mode: "qa",
|
|
54
|
+
acceptClean: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const answerTechnicalQuestions = (): Promise<void> =>
|
|
58
|
+
human("gate.answer", {
|
|
59
|
+
message: t.architectureGateAnswerMessage(),
|
|
60
|
+
label: "Awaiting your technical answers",
|
|
61
|
+
file: ARCHITECTURE,
|
|
62
|
+
mode: "qa",
|
|
63
|
+
acceptClean: true,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// ── Packages ────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export const build = (pkg: string): Promise<void> =>
|
|
69
|
+
agent("building", t.withSkills(vars.buildSkills, t.packagesItemBuildingPrompt(pkg)), {
|
|
70
|
+
label: "Building",
|
|
71
|
+
model: coder(),
|
|
72
|
+
system: t.builderSystem(),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const fixSuite = (): Promise<void> =>
|
|
76
|
+
agent("fix-suite", t.withSkills(vars.fixSkills, t.packagesItemFixSuitePrompt()), {
|
|
77
|
+
label: "Fixing the check",
|
|
78
|
+
file: FEEDBACK,
|
|
79
|
+
model: coder(),
|
|
80
|
+
system: t.builderSystem(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export const fixSpec = (pkg: string): Promise<void> =>
|
|
84
|
+
agent("fix-spec", t.withSkills(vars.reviewFixSkills, t.packagesItemFixSpecPrompt(pkg)), {
|
|
85
|
+
label: "Fixing review feedback",
|
|
86
|
+
file: SPEC_FEEDBACK,
|
|
87
|
+
model: coder(),
|
|
88
|
+
system: t.builderSystem(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** Review `pkg` against its spec, focused on the `failing` sections the pre-judge could not clear. */
|
|
92
|
+
export const reviewPackage = (pkg: string, failing: readonly string[] = []): Promise<void> =>
|
|
93
|
+
agent(
|
|
94
|
+
"spec.review",
|
|
95
|
+
t.withSkills(vars.specReviewSkills, t.packagesItemSpecReviewPrompt(pkg, failing)),
|
|
96
|
+
{
|
|
97
|
+
label: "Reviewing the package",
|
|
98
|
+
model: planner(),
|
|
99
|
+
system: t.specReviewerSystem(),
|
|
100
|
+
allowEmpty: true,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// ── Keeping the suite green ─────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export const fix = (): Promise<void> =>
|
|
107
|
+
agent("fix", t.withSkills(vars.fixSkills, t.buildFixPrompt()), {
|
|
108
|
+
label: "Fixing the check",
|
|
109
|
+
file: FEEDBACK,
|
|
110
|
+
model: coder(),
|
|
111
|
+
system: t.finisherSystem(),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export const describeEscalation = (): Promise<void> =>
|
|
115
|
+
agent("health.describe", t.withSkills(vars.escalateSkills, t.healthDescribePrompt()), {
|
|
116
|
+
label: "Describing the escalation",
|
|
117
|
+
file: FEEDBACK,
|
|
118
|
+
model: coder(),
|
|
119
|
+
system: t.escalationSystem(),
|
|
120
|
+
allowEmpty: true,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export const escalate = (): Promise<void> =>
|
|
124
|
+
human("health.stop", {
|
|
125
|
+
message: t.healthStopMessage(),
|
|
126
|
+
label: "Escalating to a human",
|
|
127
|
+
file: ESCALATION,
|
|
128
|
+
acceptClean: true,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
export const escalationExhausted = (): Promise<void> =>
|
|
132
|
+
human("health.exhausted", {
|
|
133
|
+
message: t.healthExhaustedMessage(),
|
|
134
|
+
label: "Escalation exhausted",
|
|
135
|
+
file: ESCALATION,
|
|
136
|
+
acceptClean: true,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ── Quality and review ──────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/** One quality review, through the skill `lens`. */
|
|
142
|
+
export const reviewQuality = (lens: string): Promise<void> =>
|
|
143
|
+
agent("quality.reviewing", t.withSkills(lens, t.buildQualityReviewingPrompt(lens)), {
|
|
144
|
+
label: "Reviewing (one quality lens)",
|
|
145
|
+
file: QUALITY,
|
|
146
|
+
model: planner(),
|
|
147
|
+
system: t.reviewerSystem(),
|
|
148
|
+
allowEmpty: true,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
export const fixQuality = (): Promise<void> =>
|
|
152
|
+
agent("fix-quality", t.withSkills(vars.reviewFixSkills, t.buildFixQualityPrompt()), {
|
|
153
|
+
label: "Fixing quality findings",
|
|
154
|
+
file: QUALITY,
|
|
155
|
+
model: coder(),
|
|
156
|
+
system: t.finisherSystem(),
|
|
157
|
+
allowEmpty: true,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
/** Write `.gtd/REVIEW.md` over everything since `base`. */
|
|
161
|
+
export const reviewing = (base: string): Promise<void> =>
|
|
162
|
+
agent("review.reviewing", t.withSkills(vars.reviewSkills, t.buildReviewReviewingPrompt(base)), {
|
|
163
|
+
label: "Reviewing",
|
|
164
|
+
file: REVIEW,
|
|
165
|
+
mode: "review",
|
|
166
|
+
model: planner(),
|
|
167
|
+
system: t.reviewerSystem(),
|
|
168
|
+
base,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
export const awaitReview = (base: string): Promise<void> =>
|
|
172
|
+
human("review.await-review", {
|
|
173
|
+
message: t.buildReviewAwaitReviewMessage(base),
|
|
174
|
+
label: "Awaiting your review",
|
|
175
|
+
file: REVIEW,
|
|
176
|
+
mode: "review",
|
|
177
|
+
base,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
181
|
+
export const reviewMissing = (round: string): Promise<void> =>
|
|
182
|
+
human("review.review-missing", {
|
|
183
|
+
message: t.buildReviewReviewMissingMessage(round),
|
|
184
|
+
label: "Nothing to review",
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
188
|
+
export const collecting = (capture: string): Promise<void> =>
|
|
189
|
+
agent(
|
|
190
|
+
"review.collecting",
|
|
191
|
+
t.withSkills(vars.reviewSkills, t.buildReviewCollectingPrompt(capture)),
|
|
192
|
+
{
|
|
193
|
+
label: "Collecting your feedback",
|
|
194
|
+
file: REQUIREMENTS,
|
|
195
|
+
mode: "qa",
|
|
196
|
+
model: planner(),
|
|
197
|
+
system: t.reviewerSystem(),
|
|
198
|
+
allowEmpty: true,
|
|
199
|
+
},
|
|
200
|
+
)
|