@pmelab/gtd 18.1.0 → 20.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 +69 -69
- package/claude/hooks/doors.ts +34 -0
- package/claude/hooks/drive.ts +1 -0
- package/claude/hooks/register.tsx +12 -13
- package/claude/hooks/ship.ts +3 -2
- package/dist/gtd.bundle.mjs +847 -721
- package/package.json +3 -2
- package/skills/authoring/SKILL.md +43 -32
- package/src/flows/helpers.ts +4 -2
- package/src/flows/runtime.ts +22 -17
- package/src/flows/scripts.test.ts +28 -1
- package/src/flows/scripts.ts +3 -0
- package/src/workflows/access.test.ts +3 -4
- package/src/workflows/access.ts +0 -1
- package/src/workflows/{unified.ts → bundled.ts} +38 -31
- package/src/workflows/health.test.ts +21 -1
- package/src/workflows/health.ts +47 -8
- package/src/workflows/index.ts +1 -1
- package/src/workflows/packages.test.ts +91 -0
- package/src/workflows/packages.ts +96 -76
- package/src/workflows/planning.ts +5 -49
- package/src/workflows/prose.ts +1 -9
- package/src/workflows/review.test.ts +91 -4
- package/src/workflows/review.ts +42 -17
- package/src/workflows/scenarios.test.ts +256 -0
- package/src/workflows/scenarios.ts +137 -0
- package/src/workflows/skills.test.ts +2 -6
- package/src/workflows/skills.ts +0 -2
- package/src/workflows/steps.test.ts +2 -12
- package/src/workflows/steps.ts +2 -20
- package/src/workflows/text.test.ts +60 -0
- package/src/workflows/text.ts +118 -96
- package/src/workflows/vars.ts +1 -2
- package/claude/hooks/entry.ts +0 -34
- package/src/workflows/diff.test.ts +0 -115
- package/src/workflows/diff.ts +0 -306
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
"./flows": "./src/flows/index.ts",
|
|
11
|
-
"./workflow": "./src/workflows/
|
|
11
|
+
"./workflow": "./src/workflows/bundled.ts"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist/",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"watch": "tsdown --watch",
|
|
48
48
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
49
49
|
"test": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:e2e:inmem test:e2e:live test:web analyze",
|
|
50
|
+
"test:fast": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:web analyze",
|
|
50
51
|
"test:unit": "vitest run --project unit",
|
|
51
52
|
"test:watch": "vitest",
|
|
52
53
|
"test:changed": "vitest run --project unit --project e2e-inmem --changed HEAD",
|
|
@@ -11,33 +11,38 @@ description: >-
|
|
|
11
11
|
# Authoring a gtd workflow
|
|
12
12
|
|
|
13
13
|
A gtd workflow is **plain async TypeScript**: a `gtd.config.ts` at the
|
|
14
|
-
repository root
|
|
15
|
-
**steps** built from `@pmelab/gtd/flows`;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
repository root exports **workflows** — every exported async function is one,
|
|
15
|
+
named by its export, awaiting **steps** built from `@pmelab/gtd/flows`; the
|
|
16
|
+
`default` export is the ordinary start. Optional reserved exports (`defaults`
|
|
17
|
+
process settings, `envDefaults` environment settings, `summary`, `base`,
|
|
18
|
+
`steering`, `skills`, `doors`) are shared by the file's workflows. **Never
|
|
19
|
+
export a helper function you do not want startable** — it becomes a workflow.
|
|
20
|
+
Every step is a commit; gtd finds where a process rests by **replaying** the
|
|
21
|
+
flow over the episode's commits, so the git history IS the state and nothing is
|
|
22
|
+
stored anywhere else.
|
|
21
23
|
|
|
22
24
|
Your job is to produce or edit that module so it loads cleanly and does what the
|
|
23
25
|
user wants. Driving a workflow once it exists is a separate concern — that is
|
|
24
26
|
what a driver does.
|
|
25
27
|
|
|
26
28
|
**Trust:** gtd evaluates `gtd.config.ts` on every command that resolves workflow
|
|
27
|
-
state (`gtd next` and `gtd
|
|
28
|
-
runs; write it with the same care as a build script.
|
|
29
|
+
state (`gtd next`, `gtd lsp`, `gtd door` and `gtd doors` included). It is code
|
|
30
|
+
the user's repository runs; write it with the same care as a build script.
|
|
29
31
|
|
|
30
32
|
## Golden rule: start from the bundled default, edit incrementally
|
|
31
33
|
|
|
32
34
|
Do **not** write a workflow from a blank page unless the user wants something
|
|
33
|
-
tiny. gtd ships
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
tiny. gtd ships three startable workflows (`feature`, `review`, `fix`) as named
|
|
36
|
+
exports of `@pmelab/gtd/workflow` and runs them when no `gtd.config.ts` is
|
|
37
|
+
found. Its default export is `feature` (the ordinary start), and every phase and
|
|
38
|
+
single step they are built from is a named export too. Start by importing what
|
|
39
|
+
you keep and writing only what changes. Import phases by name; **never
|
|
40
|
+
`export *` from it** — every exported function of your file would become a
|
|
41
|
+
startable workflow:
|
|
37
42
|
|
|
38
43
|
```ts
|
|
39
44
|
import { start } from "@pmelab/gtd/flows"
|
|
40
|
-
import
|
|
45
|
+
import { afterTail, buildTail, feature } from "@pmelab/gtd/workflow"
|
|
41
46
|
|
|
42
47
|
export {
|
|
43
48
|
defaults,
|
|
@@ -45,14 +50,20 @@ export {
|
|
|
45
50
|
summary,
|
|
46
51
|
base,
|
|
47
52
|
steering,
|
|
53
|
+
skills,
|
|
48
54
|
} from "@pmelab/gtd/workflow"
|
|
49
55
|
|
|
50
|
-
export default
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
export default feature
|
|
57
|
+
|
|
58
|
+
export async function hotfix() {
|
|
59
|
+
return afterTail(await buildTail(true, start()))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const doors = { hotfix: { workflow: "hotfix" } }
|
|
54
63
|
```
|
|
55
64
|
|
|
65
|
+
`gtd --workflow hotfix` (or `gtd door hotfix`) starts it.
|
|
66
|
+
|
|
56
67
|
To change a phase itself, read its source in the npm package
|
|
57
68
|
(`node_modules/@pmelab/gtd/src/workflows/`, or under `$(npm root -g)` for a
|
|
58
69
|
global install) and write your own version in `gtd.config.ts`, reusing its
|
|
@@ -63,9 +74,9 @@ current directory is the whole workflow. If one already exists, read it and edit
|
|
|
63
74
|
it in place.
|
|
64
75
|
|
|
65
76
|
Prefer the bundled workflow's own parts over re-implementing them — `healthy`,
|
|
66
|
-
`escalation`, `gate`, `design`, `
|
|
67
|
-
`
|
|
68
|
-
|
|
77
|
+
`escalation`, `gate`, `design`, `architecture`, `packages`, `qualityLap`,
|
|
78
|
+
`buildTail`, and single steps like `triage` or `fixCheck`. Their full step names
|
|
79
|
+
are versioned API.
|
|
69
80
|
|
|
70
81
|
Make one small change, **verify it loads** (see "Verify"), then make the next. A
|
|
71
82
|
workflow that fails to load breaks every gtd command in the repository.
|
|
@@ -132,13 +143,14 @@ the pending landing — call it right after the step whose turn you reject.
|
|
|
132
143
|
- An episode ends when the flow returns or calls `restart()`; the next starts at
|
|
133
144
|
the flow's first step on an ordinary start — that step is where a finished
|
|
134
145
|
process waits (the bundled one is `human("idle", …)`).
|
|
135
|
-
- `gtd --
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
- `gtd --workflow <name>` starts a process on the exported workflow `<name>` (an
|
|
147
|
+
unknown name is a usage error); `gtd door <name> [args]` starts one through a
|
|
148
|
+
`doors` entry `{ workflow, args?, vars? }` (`gtd doors` lists them; bundled:
|
|
149
|
+
`fix`, `review [base]`; yours merge over them). An
|
|
150
|
+
`export const base = (workflow, vars) => commitish | undefined` fixes a
|
|
151
|
+
started process's diff base (blank = default-branch merge-base).
|
|
152
|
+
`--var <name>=<value>` only pins process settings: names the workflow's
|
|
153
|
+
`defaults` or `.gtdrc` `vars:` declare — never an environment setting.
|
|
142
154
|
|
|
143
155
|
## Landing rules you are designing for
|
|
144
156
|
|
|
@@ -212,7 +224,7 @@ sign-off between the two:
|
|
|
212
224
|
const planAndBuild = async (): Promise<void> => {
|
|
213
225
|
for (;;) {
|
|
214
226
|
await design()
|
|
215
|
-
await
|
|
227
|
+
await architecture()
|
|
216
228
|
await human("approve-plan", {
|
|
217
229
|
message:
|
|
218
230
|
"The packages under .gtd/packages/ are ready. Edit them to adjust the plan, or change nothing — then run `gtd land` to start building.",
|
|
@@ -229,9 +241,8 @@ const planAndBuild = async (): Promise<void> => {
|
|
|
229
241
|
`acceptClean: true` is what makes an untouched landing approve; without it the
|
|
230
242
|
gate would wait for an edit. `approve-plan` sits in the `root` scope and is a
|
|
231
243
|
new, unique name. Verify: `gtd next` loads without errors, and in a scratch
|
|
232
|
-
repository a landing at `architecture.decompose`
|
|
233
|
-
|
|
234
|
-
`packages.item.building`.
|
|
244
|
+
repository a landing at `architecture.decompose` now leads to `approve-plan`,
|
|
245
|
+
and an untouched landing there to `packages.item.building`.
|
|
235
246
|
|
|
236
247
|
## No migration
|
|
237
248
|
|
package/src/flows/helpers.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface CheckOptions {
|
|
|
36
36
|
readonly sweep?: readonly string[] | undefined
|
|
37
37
|
/** Paths removed once it passes. */
|
|
38
38
|
readonly sweepOnGreen?: readonly string[] | undefined
|
|
39
|
+
/** Shell lines run before the command, outside its output capture; an `exit 0` skips it and leaves `report` untouched. */
|
|
40
|
+
readonly preamble?: readonly string[] | undefined
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -48,10 +50,10 @@ export const check = async (
|
|
|
48
50
|
command: string,
|
|
49
51
|
options: CheckOptions,
|
|
50
52
|
): Promise<boolean> => {
|
|
51
|
-
const { report, label, sweep, sweepOnGreen } = options
|
|
53
|
+
const { report, label, sweep, sweepOnGreen, preamble } = options
|
|
52
54
|
await run(
|
|
53
55
|
name,
|
|
54
|
-
checkScript(command, { report, stamp: head().slice(0, 7), sweep, sweepOnGreen }),
|
|
56
|
+
checkScript(command, { report, stamp: head().slice(0, 7), sweep, sweepOnGreen, preamble }),
|
|
55
57
|
{ label },
|
|
56
58
|
)
|
|
57
59
|
return !wrote(report)
|
package/src/flows/runtime.ts
CHANGED
|
@@ -259,7 +259,7 @@ export const changesSince = (hash: string, pattern?: string): Changes => {
|
|
|
259
259
|
/** The commit the process stands on at this point of the flow. */
|
|
260
260
|
export const head = (): string => ctx().head()
|
|
261
261
|
|
|
262
|
-
/** The process's diff base: the commit before it began, or the base `gtd --
|
|
262
|
+
/** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
|
|
263
263
|
export const start = (): string => ctx().start()
|
|
264
264
|
|
|
265
265
|
/** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
|
|
@@ -348,17 +348,8 @@ export const openQuestions = (text: string): readonly OpenQuestion[] => ctx().op
|
|
|
348
348
|
|
|
349
349
|
// ── The workflow ────────────────────────────────────────────────────────────
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
readonly entry: string | undefined
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* A workflow's one flow. A flow that never reads `entry` accepts no
|
|
358
|
-
* `--entry`; one that does decides for itself which names it honours,
|
|
359
|
-
* `refuse()`-ing the rest.
|
|
360
|
-
*/
|
|
361
|
-
export type Flow = (args: FlowArgs) => Promise<void>
|
|
351
|
+
/** A workflow's one flow. */
|
|
352
|
+
export type Flow = () => Promise<void>
|
|
362
353
|
|
|
363
354
|
export interface SummaryContext {
|
|
364
355
|
readonly entryCommit: string
|
|
@@ -375,11 +366,25 @@ export interface SummaryContext {
|
|
|
375
366
|
export type Summary = (context: SummaryContext) => string
|
|
376
367
|
|
|
377
368
|
/**
|
|
378
|
-
* A workflow
|
|
379
|
-
*
|
|
380
|
-
* none. Runs when the process is
|
|
369
|
+
* A workflow file's optional `base` export: the commitish that fixes the diff
|
|
370
|
+
* base of a process `gtd --workflow <workflow>` starts, or `undefined` for
|
|
371
|
+
* none. Runs when the process is started, with the vars `--var` sets.
|
|
381
372
|
*/
|
|
382
|
-
export type
|
|
383
|
-
|
|
373
|
+
export type WorkflowBase = (
|
|
374
|
+
workflow: string,
|
|
384
375
|
vars: Readonly<Record<string, string>>,
|
|
385
376
|
) => string | undefined
|
|
377
|
+
|
|
378
|
+
/** A named shortcut: `gtd door <name> [args…]` starts `workflow` with the positional args mapped to process settings. */
|
|
379
|
+
export interface Door {
|
|
380
|
+
readonly workflow: string
|
|
381
|
+
/** Static, so `gtd doors --json` can list them. */
|
|
382
|
+
readonly args?: readonly { readonly name: string; readonly optional?: boolean }[]
|
|
383
|
+
/** A pure function of the args: no git runs at declaration time. */
|
|
384
|
+
readonly vars?: (
|
|
385
|
+
args: Readonly<Record<string, string | undefined>>,
|
|
386
|
+
) => Readonly<Record<string, string>>
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** A workflow module's reserved `doors` export. */
|
|
390
|
+
export type Doors = Readonly<Record<string, Door>>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
1
5
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { restoreScript } from "./scripts.js"
|
|
6
|
+
import { checkScript, restoreScript } from "./scripts.js"
|
|
3
7
|
|
|
4
8
|
describe("restoreScript", () => {
|
|
5
9
|
const paths = { restore: ["a.ts"], remove: [] }
|
|
@@ -14,3 +18,26 @@ describe("restoreScript", () => {
|
|
|
14
18
|
expect(script).not.toContain("abc~1")
|
|
15
19
|
})
|
|
16
20
|
})
|
|
21
|
+
|
|
22
|
+
describe("checkScript preamble", () => {
|
|
23
|
+
const options = { report: ".gtd/FEEDBACK.md", stamp: "abc" }
|
|
24
|
+
const runIn = (script: string, dir: string): void => {
|
|
25
|
+
writeFileSync(join(dir, "s.sh"), script)
|
|
26
|
+
spawnSync("sh", ["s.sh"], { cwd: dir })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("renders before the command subshell", () => {
|
|
30
|
+
const script = checkScript("npm test", { ...options, preamble: ["echo pre"] })
|
|
31
|
+
expect(script.indexOf("echo pre")).toBeGreaterThan(-1)
|
|
32
|
+
expect(script.indexOf("echo pre")).toBeLessThan(script.indexOf("npm test"))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("an exit 0 in the preamble skips the command and leaves report alone", () => {
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), "gtd-pre-"))
|
|
37
|
+
mkdirSync(join(dir, ".gtd"))
|
|
38
|
+
writeFileSync(join(dir, ".gtd/FEEDBACK.md"), "old")
|
|
39
|
+
runIn(checkScript("touch ran", { ...options, preamble: ["exit 0"] }), dir)
|
|
40
|
+
expect(existsSync(join(dir, "ran"))).toBe(false)
|
|
41
|
+
expect(readFileSync(join(dir, ".gtd/FEEDBACK.md"), "utf8")).toBe("old")
|
|
42
|
+
})
|
|
43
|
+
})
|
package/src/flows/scripts.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface CheckScriptOptions {
|
|
|
14
14
|
readonly sweep?: readonly string[] | undefined
|
|
15
15
|
/** Paths removed once it passes. */
|
|
16
16
|
readonly sweepOnGreen?: readonly string[] | undefined
|
|
17
|
+
/** Shell lines run before the command, outside its output capture; an `exit 0` skips it and leaves `report` untouched. */
|
|
18
|
+
readonly preamble?: readonly string[] | undefined
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const removal = (paths: readonly string[]): string[] =>
|
|
@@ -29,6 +31,7 @@ export const checkScript = (command: string, options: CheckScriptOptions): strin
|
|
|
29
31
|
return [
|
|
30
32
|
"#!/usr/bin/env sh",
|
|
31
33
|
"set +e",
|
|
34
|
+
...(options.preamble ?? []),
|
|
32
35
|
...removal(options.sweep ?? []),
|
|
33
36
|
`mkdir -p "$(dirname ${report})"`,
|
|
34
37
|
// A subshell, so an `exit` inside the command ends only the command.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { access } from "./access.js"
|
|
3
|
-
import {
|
|
3
|
+
import { bundled as workflow } from "./index.js"
|
|
4
4
|
|
|
5
|
-
const { defaults } =
|
|
5
|
+
const { defaults } = workflow
|
|
6
6
|
|
|
7
7
|
const bundled = access(defaults)
|
|
8
8
|
|
|
@@ -12,7 +12,6 @@ describe("the bundled workflow's access export", () => {
|
|
|
12
12
|
design: { write: [] },
|
|
13
13
|
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
14
14
|
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
15
|
-
"packages.item.spec": { write: [".gtd/SPEC_FEEDBACK.md"] },
|
|
16
15
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
17
16
|
"build.review.fix.nits": {},
|
|
18
17
|
"build.review.fix.risks": {},
|
|
@@ -35,7 +34,7 @@ describe("the bundled workflow's access export", () => {
|
|
|
35
34
|
})
|
|
36
35
|
|
|
37
36
|
it("keys only scopes that run a turn (every key is a skills key)", () => {
|
|
38
|
-
const known = Object.keys(
|
|
37
|
+
const known = Object.keys(workflow.skills(defaults))
|
|
39
38
|
for (const key of Object.keys(bundled)) expect(known).toContain(key)
|
|
40
39
|
})
|
|
41
40
|
})
|
package/src/workflows/access.ts
CHANGED
|
@@ -12,7 +12,6 @@ export const access = (
|
|
|
12
12
|
design: { write: [] },
|
|
13
13
|
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
14
14
|
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
15
|
-
"packages.item.spec": { write: [".gtd/SPEC_FEEDBACK.md"] },
|
|
16
15
|
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
17
16
|
// `{}` reopens what `build.review` restricted: the fixes edit code.
|
|
18
17
|
"build.review.fix.nits": {},
|
|
@@ -2,30 +2,30 @@ import {
|
|
|
2
2
|
changes,
|
|
3
3
|
head,
|
|
4
4
|
human,
|
|
5
|
-
refuse,
|
|
6
5
|
requireRevert,
|
|
7
6
|
restoreScript,
|
|
8
7
|
revertScript,
|
|
9
8
|
run,
|
|
10
9
|
start,
|
|
11
|
-
type
|
|
12
|
-
type
|
|
10
|
+
type Doors,
|
|
11
|
+
type WorkflowBase,
|
|
13
12
|
type Summary,
|
|
14
13
|
} from "../flows/index.js"
|
|
15
14
|
import { baseline, gate } from "./health.js"
|
|
16
15
|
import { packages } from "./packages.js"
|
|
17
|
-
import {
|
|
16
|
+
import { architecture, design } from "./planning.js"
|
|
17
|
+
import type { FrozenScenarios } from "./scenarios.js"
|
|
18
18
|
import { buildTail, type ReviewOutcome } from "./review.js"
|
|
19
19
|
import { ARCHITECTURE, FEEDBACK, REQUIREMENTS, REVIEW } from "./steps.js"
|
|
20
20
|
import * as t from "./text.js"
|
|
21
21
|
|
|
22
|
-
// gtd's
|
|
22
|
+
// gtd's bundled workflows. Any change to the tree starts a process:
|
|
23
23
|
// `idle` → `unwind` reverts the sketch (its intent survives in history) → a
|
|
24
24
|
// green-baseline gate → design, architecture and one package per concern →
|
|
25
25
|
// the quality lap → human review, which signs off (the episode ends back at
|
|
26
|
-
// `idle`) or sends a full re-plan lap.
|
|
27
|
-
// review
|
|
28
|
-
//
|
|
26
|
+
// `idle`) or sends a full re-plan lap. `feature` is that ordinary start;
|
|
27
|
+
// `gtd --workflow fix` and `gtd --workflow review --var reviewBase=<commitish>`
|
|
28
|
+
// enter the same build tail further in.
|
|
29
29
|
//
|
|
30
30
|
// Every part is exported for other workflows to compose; see the modules
|
|
31
31
|
// re-exported below.
|
|
@@ -39,6 +39,7 @@ export * from "./steps.js"
|
|
|
39
39
|
export * from "./health.js"
|
|
40
40
|
export * from "./planning.js"
|
|
41
41
|
export * from "./packages.js"
|
|
42
|
+
export * from "./scenarios.js"
|
|
42
43
|
export * from "./review.js"
|
|
43
44
|
|
|
44
45
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
@@ -70,11 +71,13 @@ export const reUnwind = async (
|
|
|
70
71
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
71
72
|
export const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
72
73
|
let base = firstBase
|
|
74
|
+
let carried: FrozenScenarios | undefined
|
|
73
75
|
for (;;) {
|
|
74
76
|
await design(base)
|
|
75
|
-
await
|
|
76
|
-
await packages()
|
|
77
|
-
|
|
77
|
+
await architecture()
|
|
78
|
+
const built = await packages(carried)
|
|
79
|
+
carried = built.frozen
|
|
80
|
+
const outcome = await buildTail(false, base, built)
|
|
78
81
|
if (outcome.verdict === "signoff") return
|
|
79
82
|
await reUnwind(outcome)
|
|
80
83
|
base = outcome.base
|
|
@@ -103,30 +106,34 @@ export const ordinaryStart = async (): Promise<void> => {
|
|
|
103
106
|
await planAndBuild(start())
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
/** Repair a red baseline through the build tail, as its own reviewed commit. */
|
|
110
|
+
export const fix = async (): Promise<void> => {
|
|
111
|
+
if (await baseline("fix-precheck")) return
|
|
112
|
+
return afterTail(await buildTail(true, start()))
|
|
113
|
+
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return afterTail(await buildTail(true, start()))
|
|
113
|
-
}
|
|
114
|
-
if (entry === "review-gate.check") {
|
|
115
|
-
await gate("review-gate", t.reviewGateBlockedMessage())
|
|
116
|
-
return afterTail(await buildTail(false, start()))
|
|
117
|
-
}
|
|
118
|
-
if (entry === "start-gate.check") {
|
|
119
|
-
await gate("start-gate", t.startGateBlockedMessage())
|
|
120
|
-
return planAndBuild(start())
|
|
121
|
-
}
|
|
122
|
-
refuse(
|
|
123
|
-
`"${entry}" is not an enterable state — enterable states:\n${ENTRIES.map((name) => ` ${name}`).join("\n")}`,
|
|
124
|
-
)
|
|
115
|
+
/** Pure review of everything since `reviewBase`. */
|
|
116
|
+
export const review = async (): Promise<void> => {
|
|
117
|
+
await gate("review-gate", t.reviewGateBlockedMessage())
|
|
118
|
+
return afterTail(await buildTail(false, start()))
|
|
125
119
|
}
|
|
126
120
|
|
|
121
|
+
export const feature = ordinaryStart
|
|
122
|
+
|
|
123
|
+
export default feature
|
|
124
|
+
|
|
127
125
|
export const summary: Summary = t.summaryPrompt
|
|
128
126
|
|
|
129
127
|
export const steering = { [REQUIREMENTS]: "qa", [ARCHITECTURE]: "qa", [REVIEW]: "review" }
|
|
130
128
|
|
|
131
|
-
export const base:
|
|
132
|
-
|
|
129
|
+
export const base: WorkflowBase = (workflow, vars) =>
|
|
130
|
+
workflow === "review" ? (vars.reviewBase ?? "") : undefined
|
|
131
|
+
|
|
132
|
+
export const doors: Doors = {
|
|
133
|
+
fix: { workflow: "fix" },
|
|
134
|
+
review: {
|
|
135
|
+
workflow: "review",
|
|
136
|
+
args: [{ name: "base", optional: true }],
|
|
137
|
+
vars: ({ base }) => (base === undefined ? {} : { reviewBase: base }),
|
|
138
|
+
},
|
|
139
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { toRequest } from "../judges/index.js"
|
|
3
|
-
import { retryQuestion } from "./health.js"
|
|
3
|
+
import { fastSuiteGuard, retryQuestion } from "./health.js"
|
|
4
|
+
import { renderText } from "./text.fixture.js"
|
|
4
5
|
|
|
5
6
|
describe("retryQuestion", () => {
|
|
6
7
|
it.each([true, false])("is sendable to jev (comparable=%s)", (comparable) => {
|
|
@@ -11,3 +12,22 @@ describe("retryQuestion", () => {
|
|
|
11
12
|
).toBeGreaterThanOrEqual(2)
|
|
12
13
|
})
|
|
13
14
|
})
|
|
15
|
+
|
|
16
|
+
describe("fastSuiteGuard", () => {
|
|
17
|
+
it("unset: names the setting and exits 0", () => {
|
|
18
|
+
const lines = renderText(fastSuiteGuard).join("\n")
|
|
19
|
+
expect(lines).toContain("fastTestCommand")
|
|
20
|
+
expect(lines).toContain("GTD_FASTTESTCOMMAND")
|
|
21
|
+
expect(lines).toContain("exit 0")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("blank after trim counts as unset", () => {
|
|
25
|
+
expect(renderText(fastSuiteGuard, { env: { fastTestCommand: " \t " } })).toContain("exit 0")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("set: only removes SETUP", () => {
|
|
29
|
+
const lines = renderText(fastSuiteGuard, { env: { fastTestCommand: "true" } })
|
|
30
|
+
expect(lines.join("\n")).not.toContain("exit 0")
|
|
31
|
+
expect(lines.join("\n")).toContain("rm -f")
|
|
32
|
+
})
|
|
33
|
+
})
|
package/src/workflows/health.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname } from "node:path"
|
|
1
2
|
import {
|
|
2
3
|
answered,
|
|
3
4
|
check,
|
|
@@ -5,6 +6,7 @@ import {
|
|
|
5
6
|
human,
|
|
6
7
|
judge,
|
|
7
8
|
numeric,
|
|
9
|
+
quote,
|
|
8
10
|
read,
|
|
9
11
|
scope,
|
|
10
12
|
vars,
|
|
@@ -13,12 +15,37 @@ import {
|
|
|
13
15
|
import { describeEscalation, escalate, escalationExhausted, ESCALATION, FEEDBACK } from "./steps.js"
|
|
14
16
|
import * as t from "./text.js"
|
|
15
17
|
|
|
18
|
+
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
19
|
+
export const SETUP = ".gtd/SETUP.md"
|
|
20
|
+
|
|
21
|
+
const SETUP_TEXT =
|
|
22
|
+
"The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it under `env:` in `.gtdrc` or as GTD_FASTTESTCOMMAND."
|
|
23
|
+
|
|
24
|
+
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
25
|
+
export const fastSuiteGuard = (): string[] =>
|
|
26
|
+
(env.fastTestCommand ?? "").trim() === ""
|
|
27
|
+
? [
|
|
28
|
+
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
29
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
30
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
31
|
+
"exit 0",
|
|
32
|
+
]
|
|
33
|
+
: [`rm -f ${quote(SETUP)}`]
|
|
34
|
+
|
|
16
35
|
/** Fix turns a run of red checks gets before it escalates. */
|
|
17
36
|
export const FIX_CAP = 3
|
|
18
37
|
|
|
19
38
|
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
20
|
-
export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
|
|
21
|
-
|
|
39
|
+
export const baseline = async (name: string, label = "Checking the baseline"): Promise<boolean> => {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const green = await check(name, env.testCommand ?? "", {
|
|
42
|
+
report: FEEDBACK,
|
|
43
|
+
label,
|
|
44
|
+
preamble: fastSuiteGuard(),
|
|
45
|
+
})
|
|
46
|
+
if (read(SETUP) === undefined) return green
|
|
47
|
+
}
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
/** How many escalation rounds a run of red checks has spent — reset once the suite goes green. */
|
|
24
51
|
export interface EscalationCount {
|
|
@@ -81,6 +108,21 @@ export interface HealthOptions {
|
|
|
81
108
|
readonly fixesSoFar?: number
|
|
82
109
|
/** Escalation rounds shared with other callers in the same run of red checks. */
|
|
83
110
|
readonly escalations?: EscalationCount
|
|
111
|
+
/** `fast` runs `fastTestCommand` (default `full`: `testCommand`). */
|
|
112
|
+
readonly suite?: "full" | "fast"
|
|
113
|
+
/** Paths removed on a green run. */
|
|
114
|
+
readonly sweepOnGreen?: readonly string[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const runSuite = (options: HealthOptions): Promise<boolean> => {
|
|
118
|
+
const fast = options.suite === "fast"
|
|
119
|
+
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
120
|
+
report: FEEDBACK,
|
|
121
|
+
label: "Running checks",
|
|
122
|
+
// Swept only on green: an unresolved analysis survives every retry.
|
|
123
|
+
sweepOnGreen: [ESCALATION, ...(options.sweepOnGreen ?? [])],
|
|
124
|
+
...(fast ? { preamble: fastSuiteGuard() } : {}),
|
|
125
|
+
})
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
/**
|
|
@@ -96,12 +138,9 @@ export const healthy = async (
|
|
|
96
138
|
let fixes = options.fixesSoFar ?? 0
|
|
97
139
|
let previous: string | undefined
|
|
98
140
|
for (;;) {
|
|
99
|
-
const green = await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Swept only on green: an unresolved analysis survives every retry.
|
|
103
|
-
sweepOnGreen: [ESCALATION],
|
|
104
|
-
})
|
|
141
|
+
const green = await runSuite(options)
|
|
142
|
+
// Unconfigured: straight back to the check, no fix turn or count.
|
|
143
|
+
if (options.suite === "fast" && read(SETUP) !== undefined) continue
|
|
105
144
|
if (green) {
|
|
106
145
|
escalations.rounds = 0
|
|
107
146
|
return
|
package/src/workflows/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * as
|
|
1
|
+
export * as bundled from "./bundled.js"
|