@pmelab/gtd 23.2.0 → 24.1.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 +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
installContext,
|
|
4
|
+
type Change,
|
|
5
|
+
type CodeThreadInfo,
|
|
6
|
+
type StepRequest,
|
|
7
|
+
} from "../../flows/index.js"
|
|
8
|
+
import { fixQualityFindings, handOffReview, REVIEW_ORIG } from "./tail.js"
|
|
9
|
+
import { fixtureContext } from "./text.fixture.js"
|
|
10
|
+
|
|
11
|
+
afterEach(() => installContext(undefined))
|
|
12
|
+
|
|
13
|
+
describe("the quality fix loop", () => {
|
|
14
|
+
it("a fix that resolves QUALITY.md and rewrites a scenario ends after the wording gate", async () => {
|
|
15
|
+
const QUALITY = ".gtd/QUALITY.md"
|
|
16
|
+
const log: string[] = []
|
|
17
|
+
const files = new Map([
|
|
18
|
+
[QUALITY, "- finding"],
|
|
19
|
+
["e2e/a.feature", "Given a"],
|
|
20
|
+
])
|
|
21
|
+
let last: readonly Change[] = []
|
|
22
|
+
installContext(
|
|
23
|
+
fixtureContext(
|
|
24
|
+
{},
|
|
25
|
+
{
|
|
26
|
+
step: (request: StepRequest) => {
|
|
27
|
+
if (request.kind === "restart") return Promise.resolve()
|
|
28
|
+
log.push(request.name)
|
|
29
|
+
last = []
|
|
30
|
+
if (request.name === "fix.quality.fixing") {
|
|
31
|
+
files.delete(QUALITY)
|
|
32
|
+
files.set("e2e/a.feature", "Given b")
|
|
33
|
+
last = [{ path: QUALITY, status: "deleted", before: "- finding", after: undefined }]
|
|
34
|
+
}
|
|
35
|
+
return Promise.resolve()
|
|
36
|
+
},
|
|
37
|
+
pushScope: () => undefined,
|
|
38
|
+
popScope: () => undefined,
|
|
39
|
+
read: (path) => files.get(path),
|
|
40
|
+
changes: () => last,
|
|
41
|
+
matches: (path, pattern) => path === pattern,
|
|
42
|
+
head: () => "h2",
|
|
43
|
+
start: () => "h0",
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
const frozen = { at: "h1", texts: { "e2e/a.feature": "Given a" } }
|
|
48
|
+
expect(await fixQualityFindings({ rounds: 0 }, frozen)).toBe(true)
|
|
49
|
+
expect(log).toEqual(["fix.quality.fixing", "scenario-wording"])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const REVIEW = ".gtd/REVIEW.md"
|
|
54
|
+
const reviewDoc = (note: string): string =>
|
|
55
|
+
[
|
|
56
|
+
"# Review: abc1234",
|
|
57
|
+
"",
|
|
58
|
+
"<!-- base: 0000000 -->",
|
|
59
|
+
"",
|
|
60
|
+
"## calc",
|
|
61
|
+
"- [ ] ./src/calc.ts#1",
|
|
62
|
+
note,
|
|
63
|
+
"",
|
|
64
|
+
].join("\n")
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run `handOffReview` with the human leaving `after` at the gate; `"deleted"`
|
|
68
|
+
* has the human delete the review, `"unwritten"` has the reviewer write none.
|
|
69
|
+
* Returns the steps and scripts, and the message of what it threw.
|
|
70
|
+
*/
|
|
71
|
+
const handOff = async (
|
|
72
|
+
after: string | "deleted" | "unwritten",
|
|
73
|
+
extra: { paths?: readonly string[]; codeThreads?: readonly CodeThreadInfo[] } = {},
|
|
74
|
+
) => {
|
|
75
|
+
const log: string[] = []
|
|
76
|
+
const scripts = new Map<string, string>()
|
|
77
|
+
const files = new Map<string, string>()
|
|
78
|
+
installContext(
|
|
79
|
+
fixtureContext(
|
|
80
|
+
{ codeThreads: extra.codeThreads ?? [] },
|
|
81
|
+
{
|
|
82
|
+
step: (request: StepRequest) => {
|
|
83
|
+
if (request.kind === "restart") return Promise.resolve()
|
|
84
|
+
log.push(request.name)
|
|
85
|
+
if (request.kind === "run") scripts.set(request.name, request.body)
|
|
86
|
+
if (request.name === "review.reviewing") {
|
|
87
|
+
if (log.filter((n) => n === "review.reviewing").length > 1) throw new Error("re-review")
|
|
88
|
+
if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
|
|
89
|
+
}
|
|
90
|
+
if (request.name === "review.await-review") {
|
|
91
|
+
if (after === "deleted") files.delete(REVIEW)
|
|
92
|
+
else if (after !== "unwritten") files.set(REVIEW, after)
|
|
93
|
+
}
|
|
94
|
+
return Promise.resolve()
|
|
95
|
+
},
|
|
96
|
+
refuse: (message: string): never => {
|
|
97
|
+
throw new Error(message)
|
|
98
|
+
},
|
|
99
|
+
pushScope: () => undefined,
|
|
100
|
+
popScope: () => undefined,
|
|
101
|
+
read: (path) => files.get(path),
|
|
102
|
+
matches: (path, pattern) => path === pattern,
|
|
103
|
+
changes: (): readonly Change[] =>
|
|
104
|
+
after === "deleted"
|
|
105
|
+
? [
|
|
106
|
+
{
|
|
107
|
+
path: REVIEW,
|
|
108
|
+
status: "deleted",
|
|
109
|
+
before: reviewDoc("new add function"),
|
|
110
|
+
after: undefined,
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
: [],
|
|
114
|
+
changesSince: (): readonly Change[] => [
|
|
115
|
+
{
|
|
116
|
+
path: REVIEW,
|
|
117
|
+
status: "modified",
|
|
118
|
+
before: reviewDoc("new add function"),
|
|
119
|
+
after: files.get(REVIEW),
|
|
120
|
+
},
|
|
121
|
+
...(extra.paths ?? []).map(
|
|
122
|
+
(path): Change => ({ path, status: "modified", before: "a", after: "b" }),
|
|
123
|
+
),
|
|
124
|
+
],
|
|
125
|
+
head: () => "c1",
|
|
126
|
+
start: () => "c0",
|
|
127
|
+
},
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
const refusal = await handOffReview("base", { rounds: 0 }).then(
|
|
131
|
+
() => undefined,
|
|
132
|
+
(e: unknown) => (e as Error).message,
|
|
133
|
+
)
|
|
134
|
+
return { log, scripts, refusal }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe("the hand-off review", () => {
|
|
138
|
+
it("no note signs off and closes the review", async () => {
|
|
139
|
+
const run = await handOff(reviewDoc("new add function"))
|
|
140
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("a note ends the round with the review kept and the reviewer's version copied beside it", async () => {
|
|
144
|
+
const run = await handOff(reviewDoc("new add function — rename it to sum"))
|
|
145
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.handing-off"])
|
|
146
|
+
expect(run.scripts.get("review.handing-off")).toContain(
|
|
147
|
+
`git show 'c1:${REVIEW}' > '${REVIEW_ORIG}'`,
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("an edit that parses to no note (a deleted chunk) is refused, not handed to a fix with nothing to read", async () => {
|
|
152
|
+
const run = await handOff("# Review: abc1234\n\n<!-- base: 0000000 -->\n")
|
|
153
|
+
expect(run.refusal).toMatch(/contains no note fix can read/)
|
|
154
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review"])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("a hand edit in a sign-off round keeps the suite green before closing", async () => {
|
|
158
|
+
const run = await handOff(reviewDoc("new add function"), { paths: ["src/calc.ts"] })
|
|
159
|
+
expect(run.log).toEqual([
|
|
160
|
+
"review.reviewing",
|
|
161
|
+
"review.await-review",
|
|
162
|
+
"health.check",
|
|
163
|
+
"review.closing",
|
|
164
|
+
])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("a change under .gtd/ alone does not run the green check", async () => {
|
|
168
|
+
const run = await handOff(reviewDoc("new add function"), { paths: [".gtd/OTHER.md"] })
|
|
169
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("a code thread waiting on the agent is refused, not signed off", async () => {
|
|
173
|
+
const run = await handOff(reviewDoc("new add function"), {
|
|
174
|
+
codeThreads: [
|
|
175
|
+
{ path: "src/calc.ts", line: 1, waitingOn: "agent", first: "why?", faults: [] },
|
|
176
|
+
],
|
|
177
|
+
})
|
|
178
|
+
expect(run.refusal).toMatch(/nothing answers it here/)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("a thread waiting on the human at the gate is refused", async () => {
|
|
182
|
+
const run = await handOff(reviewDoc("new add function"), {
|
|
183
|
+
codeThreads: [
|
|
184
|
+
{ path: "src/calc.ts", line: 1, waitingOn: "human", first: "why?", faults: [] },
|
|
185
|
+
],
|
|
186
|
+
})
|
|
187
|
+
expect(run.refusal).toMatch(/wait on you/)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("a round the reviewer left no review for rests at review-missing, then reviews again", async () => {
|
|
191
|
+
const run = await handOff("unwritten")
|
|
192
|
+
expect(run.log).toEqual([
|
|
193
|
+
"review.reviewing",
|
|
194
|
+
"review.await-review",
|
|
195
|
+
"review.review-missing",
|
|
196
|
+
"review.reviewing",
|
|
197
|
+
])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("a review the human deleted is refused, not re-reviewed", async () => {
|
|
201
|
+
const run = await handOff("deleted")
|
|
202
|
+
expect(run.refusal).toMatch(/was deleted — restore it, or leave a note to hand to fix/)
|
|
203
|
+
expect(run.log).toEqual(["review.reviewing", "review.await-review"])
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { quote, read, refuse, removeScript, run, scope, vars } from "../../flows/index.js"
|
|
2
|
+
import * as kit from "../../workflow-kit/index.js"
|
|
3
|
+
import { escalation, FIX_CAP, healthy, type EscalationCount } from "./health.js"
|
|
4
|
+
import { guarded, type FrozenScenarios } from "./scenarios.js"
|
|
5
|
+
import {
|
|
6
|
+
ARCHITECTURE,
|
|
7
|
+
awaitReview,
|
|
8
|
+
fixCheck,
|
|
9
|
+
fixQuality,
|
|
10
|
+
fixRisks,
|
|
11
|
+
QUALITY,
|
|
12
|
+
REVIEW,
|
|
13
|
+
reviewing,
|
|
14
|
+
reviewMissing,
|
|
15
|
+
reviewQuality,
|
|
16
|
+
} from "./steps.js"
|
|
17
|
+
import * as t from "./text.js"
|
|
18
|
+
import type { BuildContext } from "./text.js"
|
|
19
|
+
|
|
20
|
+
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
21
|
+
export const REVIEW_ORIG = ".gtd/REVIEW.orig.md"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One review turn per lens over the whole change, each appending every
|
|
25
|
+
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
26
|
+
* has any.
|
|
27
|
+
*/
|
|
28
|
+
export const qualityLap = (): Promise<"clean" | "findings"> =>
|
|
29
|
+
kit.qualityLap(kit.lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY)
|
|
30
|
+
|
|
31
|
+
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
32
|
+
export const fixQualityFindings = (
|
|
33
|
+
escalations: EscalationCount,
|
|
34
|
+
frozen?: FrozenScenarios,
|
|
35
|
+
): Promise<boolean> =>
|
|
36
|
+
kit.fixFindings({
|
|
37
|
+
fix: guarded(frozen, fixQuality),
|
|
38
|
+
findings: QUALITY,
|
|
39
|
+
cap: FIX_CAP,
|
|
40
|
+
escalate: () => escalation(escalations),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** What the tail's full run knows of a plan the process built. */
|
|
44
|
+
export interface TailPlan extends BuildContext {
|
|
45
|
+
readonly frozen: FrozenScenarios | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
49
|
+
export const closeReview = (round: string): Promise<void> =>
|
|
50
|
+
run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
51
|
+
label: "Closing the review",
|
|
52
|
+
base: round,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** Refuses while a reviewer's notes still await `gtd door fix`. */
|
|
56
|
+
export const refusePendingHandOff = (): void => {
|
|
57
|
+
if (read(REVIEW_ORIG) !== undefined) {
|
|
58
|
+
refuse(
|
|
59
|
+
`gtd: ${REVIEW_ORIG} is a pending hand-off — run \`gtd door fix\`, or remove ${REVIEW} and ${REVIEW_ORIG}.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
66
|
+
* `finish` — the human review, as the calling workflow runs it.
|
|
67
|
+
*/
|
|
68
|
+
export const tail = <T>(
|
|
69
|
+
fixFirst: boolean,
|
|
70
|
+
finish: (escalations: EscalationCount, frozen: FrozenScenarios | undefined) => Promise<T>,
|
|
71
|
+
built?: TailPlan,
|
|
72
|
+
): Promise<T> =>
|
|
73
|
+
scope("build", async () => {
|
|
74
|
+
const frozen = built?.frozen
|
|
75
|
+
const escalations: EscalationCount = { rounds: 0 }
|
|
76
|
+
const guardedFix = guarded(frozen, () => fixCheck())
|
|
77
|
+
const repair = async () => {
|
|
78
|
+
await guardedFix()
|
|
79
|
+
await healthy(guardedFix, { fixesSoFar: 1, escalations })
|
|
80
|
+
}
|
|
81
|
+
// fixFirst's own fix + healthy below already is the full run.
|
|
82
|
+
if (fixFirst) await repair()
|
|
83
|
+
else {
|
|
84
|
+
await healthy(
|
|
85
|
+
guarded(frozen, () => fixCheck(built)),
|
|
86
|
+
{ escalations, sweepOnGreen: [ARCHITECTURE] },
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
// The lap runs once a tail: after its findings are fixed, review follows.
|
|
90
|
+
if ((await qualityLap()) === "findings") {
|
|
91
|
+
if (await fixQualityFindings(escalations, frozen)) {
|
|
92
|
+
await healthy(guardedFix, { escalations })
|
|
93
|
+
} else await repair()
|
|
94
|
+
}
|
|
95
|
+
return finish(escalations, frozen)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// The committed blob, not the working copy: the human's notes already landed over it.
|
|
99
|
+
const handOffScript = (reviewed: string): string =>
|
|
100
|
+
[
|
|
101
|
+
"#!/usr/bin/env sh",
|
|
102
|
+
"set -e",
|
|
103
|
+
`git show ${quote(`${reviewed}:${REVIEW}`)} > ${quote(REVIEW_ORIG)}`,
|
|
104
|
+
"",
|
|
105
|
+
].join("\n")
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The review and fix workflows' human review since `base`: a sign-off closes
|
|
109
|
+
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
110
|
+
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
111
|
+
*/
|
|
112
|
+
export const handOffReview = (base: string, escalations: EscalationCount): Promise<void> =>
|
|
113
|
+
kit.reviewToHandOff(base, {
|
|
114
|
+
file: REVIEW,
|
|
115
|
+
reviewing,
|
|
116
|
+
fixRisks,
|
|
117
|
+
keepGreen: () => healthy(() => fixCheck(), { escalations }),
|
|
118
|
+
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
119
|
+
reviewMissing,
|
|
120
|
+
close: closeReview,
|
|
121
|
+
handOff: (reviewed) =>
|
|
122
|
+
run("review.handing-off", handOffScript(reviewed), {
|
|
123
|
+
label: "Handing your notes to fix",
|
|
124
|
+
}),
|
|
125
|
+
threadsPending:
|
|
126
|
+
"gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
|
|
127
|
+
unparsed:
|
|
128
|
+
"gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
|
|
129
|
+
deleted:
|
|
130
|
+
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix.",
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
134
|
+
export const handOffTail = (fixFirst: boolean, base: string): Promise<void> =>
|
|
135
|
+
tail(fixFirst, (escalations) => handOffReview(base, escalations))
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
type FlowContext,
|
|
6
6
|
type ScopeAccess,
|
|
7
7
|
type StepRequest,
|
|
8
|
-
} from "
|
|
9
|
-
import { foldAccess } from "
|
|
8
|
+
} from "../../flows/index.js"
|
|
9
|
+
import { foldAccess } from "../../replay/index.js"
|
|
10
10
|
import { access as bundledAccess } from "./access.js"
|
|
11
11
|
import { skills as bundledSkills } from "./skills.js"
|
|
12
12
|
import { defaults, envDefaults } from "./vars.js"
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
start,
|
|
10
10
|
type AgentOptions,
|
|
11
11
|
type SummaryContext,
|
|
12
|
-
} from "
|
|
12
|
+
} from "../../flows/index.js"
|
|
13
|
+
import type { WordingDrift } from "../../workflow-kit/index.js"
|
|
13
14
|
import {
|
|
14
15
|
accessPreamble,
|
|
15
16
|
skillsPreamble,
|
|
@@ -628,6 +629,31 @@ ${footnoteRules}
|
|
|
628
629
|
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
629
630
|
`
|
|
630
631
|
|
|
632
|
+
export const handOffAwaitReviewMessage = (base: string): string =>
|
|
633
|
+
`\`.gtd/REVIEW.md\` holds the review record — one \`- [ ]\` checkbox per
|
|
634
|
+
reviewable item, grouped into chunks. Tick a box as you read each hunk; ticks
|
|
635
|
+
are read-progress only, and landing clears them.
|
|
636
|
+
|
|
637
|
+
Review the diff yourself, with whatever tool you like (\`gtd base\` prints this
|
|
638
|
+
hash again):
|
|
639
|
+
|
|
640
|
+
git diff ${base}
|
|
641
|
+
When you've been through the whole diff, run \`gtd land\`:
|
|
642
|
+
|
|
643
|
+
- **Sign off** — leave no note in \`.gtd/REVIEW.md\` to close the process.
|
|
644
|
+
A code edit of your own stays as you left it; a comment thread in source is
|
|
645
|
+
not read.
|
|
646
|
+
- **Leave notes** — a note on a \`.gtd/REVIEW.md\` line, a footnote anchored to
|
|
647
|
+
a hunk, or prose under a chunk. The process ends with \`.gtd/REVIEW.md\` and
|
|
648
|
+
your notes left in place for \`gtd door fix\`, which fixes every note and
|
|
649
|
+
every \`Risk:\` the reviewer marked, then asks for your review again.
|
|
650
|
+
|
|
651
|
+
A footnote works the same way here as a line note:
|
|
652
|
+
|
|
653
|
+
${footnoteRules}
|
|
654
|
+
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
655
|
+
`
|
|
656
|
+
|
|
631
657
|
export const reviewEditsCapture = (commit: string): string =>
|
|
632
658
|
`This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
633
659
|
|
|
@@ -703,6 +729,27 @@ Commit: ${commit}
|
|
|
703
729
|
The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
|
|
704
730
|
${notesCapture(edits)}${answeredAt === undefined ? "" : `Answered questions: commit ${answeredAt} answered the question notes inline in .gtd/REVIEW.md. Run: git show ${answeredAt}\n`}`
|
|
705
731
|
|
|
732
|
+
export const fixReviewNotesPrompt = (
|
|
733
|
+
notes: readonly NoteInput[],
|
|
734
|
+
risks: readonly NoteInput[],
|
|
735
|
+
): string =>
|
|
736
|
+
`${stateFileRules}
|
|
737
|
+
- \`.gtd/REVIEW.md\` is a finished review; \`.gtd/REVIEW.orig.md\` is the same
|
|
738
|
+
review as the reviewer wrote it, before the human's notes
|
|
739
|
+
- Fix every note and every risk below in this one turn, all together; a note
|
|
740
|
+
that only praises needs no change; answer a note that asks a question in
|
|
741
|
+
your final message, since the review is removed afterwards
|
|
742
|
+
- Where a fix is behavioural, add a test that fails without it
|
|
743
|
+
- Leave both review files untouched — the next step removes them
|
|
744
|
+
- Leave everything uncommitted and finish your turn
|
|
745
|
+
|
|
746
|
+
The human's notes are:
|
|
747
|
+
|
|
748
|
+
${notes.length === 0 ? "(none)\n" : notesCapture(notes)}
|
|
749
|
+
The reviewer's risks are:
|
|
750
|
+
|
|
751
|
+
${risks.length === 0 ? "(none)\n" : notesCapture(risks)}`
|
|
752
|
+
|
|
706
753
|
export const buildReviewReviewMissingMessage = (commit: string): string =>
|
|
707
754
|
`The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
|
|
708
755
|
nothing to sign off on.
|
|
@@ -884,12 +931,6 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
884
931
|
`
|
|
885
932
|
}
|
|
886
933
|
|
|
887
|
-
export interface WordingDrift {
|
|
888
|
-
readonly path: string
|
|
889
|
-
/** `- `/`+ `-prefixed, in file order. */
|
|
890
|
-
readonly lines: readonly string[]
|
|
891
|
-
}
|
|
892
|
-
|
|
893
934
|
export const scenarioWordingMessage = (at: string, drifted: readonly WordingDrift[]): string =>
|
|
894
935
|
`The e2e scenario wording was frozen when package 0 landed, and the last
|
|
895
936
|
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Given } from "quickpickle"
|
|
2
|
+
import { CONFIG_SCAFFOLD } from "../../install/index.js"
|
|
3
|
+
import type { TestkitWorld } from "../../testkit/index.js"
|
|
4
|
+
|
|
5
|
+
Given("the shipped workflows, as gtd install scaffolds them", (world: TestkitWorld) => {
|
|
6
|
+
world.configure(CONFIG_SCAFFOLD)
|
|
7
|
+
})
|