@pmelab/gtd 46.0.0 → 48.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 +21 -19
- package/dist/chunks/{reserved-BENPy6IJ.mjs → reserved-DF1HD6jL.mjs} +41 -22
- package/dist/chunks/{review-DcbgUwr7.mjs → review-Bv40kZu9.mjs} +5 -8
- package/dist/chunks/{tail-CDNMZ-2E.mjs → tail-D89fU_wf.mjs} +68 -39
- package/dist/gtd.bundle.mjs +203 -867
- package/dist/testkit.mjs +89 -53
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +26 -22
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +257 -205
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +0 -2
- package/src/cli/Cli.ts +0 -15
- package/src/cli/Program.test.ts +0 -1
- package/src/cli/Program.ts +0 -8
- package/src/cli/ToolCommands.ts +1 -2
- package/src/install/Install.ts +89 -35
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/cleanup/cleanup.test.ts +11 -9
- package/src/workflows/feature/feature.ts +38 -28
- package/src/workflows/feature/lens-review.feature +3 -1
- package/src/workflows/feature/slice-findings.feature +198 -0
- package/src/workflows/fix/fix-lens-origin.feature +101 -0
- package/src/workflows/fix/fix-quality.feature +131 -0
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/lenses.ts +30 -5
- package/src/workflows/shared/review.ts +3 -8
- package/src/workflows/shared/tail.ts +74 -57
- package/src/workflows/shared/text.ts +85 -29
- package/src/workflows/feature/quality-unresolved.feature +0 -121
package/src/cli/Program.test.ts
CHANGED
|
@@ -1685,7 +1685,6 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1685
1685
|
check: { kind: "check", mode: "qa", file: ".gtd/TODO.md" },
|
|
1686
1686
|
uncheck: { kind: "uncheck", file: ".gtd/REVIEW.md" },
|
|
1687
1687
|
install: { kind: "install" },
|
|
1688
|
-
loop: { kind: "loop" },
|
|
1689
1688
|
contextHook: { kind: "contextHook" },
|
|
1690
1689
|
summary: { kind: "summary" },
|
|
1691
1690
|
base: { kind: "base" },
|
package/src/cli/Program.ts
CHANGED
|
@@ -163,7 +163,6 @@ export type Command =
|
|
|
163
163
|
}
|
|
164
164
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
165
165
|
| { readonly kind: "install" }
|
|
166
|
-
| { readonly kind: "loop" }
|
|
167
166
|
| {
|
|
168
167
|
readonly kind: "contextHook"
|
|
169
168
|
readonly soft?: number
|
|
@@ -239,9 +238,6 @@ const runInstallCommand = (
|
|
|
239
238
|
)
|
|
240
239
|
})
|
|
241
240
|
|
|
242
|
-
const runLoopCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
243
|
-
Effect.flatMap(ToolCommands, (tools) => tools.loop(selfGtd()))
|
|
244
|
-
|
|
245
241
|
/**
|
|
246
242
|
* `planLanding`'s result — a preview of what `gtd land` would do, plus the
|
|
247
243
|
* combined `script` the driver runs to do it. `script` is
|
|
@@ -1221,7 +1217,6 @@ export const needsOf = (kind: Command["kind"]): Needs => {
|
|
|
1221
1217
|
switch (kind) {
|
|
1222
1218
|
case "lsp":
|
|
1223
1219
|
case "install":
|
|
1224
|
-
case "loop":
|
|
1225
1220
|
case "contextHook":
|
|
1226
1221
|
case "judgeRun":
|
|
1227
1222
|
case "strays":
|
|
@@ -1240,7 +1235,6 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
|
|
|
1240
1235
|
"check",
|
|
1241
1236
|
"uncheck",
|
|
1242
1237
|
"install",
|
|
1243
|
-
"loop",
|
|
1244
1238
|
"contextHook",
|
|
1245
1239
|
"judgeRun",
|
|
1246
1240
|
"strays",
|
|
@@ -1311,8 +1305,6 @@ const dispatchVoidCommand = (
|
|
|
1311
1305
|
return runUncheckCommand(command.file)
|
|
1312
1306
|
case "install":
|
|
1313
1307
|
return runInstallCommand(out)
|
|
1314
|
-
case "loop":
|
|
1315
|
-
return runLoopCommand()
|
|
1316
1308
|
case "contextHook":
|
|
1317
1309
|
return runContextHookCommand(command, out)
|
|
1318
1310
|
case "summary":
|
package/src/cli/ToolCommands.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Effect } from "effect"
|
|
|
3
3
|
import type { UiSettings } from "../flows/index.js"
|
|
4
4
|
import type { ContextHookRequest } from "../wire/index.js"
|
|
5
5
|
|
|
6
|
-
/** How to run THIS gtd — a
|
|
6
|
+
/** How to run THIS gtd — a tool (the LSP) never reads a different version's state than the one it ships with. */
|
|
7
7
|
export type SelfGtd = readonly [string, ...string[]]
|
|
8
8
|
|
|
9
9
|
export interface UiRequest {
|
|
@@ -38,7 +38,6 @@ export class ToolCommands extends Context.Tag("ToolCommands")<
|
|
|
38
38
|
out: { readonly write: (chunk: string) => void; readonly flush: () => void },
|
|
39
39
|
) => Effect.Effect<void, Error>
|
|
40
40
|
readonly lsp: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
41
|
-
readonly loop: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
42
41
|
/** The hook's stdout: a JSON document, or empty to let the turn go on. */
|
|
43
42
|
readonly contextHook: (request: ContextHookRequest) => Effect.Effect<string, Error>
|
|
44
43
|
readonly judge: (request: JudgeRequest) => Promise<string>
|
package/src/install/Install.ts
CHANGED
|
@@ -11,8 +11,15 @@ export CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1
|
|
|
11
11
|
export BASH_DEFAULT_TIMEOUT_MS=1800000
|
|
12
12
|
export BASH_MAX_TIMEOUT_MS=7200000
|
|
13
13
|
|
|
14
|
+
# gtd's paths, scripts and checks are relative to the repository root.
|
|
15
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
16
|
+
|
|
14
17
|
beat=1
|
|
15
18
|
while :; do
|
|
19
|
+
cost=
|
|
20
|
+
model=
|
|
21
|
+
judged_before="\${judged:-}"
|
|
22
|
+
judged=
|
|
16
23
|
# One value per invocation: \`gtd next --json=<path>\` is a pure read (peek
|
|
17
24
|
# or dispatch, indistinguishable, always safe to call again), so nothing is
|
|
18
25
|
# lost by reading \`kind\`/\`idle\` as two separate calls rather than one
|
|
@@ -40,14 +47,22 @@ while :; do
|
|
|
40
47
|
# \`gtd judge run\` and land the verdict via \`gtd judge answer\`. POSIX sh has
|
|
41
48
|
# no pipefail, so each step is captured first and only piped on when it
|
|
42
49
|
# succeeded; if \`gtd judge run\` fails or \`gtd judge answer\` rejects the
|
|
43
|
-
# verdict, its stderr reaches you and
|
|
50
|
+
# verdict, its stderr reaches you and the gate is handed to you — on the
|
|
51
|
+
# opening beat too, so it never lands unanswered — as it is when the gate
|
|
52
|
+
# we just answered comes straight back.
|
|
44
53
|
message)
|
|
45
|
-
if [ -n "$(gtd next --json=judge)" ]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
if [ -n "$(gtd next --json=judge)" ]; then
|
|
55
|
+
state="$(gtd next --json=state)"
|
|
56
|
+
if [ "$state" != "$judged_before" ] &&
|
|
57
|
+
verdict="$(gtd judge --json | gtd judge run)" &&
|
|
58
|
+
script="$(printf '%s\\n' "$verdict" | gtd judge answer --json=script)"; then
|
|
59
|
+
printf '%s\\n' "$script" | sh
|
|
60
|
+
judged="$state"
|
|
61
|
+
beat=$((beat + 1))
|
|
62
|
+
continue
|
|
63
|
+
fi
|
|
64
|
+
gtd next
|
|
65
|
+
exit 0
|
|
51
66
|
fi
|
|
52
67
|
[ "$beat" = 1 ] || {
|
|
53
68
|
gtd next
|
|
@@ -66,45 +81,83 @@ while :; do
|
|
|
66
81
|
model="$(gtd next --json=model)"
|
|
67
82
|
system="$(gtd next --json=system)"
|
|
68
83
|
validate="$(gtd next --json=validate)"
|
|
84
|
+
context="$(gtd next --json=context)"
|
|
69
85
|
log="$(gtd next --json=log)"
|
|
86
|
+
# Absolute, since the hook runs wherever the agent last cd'ed to.
|
|
87
|
+
case "$log" in /*) ;; *) log="$(git rev-parse --show-toplevel)/$log" ;; esac
|
|
70
88
|
# \`model\` is a hint, not a model name — the bundled workflow ships
|
|
71
89
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
72
|
-
#
|
|
73
|
-
#
|
|
90
|
+
# from your environment; unset, the hint is dropped and the agent runs
|
|
91
|
+
# its own default. Any other value is a real model name.
|
|
74
92
|
case "$model" in
|
|
75
93
|
smart) model="\${GTD_LOOP_MODEL_SMART:-}" ;;
|
|
76
94
|
base) model="\${GTD_LOOP_MODEL_BASE:-}" ;;
|
|
77
95
|
esac
|
|
78
|
-
# The
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
# The step's context limit, as a Claude Code hook after every tool
|
|
97
|
+
# call and before compacting. Past \`hard\` it stops the turn and
|
|
98
|
+
# appends why to the stop file, so we land nothing and stop too.
|
|
99
|
+
soft="$(printf '%s' "$context" | sed -n 's/.*"soft":\\([0-9.]*\\).*/\\1/p')"
|
|
100
|
+
hard="$(printf '%s' "$context" | sed -n 's/.*"hard":\\([0-9.]*\\).*/\\1/p')"
|
|
101
|
+
window="\${GTD_LOOP_CONTEXT_WINDOW:-}"
|
|
102
|
+
case "$model" in *'[1m]'* | *'[1M]'*) window="\${window:-1000000}" ;; esac
|
|
103
|
+
hook="gtd context-hook\${soft:+ --soft $soft}\${hard:+ --hard $hard}\${window:+ --window $window}"
|
|
104
|
+
hooks="[{\\"hooks\\":[{\\"type\\":\\"command\\",\\"command\\":\\"$hook\\"}]}]"
|
|
105
|
+
settings="{\\"hooks\\":{\\"PostToolUse\\":$hooks,\\"PreCompact\\":$hooks}}"
|
|
106
|
+
stop="$log.context-stop"
|
|
107
|
+
rm -f "$stop"
|
|
108
|
+
# One turn, its prompt on stdin. The prompt embeds a full diff, so it
|
|
109
|
+
# never goes on argv — argv is capped (~1 MB on macOS, and POSIX
|
|
110
|
+
# guarantees only 4 KB, ARG_MAX), and a diff crosses that far sooner
|
|
111
|
+
# than you'd expect. Every turn passes the same $model/$system:
|
|
112
|
+
# whether \`claude --resume\` re-applies the original session's is a
|
|
113
|
+
# harness detail gtd cannot verify from outside. Its JSON envelope
|
|
114
|
+
# goes to the log, and its \`total_cost_usd\` adds up into $cost. An
|
|
115
|
+
# \`is_error\` envelope (max turns, an error mid-turn) or one another
|
|
116
|
+
# hook stopped fails the turn even at exit 0: landing it would land
|
|
117
|
+
# the failure as work. Only a turn that never ran (no envelope: the
|
|
118
|
+
# session flag was refused) is worth retrying under the other flag.
|
|
119
|
+
turn() {
|
|
120
|
+
out="$(GTD_LOOP_CONTEXT_STOP_FILE="$stop" claude -p "$@" \\
|
|
121
|
+
\${model:+--model "$model"} \\
|
|
122
|
+
\${system:+--system-prompt "$system"} \\
|
|
123
|
+
--settings "$settings" --output-format json \\
|
|
124
|
+
--dangerously-skip-permissions 2>>"$log")" && ok=0 || ok=$?
|
|
125
|
+
printf '%s\\n' "$out" >>"$log"
|
|
126
|
+
case "$out" in *'"type":"result"'*) ran=true ;; *) ran=false ;; esac
|
|
127
|
+
case "$out" in
|
|
128
|
+
*'"is_error":true'* | *'"terminal_reason":"hook_stopped"'*) ok=1 ;;
|
|
129
|
+
esac
|
|
130
|
+
usd="$(printf '%s\\n' "$out" | sed -n 's/.*"total_cost_usd":\\([0-9.eE+-]*\\).*/\\1/p' | tail -n 1)"
|
|
131
|
+
[ -z "$usd" ] || cost="$(awk -v a="\${cost:-0}" -v b="$usd" 'BEGIN { printf "%.6f", a + b }')"
|
|
132
|
+
[ "\${cost:-}" != 0.000000 ] || cost=
|
|
133
|
+
if [ -s "$stop" ]; then
|
|
134
|
+
cat "$stop" >&2
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
return "$ok"
|
|
138
|
+
}
|
|
139
|
+
# Plain \`gtd next\` (not --json=content) renders the diff once.
|
|
140
|
+
gtd next >"$log.prompt"
|
|
88
141
|
if [ "\${resume:-}" = true ]
|
|
89
|
-
then
|
|
90
|
-
else
|
|
142
|
+
then first=--resume second=--session-id
|
|
143
|
+
else first=--session-id second=--resume
|
|
91
144
|
fi
|
|
145
|
+
turn "$first" "$session_id" <"$log.prompt" ||
|
|
146
|
+
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$log.prompt"; } || {
|
|
147
|
+
echo "agent turn failed — see $log" >&2
|
|
148
|
+
exit 1
|
|
149
|
+
}
|
|
92
150
|
n=0
|
|
93
151
|
while [ -n "\${validate:-}" ] && ! fix="$(sh -c "$validate" 2>&1)"; do
|
|
94
152
|
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
|
|
95
|
-
# $fix IS the fix prompt, verbatim
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
printf '%s' "$fix" | claude -p --resume "$session_id" \\
|
|
104
|
-
\${model:+--model "$model"} \\
|
|
105
|
-
\${system:+--system-prompt "$system"} \\
|
|
106
|
-
--dangerously-skip-permissions >>"$log" 2>&1
|
|
107
|
-
done ;;
|
|
153
|
+
# $fix IS the fix prompt, verbatim.
|
|
154
|
+
printf '%s' "$fix" >"$log.prompt"
|
|
155
|
+
turn --resume "$session_id" <"$log.prompt" || {
|
|
156
|
+
echo "fix turn failed — see $log" >&2
|
|
157
|
+
exit 1
|
|
158
|
+
}
|
|
159
|
+
done
|
|
160
|
+
rm -f "$log.prompt" ;;
|
|
108
161
|
esac
|
|
109
162
|
|
|
110
163
|
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
@@ -114,7 +167,8 @@ while :; do
|
|
|
114
167
|
# before the script runs and changes the tree out from under them.
|
|
115
168
|
settled="$(gtd land --json=settled)"
|
|
116
169
|
idle="$(gtd land --json=idle)"
|
|
117
|
-
|
|
170
|
+
# A prompt beat's turns record their cost, tagged with the model that ran.
|
|
171
|
+
gtd land --json=script \${cost:+--cost="$cost"} \${cost:+\${model:+--model="$model"}} | sh
|
|
118
172
|
if [ "\${settled:-}" = true ]; then
|
|
119
173
|
[ "\${idle:-}" = true ] && gtd next
|
|
120
174
|
exit 0
|
package/src/testkit/Layers.ts
CHANGED
|
@@ -205,7 +205,6 @@ const missingTool = (name: string) => () =>
|
|
|
205
205
|
const missingToolCommands: ToolCommands["Type"] = {
|
|
206
206
|
ui: missingTool("ui"),
|
|
207
207
|
lsp: missingTool("lsp"),
|
|
208
|
-
loop: missingTool("loop"),
|
|
209
208
|
contextHook: missingTool("context-hook"),
|
|
210
209
|
judge: () =>
|
|
211
210
|
Promise.reject(
|
package/src/wire/ContextLimit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The context-limit decision every driver shares:
|
|
1
|
+
// The context-limit decision every driver shares: `gtd context-hook`
|
|
2
2
|
// and the Claude Code mod both answer a hook event through `contextHook`, so
|
|
3
3
|
// the two cannot drift on thresholds, wording or which tokens count.
|
|
4
4
|
|
|
@@ -90,6 +90,13 @@ describe("the cleanup guard over every later agent turn", () => {
|
|
|
90
90
|
"build.health.check": () => undefined,
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// One lens finding, judged mechanical, so it reaches the fix turn.
|
|
94
|
+
const MECHANICAL: Actors = {
|
|
95
|
+
"build.quality.correctness.reviewing": (_, r) =>
|
|
96
|
+
r.write(".gtd/QUALITY.md", "# Quality\n\n## a finding\n\nFix it.\n"),
|
|
97
|
+
"build.quality.triage": () => '[{"id":"Q1","answer":"mechanical","p":0.9}]',
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
// Without the shipped access rules, which would refuse a source write from a read-only turn first: the guard is the second line.
|
|
94
101
|
const NO_ACCESS = CONFIG.replace(" access,\n", "")
|
|
95
102
|
|
|
@@ -185,15 +192,11 @@ describe("the cleanup guard over every later agent turn", () => {
|
|
|
185
192
|
).rejects.toThrow("src/b.ts:1 still names GTD_FLAG_LIVE")
|
|
186
193
|
})
|
|
187
194
|
|
|
188
|
-
it("refuses
|
|
195
|
+
it("refuses a quality fix turn that re-adds the env var", async () => {
|
|
189
196
|
await expect(
|
|
190
197
|
driven(guarded("correctness", NO_ACCESS), {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"build.fix.quality.fixing": () => undefined,
|
|
194
|
-
"build.health.describe": () => undefined,
|
|
195
|
-
"build.health.stop": () => undefined,
|
|
196
|
-
"build.fix": (_, r) => r.write("src/b.ts", NAMED),
|
|
198
|
+
...MECHANICAL,
|
|
199
|
+
"build.fix.quality.fixing": (_, r) => r.write("src/b.ts", NAMED),
|
|
197
200
|
}),
|
|
198
201
|
).rejects.toThrow("src/b.ts:1 still names GTD_FLAG_LIVE")
|
|
199
202
|
})
|
|
@@ -209,8 +212,7 @@ describe("the cleanup guard over every later agent turn", () => {
|
|
|
209
212
|
it("refuses a quality fix turn that restores the registry entry", async () => {
|
|
210
213
|
await expect(
|
|
211
214
|
driven(guarded("correctness"), {
|
|
212
|
-
|
|
213
|
-
r.write(".gtd/QUALITY.md", "- a finding\n"),
|
|
215
|
+
...MECHANICAL,
|
|
214
216
|
"build.fix.quality.fixing": (_, r) => r.write("src/flags/index.ts", REGISTRY),
|
|
215
217
|
}),
|
|
216
218
|
).rejects.toThrow('still declares flag "live"')
|
|
@@ -9,9 +9,10 @@ import {
|
|
|
9
9
|
slice,
|
|
10
10
|
start,
|
|
11
11
|
} from "../../flows/index.js"
|
|
12
|
-
import { gate
|
|
12
|
+
import { gate } from "../shared/health.js"
|
|
13
|
+
import { joinReports, type LensReport } from "../shared/lenses.js"
|
|
13
14
|
import { flagModeOn, shipped } from "../shared/options.js"
|
|
14
|
-
import { FEEDBACK,
|
|
15
|
+
import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
15
16
|
import { refusePendingHandOff, tail } from "../shared/tail.js"
|
|
16
17
|
import * as t from "../shared/text.js"
|
|
17
18
|
import {
|
|
@@ -36,7 +37,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
|
|
|
36
37
|
// green-baseline gate → design → per slice of the criteria, a naive pass,
|
|
37
38
|
// judged with `flagMode` on: it lands as the implementation, or (refactoring
|
|
38
39
|
// first when the judge asks) is built fresh from → the health loop → the
|
|
39
|
-
// quality lap
|
|
40
|
+
// triaged quality lap, whose leftovers are carried on → after the last slice,
|
|
41
|
+
// human review of the whole change and every slice's findings, which
|
|
40
42
|
// signs off (the episode ends back at `idle`) or sends a design → implement
|
|
41
43
|
// lap.
|
|
42
44
|
|
|
@@ -63,10 +65,12 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
|
63
65
|
const one = slices.length === 1
|
|
64
66
|
// A steer may delete every criterion of a later slice: there is nothing to build there.
|
|
65
67
|
const built = (i: number): boolean => sliceIds(sliceCriteria(slices, i))?.length !== 0
|
|
68
|
+
const reports: { slice: string; report: LensReport }[] = []
|
|
66
69
|
for (const i of slices.keys()) {
|
|
67
70
|
if (!built(i)) continue
|
|
68
71
|
const n = one ? "" : `-${i + 1}`
|
|
69
72
|
await buildSlice(i === 0 ? firstBase : head(), {
|
|
73
|
+
number: `${i + 1}`,
|
|
70
74
|
tier,
|
|
71
75
|
criteria: sliceCriteria(slices, i),
|
|
72
76
|
refactor: `refactor${n}`,
|
|
@@ -76,6 +80,7 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
|
76
80
|
// Every slice but the one is built unreviewed, so the review covers them all.
|
|
77
81
|
reviewBase: one ? undefined : firstBase,
|
|
78
82
|
handedOut,
|
|
83
|
+
laps: reports,
|
|
79
84
|
})
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -88,6 +93,8 @@ const gather = (handedOut: Set<string>): readonly string[] => {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
interface SliceBuild {
|
|
96
|
+
/** The slice's number, from 1. */
|
|
97
|
+
readonly number: string
|
|
91
98
|
readonly tier: RiskTier | undefined
|
|
92
99
|
readonly criteria: CriteriaSlice | undefined
|
|
93
100
|
readonly refactor: string
|
|
@@ -97,6 +104,8 @@ interface SliceBuild {
|
|
|
97
104
|
readonly reviewBase?: string | undefined
|
|
98
105
|
/** Every criterion ID handed out so far; the slice adds those a steer adds. */
|
|
99
106
|
readonly handedOut: Set<string>
|
|
107
|
+
/** Each earlier slice's lap report, by slice number, for the review after the last one. */
|
|
108
|
+
readonly laps: { slice: string; report: LensReport }[]
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap; after the last slice, the review. */
|
|
@@ -127,37 +136,38 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
|
|
|
127
136
|
if (more) {
|
|
128
137
|
// The criteria stay in the tree for the next slice; no quality fix may move them.
|
|
129
138
|
const guard = () => requireCriteriaFrozen(REQUIREMENTS)
|
|
130
|
-
|
|
139
|
+
// No gate between slices: what the lap left waits for the review after the last one.
|
|
140
|
+
const report = await tail(async (_, report) => report, {
|
|
141
|
+
green: true,
|
|
142
|
+
lensBase: base,
|
|
143
|
+
guard,
|
|
144
|
+
guardRests: false,
|
|
145
|
+
})
|
|
146
|
+
build.laps.push({ slice: build.number, report })
|
|
131
147
|
return
|
|
132
148
|
}
|
|
133
149
|
const reviewBase = build.reviewBase ?? base
|
|
134
|
-
|
|
150
|
+
const outcome = await reviewedTail(reviewBase, base, { slice: build.number, laps: build.laps })
|
|
151
|
+
await featureFeedback(outcome, base, { closed, used })
|
|
135
152
|
})
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** The triaged quality lap, then the human review since `base`. */
|
|
155
|
-
const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
|
|
156
|
-
tail((escalations, report) => humanReview(base, escalations, { report }), {
|
|
157
|
-
green: true,
|
|
158
|
-
lensBase,
|
|
159
|
-
triage: true,
|
|
160
|
-
})
|
|
155
|
+
/** The quality lap of slice `last.slice`, then the human review since `base`, listing what earlier slices' `laps` left too. */
|
|
156
|
+
const reviewedTail = (
|
|
157
|
+
base: string,
|
|
158
|
+
lensBase: string,
|
|
159
|
+
last: { readonly slice: string; readonly laps: SliceBuild["laps"] } = { slice: "", laps: [] },
|
|
160
|
+
): Promise<ReviewOutcome> =>
|
|
161
|
+
tail(
|
|
162
|
+
(escalations, report) =>
|
|
163
|
+
humanReview(base, escalations, {
|
|
164
|
+
report:
|
|
165
|
+
last.laps.length === 0
|
|
166
|
+
? report
|
|
167
|
+
: joinReports([...last.laps, { slice: last.slice, report }]),
|
|
168
|
+
}),
|
|
169
|
+
{ green: true, lensBase },
|
|
170
|
+
)
|
|
161
171
|
|
|
162
172
|
// seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
|
|
163
173
|
// and unrefactored, held to every criterion's coverage since the process start.
|
|
@@ -74,6 +74,7 @@ Feature: the quality lenses are the only agentic review
|
|
|
74
74
|
Should subtract round? The criterion does not say.
|
|
75
75
|
"""
|
|
76
76
|
And step "build.quality.sorting" runs its script
|
|
77
|
+
And step "build.quality.closing" runs its script
|
|
77
78
|
And step "build.review.composing" runs its script
|
|
78
79
|
|
|
79
80
|
Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
|
|
@@ -184,7 +185,8 @@ Feature: the quality lenses are the only agentic review
|
|
|
184
185
|
gtd(agent): build.fix.quality.fixing
|
|
185
186
|
gtd(agent): build.fix.quality.fixing
|
|
186
187
|
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
187
|
-
gtd(check): build.health.check → build.
|
|
188
|
+
gtd(check): build.health.check → build.quality.closing
|
|
189
|
+
gtd(check): build.quality.closing → build.review.composing
|
|
188
190
|
gtd(check): build.review.composing → build.review.await-review
|
|
189
191
|
"""
|
|
190
192
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: every slice's quality lap is triaged, and its leftovers wait for the review
|
|
3
|
+
|
|
4
|
+
A judge sorts each slice's findings like the last slice's (see
|
|
5
|
+
`lens-review.feature`): mechanical ones are fixed, noise is dropped, design
|
|
6
|
+
ones are never sent to the fixer. Nothing between slices asks a person
|
|
7
|
+
anything — what a lap leaves is carried to the review after the last slice,
|
|
8
|
+
each finding naming its slice and lens.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
13
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
14
|
+
And an environment variable "GTD_SLICEMAXCRITERIA" set to "1"
|
|
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
|
+
# More arithmetic
|
|
23
|
+
|
|
24
|
+
subtract and multiply.
|
|
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
|
+
## Acceptance Criteria
|
|
33
|
+
|
|
34
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
35
|
+
- AC-2: multiply(a, b) returns a * b.
|
|
36
|
+
"""
|
|
37
|
+
And step "design.gate.answer" changes nothing
|
|
38
|
+
And step "design.slicing" writes ".gtd/SLICES.md" with:
|
|
39
|
+
"""
|
|
40
|
+
# Slices
|
|
41
|
+
|
|
42
|
+
## Subtract
|
|
43
|
+
|
|
44
|
+
- AC-1
|
|
45
|
+
|
|
46
|
+
## Multiply
|
|
47
|
+
|
|
48
|
+
- AC-2
|
|
49
|
+
"""
|
|
50
|
+
And step "seam.naive", when prompted with "This slice builds AC-1 only", writes "src/sub.test.ts" with:
|
|
51
|
+
"""
|
|
52
|
+
it("[AC-1] subtracts", () => {})
|
|
53
|
+
"""
|
|
54
|
+
And step "seam.naive", when prompted with "This slice builds AC-2 only", writes "src/mul.test.ts" with:
|
|
55
|
+
"""
|
|
56
|
+
it("[AC-2] multiplies", () => {})
|
|
57
|
+
"""
|
|
58
|
+
And step "seam.check" changes nothing
|
|
59
|
+
And step "implement.applying" runs its script
|
|
60
|
+
And step "implement.health.check" changes nothing
|
|
61
|
+
And step "implement.closing" runs its script
|
|
62
|
+
And judge step "implement.coverage", when its questions contain "AC-1", answers:
|
|
63
|
+
"""
|
|
64
|
+
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
65
|
+
"""
|
|
66
|
+
And judge step "implement.coverage", when its questions contain "AC-2", answers:
|
|
67
|
+
"""
|
|
68
|
+
[{"id":"AC-2","answer":"yes","p":0.9}]
|
|
69
|
+
"""
|
|
70
|
+
And step "build.quality.sorting" runs its script
|
|
71
|
+
And step "build.quality.closing" runs its script
|
|
72
|
+
And step "build.health.check" changes nothing
|
|
73
|
+
And step "build.review.composing" runs its script
|
|
74
|
+
|
|
75
|
+
Scenario: a finding questioning a criterion reaches the final review, never the fixer
|
|
76
|
+
Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
77
|
+
"""
|
|
78
|
+
# Quality
|
|
79
|
+
|
|
80
|
+
## AC-1 ignores floating point
|
|
81
|
+
|
|
82
|
+
Should subtract round? The criterion does not say.
|
|
83
|
+
"""
|
|
84
|
+
And judge step "build.quality.triage" answers:
|
|
85
|
+
"""
|
|
86
|
+
[{"id":"Q1","answer":"design","p":0.9}]
|
|
87
|
+
"""
|
|
88
|
+
When the process is driven
|
|
89
|
+
Then the process rests at "build.review.await-review"
|
|
90
|
+
And no commit body contains "build.fix.quality.fixing"
|
|
91
|
+
And ".gtd/QUALITY.md" does not exist
|
|
92
|
+
And ".gtd/REVIEW.md" contains:
|
|
93
|
+
"""
|
|
94
|
+
## AC-1 ignores floating point
|
|
95
|
+
|
|
96
|
+
_From slice 1, lens correctness._
|
|
97
|
+
|
|
98
|
+
Should subtract round? The criterion does not say.
|
|
99
|
+
"""
|
|
100
|
+
And ".gtd/REVIEW.md" contains:
|
|
101
|
+
"""
|
|
102
|
+
## AC-1 ignores floating point (2)
|
|
103
|
+
|
|
104
|
+
_From slice 2, lens correctness._
|
|
105
|
+
"""
|
|
106
|
+
And the commit subjects end with:
|
|
107
|
+
"""
|
|
108
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
109
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
110
|
+
gtd(judge): build.quality.triage → build.quality.sorting
|
|
111
|
+
gtd(check): build.quality.sorting → seam.naive
|
|
112
|
+
gtd(agent): seam.naive → seam.check
|
|
113
|
+
gtd(check): seam.check → implement.applying
|
|
114
|
+
gtd(check): implement.applying → implement.health.check
|
|
115
|
+
gtd(check): implement.health.check → implement.coverage
|
|
116
|
+
gtd(judge): implement.coverage → implement.closing
|
|
117
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
118
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
119
|
+
gtd(judge): build.quality.triage → build.quality.sorting
|
|
120
|
+
gtd(check): build.quality.sorting → build.review.composing
|
|
121
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
Scenario: a mechanical finding the fix cap leaves in an early slice is carried, never escalated
|
|
125
|
+
Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
126
|
+
"""
|
|
127
|
+
# Quality
|
|
128
|
+
|
|
129
|
+
## subtract has no negative test
|
|
130
|
+
|
|
131
|
+
Add a test subtracting a larger number.
|
|
132
|
+
"""
|
|
133
|
+
And judge step "build.quality.triage" answers:
|
|
134
|
+
"""
|
|
135
|
+
[{"id":"Q1","answer":"mechanical","p":0.9}]
|
|
136
|
+
"""
|
|
137
|
+
And step "build.fix.quality.fixing" changes nothing
|
|
138
|
+
When the process is driven
|
|
139
|
+
Then the process rests at "build.review.await-review"
|
|
140
|
+
And no commit body contains "build.health.describe"
|
|
141
|
+
And ".gtd/REVIEW.md" contains:
|
|
142
|
+
"""
|
|
143
|
+
## subtract has no negative test
|
|
144
|
+
|
|
145
|
+
_From slice 1, lens correctness._
|
|
146
|
+
|
|
147
|
+
_The fixer left this unresolved._
|
|
148
|
+
"""
|
|
149
|
+
And the commit subjects end with:
|
|
150
|
+
"""
|
|
151
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
152
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
153
|
+
gtd(judge): build.quality.triage → build.fix.quality.fixing
|
|
154
|
+
gtd(agent): build.fix.quality.fixing
|
|
155
|
+
gtd(agent): build.fix.quality.fixing
|
|
156
|
+
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
157
|
+
gtd(check): build.health.check → build.quality.closing
|
|
158
|
+
gtd(check): build.quality.closing → seam.naive
|
|
159
|
+
gtd(agent): seam.naive → seam.check
|
|
160
|
+
gtd(check): seam.check → implement.applying
|
|
161
|
+
gtd(check): implement.applying → implement.health.check
|
|
162
|
+
gtd(check): implement.health.check → implement.coverage
|
|
163
|
+
gtd(judge): implement.coverage → implement.closing
|
|
164
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
165
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
166
|
+
gtd(judge): build.quality.triage → build.fix.quality.fixing
|
|
167
|
+
gtd(agent): build.fix.quality.fixing
|
|
168
|
+
gtd(agent): build.fix.quality.fixing
|
|
169
|
+
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
170
|
+
gtd(check): build.health.check → build.quality.closing
|
|
171
|
+
gtd(check): build.quality.closing → build.review.composing
|
|
172
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
Scenario: the final review lists every slice's fixes with their commits
|
|
176
|
+
Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
177
|
+
"""
|
|
178
|
+
# Quality
|
|
179
|
+
|
|
180
|
+
## subtract has no negative test
|
|
181
|
+
|
|
182
|
+
Add a test subtracting a larger number.
|
|
183
|
+
"""
|
|
184
|
+
And judge step "build.quality.triage" answers:
|
|
185
|
+
"""
|
|
186
|
+
[{"id":"Q1","answer":"mechanical","p":0.9}]
|
|
187
|
+
"""
|
|
188
|
+
And step "build.fix.quality.fixing" writes "src/edge.test.ts" with:
|
|
189
|
+
"""
|
|
190
|
+
it("subtracts a larger number", () => {})
|
|
191
|
+
"""
|
|
192
|
+
And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
|
|
193
|
+
When the process is driven
|
|
194
|
+
Then the process rests at "build.review.await-review"
|
|
195
|
+
And ".gtd/REVIEW.md" contains ", slice 1, lens correctness)"
|
|
196
|
+
And ".gtd/REVIEW.md" contains ", slice 2, lens correctness)"
|
|
197
|
+
And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
|
|
198
|
+
And no commit body contains "build.quality.closing"
|