@pmelab/gtd 58.1.0 → 59.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 +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +30 -2
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/gherkin/git-control.feature +78 -0
- package/src/gherkin/index.ts +35 -1
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +118 -2
- package/src/testkit/TestRepo.ts +57 -6
- package/src/testkit/index.ts +1 -0
- package/src/twins/EmittedScriptRecognizer.ts +12 -14
- package/src/twins/FlowScripts.ts +10 -9
- package/src/twins/InMemRepo.ts +26 -1
- package/src/twins/Layers.ts +12 -1
- package/src/twins/index.ts +6 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
package/src/cli/Program.test.ts
CHANGED
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
restoredOutcome,
|
|
28
28
|
} from "../emit/index.js"
|
|
29
29
|
import { HISTORY_REF } from "../platform/index.js"
|
|
30
|
-
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
31
30
|
import { contentHash } from "../workflow/index.js"
|
|
31
|
+
import { WORKFLOWS_REPO } from "../install/index.js"
|
|
32
32
|
|
|
33
33
|
const run = async (
|
|
34
34
|
repo: InMemRepo,
|
|
@@ -64,15 +64,6 @@ const seed = (source: string, extra: Record<string, string> = {}): InMemRepo =>
|
|
|
64
64
|
return repo
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/** A repo `gtd install` set up: its config runs the shipped workflows. */
|
|
68
|
-
const seedBundled = (): InMemRepo => {
|
|
69
|
-
const repo = new InMemRepo()
|
|
70
|
-
repo.writeFile("README.md", "# test project\n")
|
|
71
|
-
repo.writeFile("gtd.config.ts", CONFIG_SCAFFOLD)
|
|
72
|
-
repo.seedCommit("chore: init")
|
|
73
|
-
return repo
|
|
74
|
-
}
|
|
75
|
-
|
|
76
67
|
const IDLE_THEN_WORKING = `import { agent, human } from "@pmelab/gtd/flows"
|
|
77
68
|
|
|
78
69
|
export default async () => {
|
|
@@ -81,6 +72,15 @@ export default async () => {
|
|
|
81
72
|
}
|
|
82
73
|
`
|
|
83
74
|
|
|
75
|
+
/** A repo at rest: its config runs {@link IDLE_THEN_WORKING}. */
|
|
76
|
+
const seedIdle = (): InMemRepo => {
|
|
77
|
+
const repo = new InMemRepo()
|
|
78
|
+
repo.writeFile("README.md", "# test project\n")
|
|
79
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
80
|
+
repo.seedCommit("chore: init")
|
|
81
|
+
return repo
|
|
82
|
+
}
|
|
83
|
+
|
|
84
84
|
describe("gtd --workflow <name> — a custom workflow's named workflow", () => {
|
|
85
85
|
const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
|
|
86
86
|
|
|
@@ -194,9 +194,50 @@ export const defaults = { greeting: "hello" }
|
|
|
194
194
|
})
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
-
describe("gtd
|
|
197
|
+
describe("gtd install", () => {
|
|
198
|
+
it("where no gtd.config.ts governs, prints the briefing and warns where to get a workflow — writing nothing", async () => {
|
|
199
|
+
const repo = new InMemRepo()
|
|
200
|
+
repo.writeFile("README.md", "# test project\n")
|
|
201
|
+
repo.seedCommit("chore: init")
|
|
202
|
+
const before = repo.snapshot()
|
|
203
|
+
const { stdout, stderr, exitCode } = await run(repo, "install")
|
|
204
|
+
expect(exitCode).toBe(0)
|
|
205
|
+
expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
|
|
206
|
+
expect(stderr).toContain(
|
|
207
|
+
"gtd install: no gtd.config.ts governs here, and gtd ships no workflow",
|
|
208
|
+
)
|
|
209
|
+
expect(stderr).toContain(WORKFLOWS_REPO)
|
|
210
|
+
expect(repo.hasPath("gtd.config.ts")).toBe(false)
|
|
211
|
+
expect(repo.snapshot()).toBe(before)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("where a gtd.config.ts governs, prints the briefing alone", async () => {
|
|
215
|
+
const repo = seedIdle()
|
|
216
|
+
const { stdout, stderr, exitCode } = await run(repo, "install")
|
|
217
|
+
expect(exitCode).toBe(0)
|
|
218
|
+
expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
|
|
219
|
+
expect(stderr).not.toContain(WORKFLOWS_REPO)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("gtd --workflow <name> — a workflow that fixes its diff base", () => {
|
|
224
|
+
const WORKFLOW = `import { human } from "@pmelab/gtd/flows"
|
|
225
|
+
|
|
226
|
+
export default async () => {
|
|
227
|
+
await human("idle", { message: "hi" })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const review = async () => {
|
|
231
|
+
await human("checking", { message: "check" })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const defaults = { reviewBase: "" }
|
|
235
|
+
|
|
236
|
+
export const base = (workflow, vars) => (workflow === "review" ? vars.reviewBase : undefined)
|
|
237
|
+
`
|
|
238
|
+
|
|
198
239
|
it("--workflow review --var reviewBase=<base> emits a script anchoring the process to that base", async () => {
|
|
199
|
-
const repo =
|
|
240
|
+
const repo = seed(WORKFLOW)
|
|
200
241
|
const base = repo.commitHistory().at(-1)!.hash
|
|
201
242
|
repo.writeFile("scratch.txt", "more work\n")
|
|
202
243
|
repo.seedCommit("chore: more work")
|
|
@@ -209,26 +250,16 @@ describe("gtd --workflow <name> — the shipped workflows", () => {
|
|
|
209
250
|
`reviewBase=${base}`,
|
|
210
251
|
)
|
|
211
252
|
expect(exitCode).toBe(0)
|
|
212
|
-
expect(stdout).toContain("gtd(human):
|
|
253
|
+
expect(stdout).toContain("gtd(human): checking")
|
|
213
254
|
expect(stdout).toContain("Gtd-Workflow: review")
|
|
214
255
|
expect(stdout).toContain(`Gtd-Review-Base: ${base}`)
|
|
215
256
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
216
257
|
})
|
|
217
|
-
|
|
218
|
-
it("--workflow fix emits a script that would start a fix process", async () => {
|
|
219
|
-
const repo = seedBundled()
|
|
220
|
-
const before = repo.commitHistory().length
|
|
221
|
-
const { stdout, exitCode } = await run(repo, "--workflow", "fix")
|
|
222
|
-
expect(exitCode).toBe(0)
|
|
223
|
-
expect(stdout).toContain("gtd(human): fix-precheck")
|
|
224
|
-
expect(stdout).toContain("Gtd-Workflow: fix")
|
|
225
|
-
expect(repo.commitHistory()).toHaveLength(before)
|
|
226
|
-
})
|
|
227
258
|
})
|
|
228
259
|
|
|
229
260
|
describe("config loading", () => {
|
|
230
261
|
it('gtd next --verbose narrates "config: module" exactly once — the extra load that surfaces warnings is silent', async () => {
|
|
231
|
-
const repo =
|
|
262
|
+
const repo = seedIdle()
|
|
232
263
|
const { stderr, exitCode } = await run(repo, "next", "--verbose")
|
|
233
264
|
expect(exitCode).toBe(0)
|
|
234
265
|
expect(stderr.match(/config: module /g)).toHaveLength(1)
|
|
@@ -287,7 +318,7 @@ describe("gtd next --json — log path emission", () => {
|
|
|
287
318
|
const seededRepo = (): InMemRepo => {
|
|
288
319
|
const repo = new InMemRepo()
|
|
289
320
|
repo.writeFile("NOTE.md", "a note\n")
|
|
290
|
-
repo.writeFile("gtd.config.ts",
|
|
321
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
291
322
|
repo.seedCommit("gtd: init")
|
|
292
323
|
return repo
|
|
293
324
|
}
|
|
@@ -1752,7 +1783,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1752
1783
|
)
|
|
1753
1784
|
|
|
1754
1785
|
it("a repository with a commit passes the guard and dispatches normally", async () => {
|
|
1755
|
-
const repo =
|
|
1786
|
+
const repo = seedIdle()
|
|
1756
1787
|
const written: string[] = []
|
|
1757
1788
|
const out = { write: (chunk: string) => written.push(chunk), flush: () => {} }
|
|
1758
1789
|
|
|
@@ -1765,7 +1796,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1765
1796
|
})
|
|
1766
1797
|
|
|
1767
1798
|
it("gtd ui in a repository WITH commits reaches its own dispatch, past the guard", async () => {
|
|
1768
|
-
const repo =
|
|
1799
|
+
const repo = seedIdle()
|
|
1769
1800
|
const out = { write: () => {}, flush: () => {} }
|
|
1770
1801
|
const requests: unknown[] = []
|
|
1771
1802
|
const ui = (request: unknown) => Effect.sync(() => void requests.push(request))
|
|
@@ -1950,7 +1981,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
|
|
|
1950
1981
|
const seededRepo = (): InMemRepo => {
|
|
1951
1982
|
const repo = new InMemRepo()
|
|
1952
1983
|
repo.writeFile("NOTE.md", "a note\n")
|
|
1953
|
-
repo.writeFile("gtd.config.ts",
|
|
1984
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
1954
1985
|
repo.seedCommit("gtd: init")
|
|
1955
1986
|
return repo
|
|
1956
1987
|
}
|
|
@@ -1987,7 +2018,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
|
|
|
1987
2018
|
|
|
1988
2019
|
it("an absent selector writes zero bytes to stdout and exits EXIT_OK", async () => {
|
|
1989
2020
|
const repo = seededRepo()
|
|
1990
|
-
// The
|
|
2021
|
+
// The workflow's initial `idle` step declares no `mode:` —
|
|
1991
2022
|
// `mode` is a real, present-but-`undefined` field on the beat document
|
|
1992
2023
|
// there (see `src/wire/BeatStatus.ts`'s `statusOf`), not merely an
|
|
1993
2024
|
// unknown path.
|
package/src/cli/Program.ts
CHANGED
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
abandonNoopOutcome,
|
|
88
88
|
restoredOutcome,
|
|
89
89
|
} from "../emit/index.js"
|
|
90
|
-
import {
|
|
90
|
+
import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
91
91
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
92
92
|
import {
|
|
93
93
|
SelectorUsageError,
|
|
@@ -207,13 +207,12 @@ const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Prints the briefing, emitted by the binary itself so it's always current.
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* Anywhere else it writes nothing.
|
|
210
|
+
* gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
|
|
211
|
+
* get one; it writes nothing.
|
|
213
212
|
*/
|
|
214
213
|
const runInstallCommand = (
|
|
215
214
|
out: ArtifactOut,
|
|
216
|
-
): Effect.Effect<void, Error, GitService |
|
|
215
|
+
): Effect.Effect<void, Error, GitService | Host | ConfigDiscovery | Narrator> =>
|
|
217
216
|
Effect.gen(function* () {
|
|
218
217
|
const briefing = renderBriefing()
|
|
219
218
|
out.write(briefing.endsWith("\n") ? briefing : briefing + "\n")
|
|
@@ -222,9 +221,8 @@ const runInstallCommand = (
|
|
|
222
221
|
if (topLevel._tag === "Left") return
|
|
223
222
|
if ((yield* host.realPath(topLevel.right)) !== (yield* host.realPath(host.root))) return
|
|
224
223
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
|
|
225
|
-
yield* (yield* Workspace).write(WORKFLOW_MODULE, CONFIG_SCAFFOLD)
|
|
226
224
|
yield* (yield* Narrator).warn(
|
|
227
|
-
`gtd install:
|
|
225
|
+
`gtd install: no ${WORKFLOW_MODULE} governs here, and gtd ships no workflow — get one from ${WORKFLOWS_REPO} or write your own (step 2 of the briefing above)`,
|
|
228
226
|
)
|
|
229
227
|
})
|
|
230
228
|
|
package/src/edge/Edge.ts
CHANGED
|
@@ -1476,12 +1476,12 @@ const historySteps = (run: ProcessRun): readonly HistoryStep[] => {
|
|
|
1476
1476
|
const versionWarningFor = (recorded: string | undefined): string | undefined =>
|
|
1477
1477
|
recorded === GTD_VERSION
|
|
1478
1478
|
? undefined
|
|
1479
|
-
: `gtd: warning: this history was recorded by ${recorded === undefined ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} —
|
|
1479
|
+
: `gtd: warning: this history was recorded by ${recorded === undefined ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} — the engine the workflow runs on may have changed since`
|
|
1480
1480
|
|
|
1481
1481
|
/**
|
|
1482
1482
|
* Replays the closed run at `tip` under today's config. The workflow is not
|
|
1483
|
-
* fingerprinted for a version change:
|
|
1484
|
-
*
|
|
1483
|
+
* fingerprinted for a version change: the engine under it moves with every
|
|
1484
|
+
* upgrade, and the version warning is the only signal.
|
|
1485
1485
|
*/
|
|
1486
1486
|
export const historyTrail = (tip: string): Effect.Effect<HistoryTrail, Error, RestRequirements> =>
|
|
1487
1487
|
Effect.gen(function* () {
|
package/src/flags/index.test.ts
CHANGED
|
@@ -102,16 +102,3 @@ describe("flagSettings", () => {
|
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
|
-
|
|
106
|
-
describe("risk_tier", () => {
|
|
107
|
-
it("is a release flag, off by default, resolved from GTD_FLAG_RISK_TIER", () => {
|
|
108
|
-
expect(FLAGS.find((f) => f.name === "risk_tier")).toMatchObject({ kind: "release" })
|
|
109
|
-
expect(resolveFlags({}).risk_tier).toBe(false)
|
|
110
|
-
expect(resolveFlags({ GTD_FLAG_RISK_TIER: "1" }).risk_tier).toBe(true)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it("is pinned as flag_risk_tier", () => {
|
|
114
|
-
expect(flagSettings(resolveFlags({ GTD_FLAG_RISK_TIER: "1" })).flag_risk_tier).toBe("1")
|
|
115
|
-
expect(flagSettings(resolveFlags({})).flag_risk_tier).toBe("0")
|
|
116
|
-
})
|
|
117
|
-
})
|
package/src/flags/index.ts
CHANGED
|
@@ -13,9 +13,7 @@ export interface FlagDeclaration {
|
|
|
13
13
|
readonly defaultOn?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const FLAGS = [
|
|
17
|
-
{ name: "risk_tier", owner: "pmelab", expiry: "2027-01-15", kind: "release" },
|
|
18
|
-
] as const satisfies readonly FlagDeclaration[]
|
|
16
|
+
export const FLAGS = [] as const satisfies readonly FlagDeclaration[]
|
|
19
17
|
|
|
20
18
|
/** A misspelt name is a compile error. */
|
|
21
19
|
export type Flags<R extends readonly FlagDeclaration[] = typeof FLAGS> = {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a scenario controls the commands and the history a workflow sees
|
|
3
|
+
|
|
4
|
+
A step's script may run programs the in-memory repository has none of: a
|
|
5
|
+
canned command stands in for each, matched by the command as the script
|
|
6
|
+
spells it. Branches and refs are set up before the process starts, so a
|
|
7
|
+
workflow that reads another branch or a remote ref finds them.
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given a gtd config with:
|
|
11
|
+
"""
|
|
12
|
+
import { check, human, run, showScript } from "@pmelab/gtd/flows"
|
|
13
|
+
|
|
14
|
+
export const tested = async () => {
|
|
15
|
+
const green = await check("test", "npm test", { report: ".gtd/FEEDBACK.md" })
|
|
16
|
+
await human(green ? "sign-off" : "red")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const restored = async () => {
|
|
20
|
+
await run("restore", showScript("origin/main", "a.txt", "a.txt"))
|
|
21
|
+
await human("done")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default tested
|
|
25
|
+
"""
|
|
26
|
+
And a file "a.txt" with:
|
|
27
|
+
"""
|
|
28
|
+
base
|
|
29
|
+
"""
|
|
30
|
+
And the working tree is committed as "chore: base"
|
|
31
|
+
|
|
32
|
+
Scenario: a red canned command writes its output to the check's report
|
|
33
|
+
Given the command "npm test" exits 1 with:
|
|
34
|
+
"""
|
|
35
|
+
1 failing
|
|
36
|
+
"""
|
|
37
|
+
And step "test" runs its script
|
|
38
|
+
When gtd starts workflow "tested"
|
|
39
|
+
And the process is driven
|
|
40
|
+
Then the process rests at "red"
|
|
41
|
+
And ".gtd/FEEDBACK.md" contains "1 failing"
|
|
42
|
+
|
|
43
|
+
Scenario: a green canned command passes the check
|
|
44
|
+
Given the command "npm test" exits 0
|
|
45
|
+
And step "test" runs its script
|
|
46
|
+
When gtd starts workflow "tested"
|
|
47
|
+
And the process is driven
|
|
48
|
+
Then the process rests at "sign-off"
|
|
49
|
+
|
|
50
|
+
Scenario: a command no canned command matches fails the drive, naming it
|
|
51
|
+
Given step "test" runs its script
|
|
52
|
+
When gtd starts workflow "tested"
|
|
53
|
+
Then driving the process is refused with "no canned command matches `npm test`"
|
|
54
|
+
|
|
55
|
+
Scenario: a remote ref is read by its short name
|
|
56
|
+
Given the ref "refs/remotes/origin/main" points at HEAD
|
|
57
|
+
And a file "a.txt" with:
|
|
58
|
+
"""
|
|
59
|
+
local
|
|
60
|
+
"""
|
|
61
|
+
And the working tree is committed as "chore: local"
|
|
62
|
+
And step "restore" runs its script
|
|
63
|
+
When gtd starts workflow "restored"
|
|
64
|
+
And the process is driven
|
|
65
|
+
Then the process rests at "done"
|
|
66
|
+
And "a.txt" contains "base"
|
|
67
|
+
|
|
68
|
+
Scenario: a branch is checked out and back
|
|
69
|
+
Given a branch "feature/x"
|
|
70
|
+
And "feature/x" is checked out
|
|
71
|
+
And a file "a.txt" with:
|
|
72
|
+
"""
|
|
73
|
+
feature
|
|
74
|
+
"""
|
|
75
|
+
And the working tree is committed as "feat: x"
|
|
76
|
+
And "main" is checked out
|
|
77
|
+
Then "a.txt" contains "base"
|
|
78
|
+
And "a.txt" does not contain "feature"
|
package/src/gherkin/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
runsItsScript,
|
|
18
18
|
testRepo,
|
|
19
19
|
type Actor,
|
|
20
|
+
type CannedCommand,
|
|
20
21
|
type TestRepo,
|
|
21
22
|
type WorkflowModule,
|
|
22
23
|
} from "../testkit/index.js"
|
|
@@ -31,6 +32,7 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
31
32
|
|
|
32
33
|
private gtdConfig: WorkflowModule | undefined = undefined
|
|
33
34
|
private env: Record<string, string> = {}
|
|
35
|
+
private commands: CannedCommand[] = []
|
|
34
36
|
/** Per step, what its scripted actor does, in the order the scenario declared it. */
|
|
35
37
|
actions = new Map<string, Actor[]>()
|
|
36
38
|
verdicts = new Map<string, string>()
|
|
@@ -54,6 +56,11 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
54
56
|
this.gtdConfig += `\n${tail}`
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
command(command: CannedCommand): void {
|
|
60
|
+
if (this.created === undefined) this.commands.push(command)
|
|
61
|
+
else this.created.command(command)
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
setEnv(name: string, value: string): void {
|
|
58
65
|
this.assertNotCreated(`the environment variable ${name}`)
|
|
59
66
|
this.env[name] = value
|
|
@@ -68,7 +75,11 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
68
75
|
repo(): TestRepo {
|
|
69
76
|
if (this.gtdConfig === undefined)
|
|
70
77
|
throw new Error("no gtd config: add a `gtd config` step first")
|
|
71
|
-
return (this.created ??= testRepo({
|
|
78
|
+
return (this.created ??= testRepo({
|
|
79
|
+
workflow: this.gtdConfig,
|
|
80
|
+
env: this.env,
|
|
81
|
+
commands: this.commands,
|
|
82
|
+
}))
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
act(step: string, action: Actor): void {
|
|
@@ -136,6 +147,29 @@ Given(
|
|
|
136
147
|
},
|
|
137
148
|
)
|
|
138
149
|
|
|
150
|
+
Given("the command {string} exits {int}", (world: TestkitWorld, run: string, exit: number) => {
|
|
151
|
+
world.command({ run, exit })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
Given(
|
|
155
|
+
"the command {string} exits {int} with:",
|
|
156
|
+
(world: TestkitWorld, run: string, exit: number, output: string) => {
|
|
157
|
+
world.command({ run, exit, output: withNewline(output) })
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
Given("a branch {string}", (world: TestkitWorld, name: string) => {
|
|
162
|
+
world.repo().branch(name)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
Given("{string} is checked out", (world: TestkitWorld, name: string) => {
|
|
166
|
+
world.repo().checkout(name)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
Given("the ref {string} points at HEAD", (world: TestkitWorld, name: string) => {
|
|
170
|
+
world.repo().ref(name)
|
|
171
|
+
})
|
|
172
|
+
|
|
139
173
|
Given("a file {string} with:", (world: TestkitWorld, path: string, content: string) => {
|
|
140
174
|
world.repo().write(path, withNewline(content))
|
|
141
175
|
})
|
|
@@ -9,8 +9,13 @@ import { readFileSync } from "node:fs"
|
|
|
9
9
|
import { resolve } from "node:path"
|
|
10
10
|
import { createRequire } from "node:module"
|
|
11
11
|
import { describe, expect, it } from "vitest"
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
EDIT_COMMAND,
|
|
14
|
+
DOOR_COMMAND,
|
|
15
|
+
MINIMAL_DRIVER,
|
|
16
|
+
renderBriefing,
|
|
17
|
+
WORKFLOWS_REPO,
|
|
18
|
+
} from "./Install.js"
|
|
14
19
|
|
|
15
20
|
const SUITE_PATHS = ["gtd-build", "gtd-edit", "gtd-door"]
|
|
16
21
|
const SUPERSEDED_PATHS = ["gtd-fix", "gtd-review"]
|
|
@@ -110,6 +115,13 @@ describe("renderBriefing", () => {
|
|
|
110
115
|
expect(briefing).not.toContain(".gtdrc")
|
|
111
116
|
})
|
|
112
117
|
|
|
118
|
+
it("says gtd ships no workflow, offering gtd-workflows or the user's own", () => {
|
|
119
|
+
const briefing = renderBriefing()
|
|
120
|
+
expect(briefing).toContain("gtd ships no")
|
|
121
|
+
expect(briefing).toContain(`git clone ${WORKFLOWS_REPO}`)
|
|
122
|
+
expect(briefing).toMatch(/\*\*Their own workflow\.\*\*/)
|
|
123
|
+
})
|
|
124
|
+
|
|
113
125
|
it("instructs committing the config before the first drive", () => {
|
|
114
126
|
expect(renderBriefing()).toMatch(/commit (it|the config).{0,40}before.{0,20}drive/is)
|
|
115
127
|
})
|
|
@@ -425,12 +437,3 @@ describe("DOOR_COMMAND", () => {
|
|
|
425
437
|
expect(DOOR_COMMAND).toMatch(/usage: gtd-door <name> \[args\.\.\.\]" >&2\n\s*exit 2/)
|
|
426
438
|
})
|
|
427
439
|
})
|
|
428
|
-
|
|
429
|
-
describe("bundled door lists", () => {
|
|
430
|
-
it("the scaffold's exports stay feature, review, fix, never rollout or cleanup", () => {
|
|
431
|
-
const code = CONFIG_SCAFFOLD.split("\n")
|
|
432
|
-
.filter((l) => !l.startsWith("//"))
|
|
433
|
-
.join("\n")
|
|
434
|
-
expect(code).not.toMatch(/rollout|cleanup/)
|
|
435
|
-
})
|
|
436
|
-
})
|
package/src/install/Install.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// `MINIMAL_DRIVER` is pinned equal to https://github.com/pmelab/gtd/blob/main/docs/driver.md's "A complete minimal driver" fenced bash block by `Install.test.ts`, so the two can never drift.
|
|
2
2
|
import { GTD_VERSION } from "./Version.js"
|
|
3
3
|
|
|
4
|
+
export const WORKFLOWS_REPO = "https://github.com/pmelab/gtd-workflows"
|
|
5
|
+
|
|
4
6
|
export const MINIMAL_DRIVER = `#!/usr/bin/env sh
|
|
5
7
|
set -eu
|
|
6
8
|
|
|
@@ -82,7 +84,7 @@ while :; do
|
|
|
82
84
|
system="$(gtd next --json=system)"
|
|
83
85
|
validate="$(gtd next --json=validate)"
|
|
84
86
|
log="$(gtd next --json=log)"
|
|
85
|
-
# \`model\` is a hint, not a model name —
|
|
87
|
+
# \`model\` is a hint, not a model name — gtd-workflows' default ships
|
|
86
88
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
87
89
|
# from your environment; unset, the hint is dropped and the agent runs
|
|
88
90
|
# its own default. Any other value is a real model name.
|
|
@@ -420,14 +422,37 @@ follow in the same numbered list.
|
|
|
420
422
|
existing config, the project's own README/docs) and ask the user what
|
|
421
423
|
they're trying to accomplish with gtd here — don't assume a fresh
|
|
422
424
|
install or silently start a process on their behalf.
|
|
423
|
-
2. **Make sure a \`gtd.config.ts\` governs the repository.**
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
425
|
+
2. **Make sure a \`gtd.config.ts\` governs the repository.** gtd ships no
|
|
426
|
+
workflow: the config imports the workflows it runs. Ask the user which
|
|
427
|
+
way they want:
|
|
428
|
+
- **A workflow from ${WORKFLOWS_REPO}.** Its \`default\` is a feature →
|
|
429
|
+
review → fix workflow. It is used from a checkout pinned on this
|
|
430
|
+
machine:
|
|
431
|
+
|
|
432
|
+
\`\`\`sh
|
|
433
|
+
git clone ${WORKFLOWS_REPO} ~/.local/share/gtd/workflows
|
|
434
|
+
~/.local/share/gtd/workflows/bin/gtd-wf pin origin/main # or a tag
|
|
435
|
+
~/.local/share/gtd/workflows/bin/gtd-wf config default > gtd.config.ts
|
|
436
|
+
\`\`\`
|
|
437
|
+
|
|
438
|
+
\`pin\` checks the ref out and bundles each workflow; \`config\` prints a
|
|
439
|
+
config importing the bundle by its ABSOLUTE path. Tell the user what
|
|
440
|
+
that means: the committed config works only where the checkout sits at
|
|
441
|
+
that same path, so a teammate or CI needs their own checkout there (or
|
|
442
|
+
their own config). If a checkout exists already, \`gtd-wf status\`
|
|
443
|
+
shows its pin — reuse it rather than cloning again. Read the
|
|
444
|
+
repository's README for the workflows it offers and their options.
|
|
445
|
+
- **Their own workflow.** A workflow is TypeScript the config exports,
|
|
446
|
+
written against \`@pmelab/gtd/flows\` — see
|
|
447
|
+
https://github.com/pmelab/gtd/blob/main/docs/configuration.md, and
|
|
448
|
+
https://github.com/pmelab/gtd/blob/main/docs/testing.md to test it. A
|
|
449
|
+
copy of a gtd-workflows workflow (\`gtd-wf new <name> --from default\`
|
|
450
|
+
in a clone of it) is a good start.
|
|
451
|
+
|
|
452
|
+
Repository settings — steering-file modes, \`gtd ui\`, per-scope skills
|
|
453
|
+
and access — go in the config's \`config\` export; workflow settings are
|
|
454
|
+
options of the workflow factory it exports.
|
|
455
|
+
3. **Commit the config before the first drive.** An uncommitted config is a
|
|
431
456
|
pending change, so starting a process without committing it first turns
|
|
432
457
|
the config into the very first thing the process plans against. Commit
|
|
433
458
|
it, THEN start driving.
|
|
@@ -453,7 +478,8 @@ follow in the same numbered list.
|
|
|
453
478
|
from every prompt line and writing no model exports, so every turn runs
|
|
454
479
|
on the chosen CLI's own default tier.
|
|
455
480
|
6. **If model hints are honored, resolve them to real model identifiers.**
|
|
456
|
-
|
|
481
|
+
A workflow may declare model hints — gtd-workflows' \`default\` ships
|
|
482
|
+
\`plannerModel: smart\` and \`coderModel: base\`.
|
|
457
483
|
**Those are opaque hints, not model names** — gtd never interprets them,
|
|
458
484
|
and passing \`--model smart\` to a real CLI fails on the first prompt
|
|
459
485
|
beat. Derive the chosen CLI's own available models at install time — its
|
|
@@ -550,11 +576,8 @@ hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
|
|
|
550
576
|
rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
551
577
|
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
552
578
|
\`gtd doors\` lists the doors a repository offers (those of the workflows its
|
|
553
|
-
config exports —
|
|
554
|
-
\`
|
|
555
|
-
(remove it), which a config adds by exporting \`rollout\` from
|
|
556
|
-
\`@pmelab/gtd/workflows/rollout\` and \`cleanup\` from
|
|
557
|
-
\`@pmelab/gtd/workflows/cleanup\` — plus any it declares). Refuses with
|
|
579
|
+
config exports — gtd-workflows' \`default\` brings \`fix\` and
|
|
580
|
+
\`review [base]\` — plus any the config declares). Refuses with
|
|
558
581
|
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
559
582
|
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
560
583
|
|
package/src/install/index.ts
CHANGED
|
@@ -175,7 +175,7 @@ const restName = (outcome: ReplayOutcome): string => {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Plain loops, a counter cap, and helper-driven branches: the shape the
|
|
178
|
-
//
|
|
178
|
+
// workflow might have.
|
|
179
179
|
const fixLoop = async () => {
|
|
180
180
|
await human("idle", { file: "TODO.md" })
|
|
181
181
|
for (let attempt = 1; ; attempt++) {
|
package/src/replay/index.ts
CHANGED
|
@@ -15,6 +15,6 @@ export {
|
|
|
15
15
|
type CommitSpec,
|
|
16
16
|
type JudgeVerdict,
|
|
17
17
|
} from "./Trailers.js"
|
|
18
|
-
export { accessShapeFault
|
|
18
|
+
export { accessShapeFault } from "./Access.js"
|
|
19
19
|
export { formatFault } from "./Format.js"
|
|
20
20
|
export { refNameFault } from "./RefName.js"
|
package/src/step/planStart.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ConfigDiscovery,
|
|
5
5
|
ConfigService,
|
|
6
6
|
multilineSetting,
|
|
7
|
+
ownFlagRefusal,
|
|
7
8
|
resolveStartVars,
|
|
8
9
|
StartFlags,
|
|
9
10
|
type ConfigFingerprint,
|
|
@@ -110,7 +111,11 @@ export const planStart = (
|
|
|
110
111
|
if (overrideRefusal !== undefined) return overrideRefusal
|
|
111
112
|
|
|
112
113
|
const baseOf = workflow.def.base
|
|
113
|
-
const
|
|
114
|
+
const flags = yield* StartFlags
|
|
115
|
+
const hostEnv = (yield* Host).env
|
|
116
|
+
const badFlag = ownFlagRefusal(workflow.vars, flags, hostEnv)
|
|
117
|
+
if (badFlag !== undefined) return refusal(`${commandLabel}: ${badFlag}`)
|
|
118
|
+
const vars = resolveStartVars(workflow.vars, flags, varOverrides, hostEnv)
|
|
114
119
|
const multiline = multilineSetting(vars)
|
|
115
120
|
if (multiline !== undefined) {
|
|
116
121
|
return refusal(
|