@pmelab/gtd 59.0.1 → 60.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.
Files changed (108) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +48 -32
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
  7. package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +11285 -19864
  10. package/dist/gtd.core.mjs +593 -0
  11. package/dist/steering.mjs +2 -2
  12. package/dist/testkit/gherkin.mjs +246 -37
  13. package/dist/testkit.mjs +3 -2
  14. package/dist/web/main.js +234 -232
  15. package/dist/wire.mjs +2 -2
  16. package/dist/workflow-kit.mjs +3 -3
  17. package/package.json +7 -7
  18. package/skills/authoring/SKILL.md +66 -60
  19. package/src/cli/Cli.test.ts +185 -184
  20. package/src/cli/Cli.ts +219 -224
  21. package/src/cli/History.ts +9 -4
  22. package/src/cli/Judges.ts +1 -1
  23. package/src/cli/Output.test.ts +35 -0
  24. package/src/cli/Output.ts +34 -3
  25. package/src/cli/Program.test.ts +78 -84
  26. package/src/cli/Program.ts +192 -128
  27. package/src/cli/ToolCommands.ts +2 -2
  28. package/src/edge/Edge.test.ts +72 -62
  29. package/src/edge/Edge.ts +6 -6
  30. package/src/flows/factory.test.ts +14 -0
  31. package/src/flows/factory.ts +22 -5
  32. package/src/flows/helpers.test.ts +35 -0
  33. package/src/flows/helpers.ts +19 -10
  34. package/src/flows/index.test.ts +66 -0
  35. package/src/flows/index.ts +109 -5
  36. package/src/flows/internal.ts +7 -0
  37. package/src/flows/runtime.test.ts +192 -1
  38. package/src/flows/runtime.ts +189 -55
  39. package/src/gherkin/git-control.feature +6 -6
  40. package/src/gherkin/index.test.ts +127 -0
  41. package/src/gherkin/index.ts +370 -51
  42. package/src/gherkin/messages.test.ts +38 -4
  43. package/src/gherkin/messages.ts +21 -4
  44. package/src/gherkin/more-steps.feature +170 -0
  45. package/src/gherkin/scenario-end.feature +101 -0
  46. package/src/gherkin/testkit.feature +83 -7
  47. package/src/install/Install.test.ts +7 -7
  48. package/src/install/Install.ts +8 -8
  49. package/src/install/Version.ts +9 -3
  50. package/src/install/index.ts +1 -1
  51. package/src/platform/CommandRunner.ts +13 -11
  52. package/src/platform/Commentary.ts +2 -2
  53. package/src/platform/Git.test.ts +4 -5
  54. package/src/platform/Git.ts +42 -38
  55. package/src/platform/Host.ts +1 -1
  56. package/src/platform/NodeServices.ts +11 -0
  57. package/src/platform/Workspace.test.ts +4 -5
  58. package/src/platform/Workspace.ts +3 -3
  59. package/src/platform/WorktreeState.test.ts +2 -3
  60. package/src/platform/WorktreeState.ts +1 -1
  61. package/src/platform/index.ts +2 -1
  62. package/src/replay/Replay.test.ts +13 -2
  63. package/src/replay/Replay.ts +19 -30
  64. package/src/replay/judgeDiffShape.test.ts +1 -1
  65. package/src/steering/Footnotes.test.ts +1 -1
  66. package/src/steering/SteeringFormats.test.ts +1 -1
  67. package/src/steering/freeform.test.ts +1 -1
  68. package/src/steering/index.test.ts +27 -1
  69. package/src/steering/index.ts +6 -104
  70. package/src/steering/internal.ts +11 -0
  71. package/src/steering/qa.test.ts +7 -2
  72. package/src/steering/reads.test.ts +42 -0
  73. package/src/steering/reads.ts +43 -0
  74. package/src/steering/registry.ts +93 -0
  75. package/src/steering/review.test.ts +2 -2
  76. package/src/step/planStart.test.ts +18 -16
  77. package/src/step/planStart.ts +12 -12
  78. package/src/testkit/TestRepo.test.ts +245 -7
  79. package/src/testkit/TestRepo.ts +271 -40
  80. package/src/testkit/imports.test.ts +58 -0
  81. package/src/testkit/imports.ts +102 -0
  82. package/src/testkit/index.test.ts +28 -0
  83. package/src/testkit/index.ts +14 -2
  84. package/src/twins/EmittedScriptRecognizer.ts +2 -6
  85. package/src/twins/EmittedScripts.test.ts +5 -3
  86. package/src/twins/GitTiers.ts +6 -7
  87. package/src/twins/InMemRepo.ts +1 -0
  88. package/src/twins/Layers.test.ts +1 -1
  89. package/src/twins/Layers.ts +10 -3
  90. package/src/twins/cliIo.ts +9 -4
  91. package/src/wire/Script.ts +11 -0
  92. package/src/wire/WireSchema.test.ts +3 -0
  93. package/src/wire/golden/1/script.json +4 -0
  94. package/src/wire/index.ts +1 -0
  95. package/src/workflow/SteeringFormats.test.ts +1 -1
  96. package/src/workflow/compile.ts +2 -2
  97. package/src/workflow/discovery.test.ts +27 -10
  98. package/src/workflow/discovery.ts +5 -6
  99. package/src/workflow/index.ts +1 -0
  100. package/src/workflow/load.test.ts +316 -119
  101. package/src/workflow/load.ts +478 -305
  102. package/src/workflow/pin.test.ts +159 -0
  103. package/src/workflow/pin.ts +72 -0
  104. package/src/workflow/vars.ts +3 -3
  105. package/src/workflow-kit/index.test.ts +25 -0
  106. package/src/workflow-kit/index.ts +37 -4
  107. package/src/workflow-kit/review.ts +2 -2
  108. package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
@@ -3,7 +3,13 @@ import { historyTrail } from "../edge/index.js"
3
3
  import { combinedScript, fetchHistoryRefs } from "../emit/index.js"
4
4
  import { GitService, Narrator, PROCESS_HISTORY_PREFIX } from "../platform/index.js"
5
5
  import { parseCommitMessage } from "../replay/index.js"
6
- import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
6
+ import {
7
+ writeJson,
8
+ writeScript,
9
+ type ArtifactOut,
10
+ type CommandRequirements,
11
+ type JsonMode,
12
+ } from "./Output.js"
7
13
 
8
14
  export interface HistoryEntry {
9
15
  readonly ref: string
@@ -138,8 +144,7 @@ export const runHistoryShowCommand = (
138
144
  /** Prints the script and never runs git: gtd's promise that it only decides and prints. */
139
145
  export const runHistoryFetchCommand = (
140
146
  remote: string,
147
+ json: JsonMode,
141
148
  out: ArtifactOut,
142
149
  ): Effect.Effect<void, Error, CommandRequirements> =>
143
- Effect.sync(() => {
144
- out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`)
145
- })
150
+ writeScript(json, out, `${combinedScript(fetchHistoryRefs(remote), "")}\n`)
package/src/cli/Judges.ts CHANGED
@@ -2,7 +2,7 @@ import { Effect, Option } from "effect"
2
2
  import { answered, numeric } from "../flows/index.js"
3
3
  import { GitService } from "../platform/index.js"
4
4
  import { parseCommitMessage, type JudgeVerdict } from "../replay/index.js"
5
- import { headingSectionBodies } from "../steering/index.js"
5
+ import { headingSectionBodies } from "../steering/internal.js"
6
6
  import { selectHistoryEntries, type HistoryEntry } from "./History.js"
7
7
  import { printable } from "./Metrics.js"
8
8
  import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
@@ -0,0 +1,35 @@
1
+ import { Effect, Exit } from "effect"
2
+ import { describe, expect, it } from "vitest"
3
+ import { WIRE_SCHEMA } from "../wire/index.js"
4
+ import { writeScript, type JsonMode } from "./Output.js"
5
+
6
+ const run = (json: JsonMode, script: string, plain?: string) => {
7
+ let written = ""
8
+ const out = { write: (chunk: string | Uint8Array) => (written += String(chunk)), flush: () => {} }
9
+ const exit = Effect.runSyncExit(writeScript(json, out, script, plain))
10
+ return { exit, written }
11
+ }
12
+
13
+ describe("writeScript", () => {
14
+ it("plain prints the script, or the plain text when given", () => {
15
+ expect(run({ kind: "off" }, "git reset\n").written).toBe("git reset\n")
16
+ expect(run({ kind: "off" }, "", "nothing to do\n").written).toBe("nothing to do\n")
17
+ })
18
+
19
+ it("--json prints { schema, script }, the script ending in exactly one newline", () => {
20
+ expect(JSON.parse(run({ kind: "document" }, "git reset\n\n").written)).toEqual({
21
+ schema: WIRE_SCHEMA,
22
+ script: "git reset\n",
23
+ })
24
+ expect(JSON.parse(run({ kind: "document" }, "", "nothing\n").written)).toEqual({
25
+ schema: WIRE_SCHEMA,
26
+ script: "",
27
+ })
28
+ })
29
+
30
+ it("--json=script prints the script alone; an unknown path fails", () => {
31
+ expect(run({ kind: "select", path: "script" }, "git reset").written).toBe("git reset\n\n")
32
+ expect(run({ kind: "select", path: "schema" }, "x").written).toBe(`${WIRE_SCHEMA}\n`)
33
+ expect(Exit.isFailure(run({ kind: "select", path: "nope" }, "x").exit)).toBe(true)
34
+ })
35
+ })
package/src/cli/Output.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { Effect } from "effect"
2
2
  import { CommandRunner, GitService, Host, Narrator, Workspace } from "../platform/index.js"
3
- import { selectPath, type EXIT_OK, type EXIT_STRAYS_FOUND } from "../wire/index.js"
4
- import { ConfigDiscovery, ConfigService } from "../workflow/index.js"
3
+ import {
4
+ renderScriptJson,
5
+ scriptFields,
6
+ selectPath,
7
+ type EXIT_OK,
8
+ type EXIT_STRAYS_FOUND,
9
+ } from "../wire/index.js"
10
+ import { ConfigDiscovery, ConfigService, EngineCore } from "../workflow/index.js"
5
11
  import type { ToolCommands } from "./ToolCommands.js"
6
12
 
7
13
  /**
@@ -53,7 +59,7 @@ export type JsonMode =
53
59
  * reach stdout.
54
60
  */
55
61
  export interface ArtifactOut {
56
- readonly write: (chunk: string) => void
62
+ readonly write: (chunk: string | Uint8Array) => void
57
63
  readonly flush: () => void
58
64
  }
59
65
 
@@ -61,6 +67,7 @@ export type CommandRequirements =
61
67
  | GitService
62
68
  | ConfigService
63
69
  | ConfigDiscovery
70
+ | EngineCore
64
71
  | Workspace
65
72
  | Host
66
73
  | Narrator
@@ -81,3 +88,27 @@ export const writeJson = (
81
88
  else yield* writeSelection(out, document, json.path)
82
89
  return true
83
90
  })
91
+
92
+ /** Exactly one trailing newline: a script embedded in a `--json` document is never re-normalized at its tail. */
93
+ export const normalizeScriptNewline = (script: string): string =>
94
+ script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`
95
+
96
+ /**
97
+ * A script-printing command's output: the script itself (or `plain`, when
98
+ * there is none to run), `{ schema, script }` under `--json`, or one field.
99
+ */
100
+ export const writeScript = (
101
+ json: JsonMode,
102
+ out: ArtifactOut,
103
+ script: string,
104
+ plain: string = script,
105
+ ): Effect.Effect<void, Error> =>
106
+ Effect.gen(function* () {
107
+ if (json.kind === "off") {
108
+ out.write(plain)
109
+ return
110
+ }
111
+ const fields = scriptFields(normalizeScriptNewline(script))
112
+ if (json.kind === "document") out.write(renderScriptJson(fields))
113
+ else yield* writeSelection(out, fields, json.path)
114
+ })
@@ -81,25 +81,26 @@ const seedIdle = (): InMemRepo => {
81
81
  return repo
82
82
  }
83
83
 
84
- describe("gtd --workflow <name> — a custom workflow's named workflow", () => {
85
- const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
84
+ describe("gtd start <name> — a custom workflow's named workflow", () => {
85
+ const WORKFLOW = `import { agent, human, workflow } from "@pmelab/gtd/flows"
86
86
 
87
87
  export default async () => {
88
88
  await human("idle", { message: "hi" })
89
89
  await agent("working", "go")
90
90
  }
91
91
 
92
- export const hotfix = async () => {
93
- await agent("working", "go")
94
- }
95
-
96
- export const defaults = { greeting: "hello" }
92
+ export const hotfix = workflow(
93
+ async () => {
94
+ await agent("working", "go")
95
+ },
96
+ { defaults: { greeting: "hello" } },
97
+ )
97
98
  `
98
99
 
99
100
  it("the happy path emits the opening commit's script — gtd itself writes nothing", async () => {
100
101
  const repo = seed(WORKFLOW)
101
102
  const before = repo.commitHistory().length
102
- const { stdout, exitCode } = await run(repo, "--workflow", "hotfix")
103
+ const { stdout, exitCode } = await run(repo, "start", "hotfix")
103
104
  expect(exitCode).toBe(0)
104
105
  expect(stdout).toContain(
105
106
  commitAll(
@@ -109,21 +110,20 @@ export const defaults = { greeting: "hello" }
109
110
  expect(repo.commitHistory()).toHaveLength(before)
110
111
  })
111
112
 
112
- it("an unknown workflow is a usage error (exit 2) listing every startable name", async () => {
113
+ it("an unknown name is a usage error (exit 2) pointing at the list", async () => {
113
114
  const repo = seed(WORKFLOW)
114
115
  const before = repo.commitHistory().length
115
- const { exitCode, stderr } = await run(repo, "--workflow", "bogus")
116
+ const { exitCode, stderr } = await run(repo, "start", "bogus")
116
117
  expect(exitCode).toBe(2)
117
118
  expect(repo.commitHistory()).toHaveLength(before)
118
- expect(stderr).toContain('unknown workflow "bogus"')
119
- expect(stderr).toContain("startable: hotfix")
119
+ expect(stderr).toContain('no workflow or door is named "bogus" — run `gtd start` to list them')
120
120
  })
121
121
 
122
122
  it("lists the paths its script commits as comments a path cannot break out of", async () => {
123
123
  const repo = seed(WORKFLOW)
124
124
  repo.writeFile("NOTE.md", "a note\n")
125
125
  repo.writeFile("evil\nrm -rf x", "")
126
- const { stdout, exitCode } = await run(repo, "--workflow", "hotfix")
126
+ const { stdout, exitCode } = await run(repo, "start", "hotfix")
127
127
  expect(exitCode).toBe(0)
128
128
  expect(stdout).toContain("# commits every change in the working tree (git add -A):\n")
129
129
  expect(stdout).toContain("# A NOTE.md\n")
@@ -134,9 +134,9 @@ export const defaults = { greeting: "hello" }
134
134
 
135
135
  it("`default` is not startable by name", async () => {
136
136
  const repo = seed(WORKFLOW)
137
- const { exitCode, stderr } = await run(repo, "--workflow", "default")
137
+ const { exitCode, stderr } = await run(repo, "start", "default")
138
138
  expect(exitCode).toBe(2)
139
- expect(stderr).toContain('unknown workflow "default"')
139
+ expect(stderr).toContain('no workflow or door is named "default"')
140
140
  })
141
141
 
142
142
  it("--entry is an ordinary unknown flag", async () => {
@@ -149,17 +149,18 @@ export const defaults = { greeting: "hello" }
149
149
  const repo = seed(WORKFLOW)
150
150
  await landTurn(repo, { ".gtd/TODO.md": "sketch\n" })
151
151
  const before = repo.commitHistory().length
152
- const { exitCode, stderr } = await run(repo, "--workflow", "hotfix")
152
+ const { exitCode, stderr } = await run(repo, "start", "hotfix")
153
153
  expect(exitCode).toBe(1)
154
154
  expect(repo.commitHistory()).toHaveLength(before)
155
155
  expect(stderr).toContain("already underway")
156
156
  })
157
157
 
158
- it("an undeclared --var name refuses, listing the declared names", async () => {
158
+ it("an undeclared --var name is a usage error (exit 2), listing the declared names", async () => {
159
159
  const repo = seed(WORKFLOW)
160
160
  const before = repo.commitHistory().length
161
- const { exitCode, stderr } = await run(repo, "--workflow", "hotfix", "--var", "bogus=1")
162
- expect(exitCode).toBe(1)
161
+ const { exitCode, stderr, stdout } = await run(repo, "start", "hotfix", "--var", "bogus=1")
162
+ expect(exitCode).toBe(2)
163
+ expect(stdout).toBe("")
163
164
  expect(repo.commitHistory()).toHaveLength(before)
164
165
  expect(stderr).toContain("bogus")
165
166
  expect(stderr).toContain("greeting")
@@ -168,7 +169,7 @@ export const defaults = { greeting: "hello" }
168
169
  it("a declared --var override renders as a Gtd-Var trailer in the emitted commit script", async () => {
169
170
  const repo = seed(WORKFLOW)
170
171
  const before = repo.commitHistory().length
171
- const { exitCode, stdout } = await run(repo, "--workflow", "hotfix", "--var", "greeting=world")
172
+ const { exitCode, stdout } = await run(repo, "start", "hotfix", "--var", "greeting=world")
172
173
  expect(exitCode).toBe(0)
173
174
  expect(stdout).toContain("Gtd-Var: greeting=world")
174
175
  expect(repo.commitHistory()).toHaveLength(before)
@@ -178,7 +179,7 @@ export const defaults = { greeting: "hello" }
178
179
  const repo = seed(WORKFLOW)
179
180
  repo.writeFile("scratch.txt", "uncommitted\n")
180
181
  const before = repo.commitHistory().length
181
- const { exitCode, stdout } = await run(repo, "--workflow", "hotfix")
182
+ const { exitCode, stdout } = await run(repo, "start", "hotfix")
182
183
  expect(exitCode).toBe(0)
183
184
  expect(stdout.length).toBeGreaterThan(0)
184
185
  expect(repo.commitHistory()).toHaveLength(before)
@@ -186,7 +187,7 @@ export const defaults = { greeting: "hello" }
186
187
 
187
188
  it("applying the start script opens the workflow's episode at its first step", async () => {
188
189
  const repo = seed(WORKFLOW)
189
- const { stdout } = await run(repo, "--workflow", "hotfix")
190
+ const { stdout } = await run(repo, "start", "hotfix")
190
191
  expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
191
192
  expect(repo.lastCommitSubject()).toBe("gtd(human): working")
192
193
  const next = JSON.parse((await run(repo, "next", "--json")).stdout) as Record<string, unknown>
@@ -197,7 +198,7 @@ export const defaults = { greeting: "hello" }
197
198
 
198
199
  it("a pinned workflow the config no longer defines is refused with the abandon advice", async () => {
199
200
  const repo = seed(WORKFLOW)
200
- const { stdout } = await run(repo, "--workflow", "hotfix")
201
+ const { stdout } = await run(repo, "start", "hotfix")
201
202
  applyEmittedScript(repo, new Map(), stdout)
202
203
  repo.writeFile("gtd.config.ts", WORKFLOW.replace("export const hotfix", "const hotfix"))
203
204
  const { exitCode, stderr } = await run(repo, "next")
@@ -207,18 +208,16 @@ export const defaults = { greeting: "hello" }
207
208
  })
208
209
  })
209
210
 
210
- describe("gtd install", () => {
211
+ describe("gtd guide", () => {
211
212
  it("where no gtd.config.ts governs, prints the briefing and warns where to get a workflow — writing nothing", async () => {
212
213
  const repo = new InMemRepo()
213
214
  repo.writeFile("README.md", "# test project\n")
214
215
  repo.seedCommit("chore: init")
215
216
  const before = repo.snapshot()
216
- const { stdout, stderr, exitCode } = await run(repo, "install")
217
+ const { stdout, stderr, exitCode } = await run(repo, "guide")
217
218
  expect(exitCode).toBe(0)
218
219
  expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
219
- expect(stderr).toContain(
220
- "gtd install: no gtd.config.ts governs here, and gtd ships no workflow",
221
- )
220
+ expect(stderr).toContain("gtd guide: no gtd.config.ts governs here, and gtd ships no workflow")
222
221
  expect(stderr).toContain(WORKFLOWS_REPO)
223
222
  expect(repo.hasPath("gtd.config.ts")).toBe(false)
224
223
  expect(repo.snapshot()).toBe(before)
@@ -226,42 +225,35 @@ describe("gtd install", () => {
226
225
 
227
226
  it("where a gtd.config.ts governs, prints the briefing alone", async () => {
228
227
  const repo = seedIdle()
229
- const { stdout, stderr, exitCode } = await run(repo, "install")
228
+ const { stdout, stderr, exitCode } = await run(repo, "guide")
230
229
  expect(exitCode).toBe(0)
231
230
  expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
232
231
  expect(stderr).not.toContain(WORKFLOWS_REPO)
233
232
  })
234
233
  })
235
234
 
236
- describe("gtd --workflow <name> — a workflow that fixes its diff base", () => {
237
- const WORKFLOW = `import { human } from "@pmelab/gtd/flows"
235
+ describe("gtd start <name> — a workflow that fixes its diff base", () => {
236
+ const WORKFLOW = `import { human, workflow } from "@pmelab/gtd/flows"
238
237
 
239
238
  export default async () => {
240
239
  await human("idle", { message: "hi" })
241
240
  }
242
241
 
243
- export const review = async () => {
244
- await human("checking", { message: "check" })
245
- }
246
-
247
- export const defaults = { reviewBase: "" }
248
-
249
- export const base = (workflow, vars) => (workflow === "review" ? vars.reviewBase : undefined)
242
+ export const review = workflow(
243
+ async () => {
244
+ await human("checking", { message: "check" })
245
+ },
246
+ { defaults: { reviewBase: "" }, base: (_workflow, vars) => vars.reviewBase },
247
+ )
250
248
  `
251
249
 
252
- it("--workflow review --var reviewBase=<base> emits a script anchoring the process to that base", async () => {
250
+ it("start review --var reviewBase=<base> emits a script anchoring the process to that base", async () => {
253
251
  const repo = seed(WORKFLOW)
254
252
  const base = repo.commitHistory().at(-1)!.hash
255
253
  repo.writeFile("scratch.txt", "more work\n")
256
254
  repo.seedCommit("chore: more work")
257
255
  const before = repo.commitHistory().length
258
- const { stdout, exitCode } = await run(
259
- repo,
260
- "--workflow",
261
- "review",
262
- "--var",
263
- `reviewBase=${base}`,
264
- )
256
+ const { stdout, exitCode } = await run(repo, "start", "review", "--var", `reviewBase=${base}`)
265
257
  expect(exitCode).toBe(0)
266
258
  expect(stdout).toContain("gtd(human): checking")
267
259
  expect(stdout).toContain("Gtd-Workflow: review")
@@ -453,7 +445,7 @@ export default async () => {
453
445
  })
454
446
 
455
447
  describe("gtd next --json — stall detection (attempt commits)", () => {
456
- const WORKFLOW = `import { agent, human, run } from "@pmelab/gtd/flows"
448
+ const WORKFLOW = `import { agent, human, run, workflow } from "@pmelab/gtd/flows"
457
449
 
458
450
  const work = async () => {
459
451
  await agent("working", "do the work described in NOTE.md")
@@ -465,7 +457,7 @@ export default async () => {
465
457
  await work()
466
458
  }
467
459
 
468
- export const resume = work
460
+ export const resume = workflow(work)
469
461
  `
470
462
 
471
463
  const atWorking = async (): Promise<InMemRepo> => {
@@ -502,7 +494,7 @@ export const resume = work
502
494
 
503
495
  it("a clean-tree entry commit resting at a prompt step is NOT a stall — the actor differs (human entered, agent acts)", async () => {
504
496
  const repo = seed(WORKFLOW)
505
- const { stdout: entry } = await run(repo, "--workflow", "resume")
497
+ const { stdout: entry } = await run(repo, "start", "resume")
506
498
  expect(applyEmittedScript(repo, new Map(), entry).ok).toBe(true)
507
499
  const { stdout, exitCode } = await run(repo, "next", "--json")
508
500
  expect(exitCode).toBe(0)
@@ -1001,18 +993,19 @@ describe("gtd check <mode> <file>", () => {
1001
993
  )
1002
994
  })
1003
995
 
1004
- it("an absent file exits 0 with no output", async () => {
996
+ it("an absent file is a refusal (exit 1) naming it", async () => {
1005
997
  const repo = bareRepo()
1006
- const { stdout, exitCode } = await run(repo, "check", "qa", ".gtd/TODO.md")
1007
- expect(exitCode).toBe(0)
998
+ const { stdout, stderr, exitCode } = await run(repo, "check", "qa", ".gtd/TODO.md")
999
+ expect(exitCode).toBe(1)
1008
1000
  expect(stdout).toBe("")
1001
+ expect(stderr).toContain("gtd check: no such file: .gtd/TODO.md")
1009
1002
  })
1010
1003
 
1011
- it("an unknown mode is a usage-style error naming the known modes", async () => {
1004
+ it("an unknown mode is a usage error (exit 2) naming the known modes", async () => {
1012
1005
  const repo = bareRepo()
1013
1006
  repo.writeFile("TODO.md", validQaDoc)
1014
1007
  const { stderr, exitCode } = await run(repo, "check", "bogus", "TODO.md")
1015
- expect(exitCode).toBe(1)
1008
+ expect(exitCode).toBe(2)
1016
1009
  expect(stderr).toContain('unknown mode "bogus"')
1017
1010
  expect(stderr).toContain("qa")
1018
1011
  expect(stderr).toContain("review")
@@ -1244,13 +1237,6 @@ describe("gtd check <mode> <file> --open-questions", () => {
1244
1237
  expect(stderr).toContain(".gtd/TODO.md")
1245
1238
  })
1246
1239
 
1247
- it("check without the flag is unchanged: an absent file still exits 0 with no output", async () => {
1248
- const repo = bareRepo()
1249
- const { stdout, exitCode } = await run(repo, "check", "qa", ".gtd/TODO.md")
1250
- expect(exitCode).toBe(0)
1251
- expect(stdout).toBe("")
1252
- })
1253
-
1254
1240
  it("--json is out of scope for check --open-questions too — a usage error, never a JSON envelope", async () => {
1255
1241
  const repo = bareRepo()
1256
1242
  repo.writeFile(".gtd/TODO.md", docWithUnanswered)
@@ -1277,7 +1263,7 @@ describe("gtd check <mode> <file> --open-questions", () => {
1277
1263
  "--open-questions",
1278
1264
  ".gtd/TODO.md",
1279
1265
  )
1280
- expect(exitCode).not.toBe(0)
1266
+ expect(exitCode).toBe(2)
1281
1267
  expect(stderr).toContain('unknown mode "bogus"')
1282
1268
  expect(stderr).toContain("qa")
1283
1269
  expect(stderr).toContain("review")
@@ -1718,16 +1704,15 @@ describe("runCommand — refuses in a repository with no commits", () => {
1718
1704
  lsp: { kind: "lsp" },
1719
1705
  ui: { kind: "ui", selfSigned: false, dev: false },
1720
1706
  land: { kind: "land" },
1721
- start: { kind: "start", actor: "human", workflow: "feature", vars: {}, label: "" },
1722
- door: { kind: "door", name: "fix", args: [] },
1723
- doors: { kind: "doors" },
1707
+ start: { kind: "start", name: "fix", args: [], vars: {} },
1724
1708
  abandon: { kind: "abandon" },
1725
1709
  restore: { kind: "restore" },
1726
1710
  next: { kind: "next" },
1727
1711
  validate: { kind: "validate" },
1728
1712
  check: { kind: "check", mode: "qa", file: ".gtd/TODO.md" },
1729
1713
  uncheck: { kind: "uncheck", file: ".gtd/REVIEW.md" },
1730
- install: { kind: "install" },
1714
+ guide: { kind: "guide" },
1715
+ bundle: { kind: "bundle" },
1731
1716
  summary: { kind: "summary" },
1732
1717
  base: { kind: "base" },
1733
1718
  judge: { kind: "judge" },
@@ -1748,13 +1733,12 @@ describe("runCommand — refuses in a repository with no commits", () => {
1748
1733
  const NO_COMMITS_MESSAGE =
1749
1734
  "gtd: the repository has no commits yet — make an initial commit, then run gtd again"
1750
1735
 
1751
- it("derives exactly the eighteen non-standalone kinds — a canary for the table-driven cases below", () => {
1736
+ it("derives exactly the seventeen non-standalone kinds — a canary for the table-driven cases below", () => {
1752
1737
  expect(stateKinds.sort()).toEqual(
1753
1738
  [
1754
1739
  "abandon",
1755
1740
  "base",
1756
- "door",
1757
- "doors",
1741
+ "bundle",
1758
1742
  "start",
1759
1743
  "land",
1760
1744
  "next",
@@ -1779,7 +1763,10 @@ describe("runCommand — refuses in a repository with no commits", () => {
1779
1763
  const repo = new InMemRepo()
1780
1764
  const before = repo.commitHistory().length
1781
1765
  const written: string[] = []
1782
- const out = { write: (chunk: string) => written.push(chunk), flush: () => {} }
1766
+ const out = {
1767
+ write: (chunk: string | Uint8Array) => written.push(String(chunk)),
1768
+ flush: () => {},
1769
+ }
1783
1770
 
1784
1771
  const exit = await Effect.runPromiseExit(
1785
1772
  runCommand(commandFor[kind], { kind: "off" }, out).pipe(Effect.provide(testLayers(repo))),
@@ -1798,7 +1785,10 @@ describe("runCommand — refuses in a repository with no commits", () => {
1798
1785
  it("a repository with a commit passes the guard and dispatches normally", async () => {
1799
1786
  const repo = seedIdle()
1800
1787
  const written: string[] = []
1801
- const out = { write: (chunk: string) => written.push(chunk), flush: () => {} }
1788
+ const out = {
1789
+ write: (chunk: string | Uint8Array) => written.push(String(chunk)),
1790
+ flush: () => {},
1791
+ }
1802
1792
 
1803
1793
  const exit = await Effect.runPromiseExit(
1804
1794
  runCommand({ kind: "next" }, { kind: "off" }, out).pipe(Effect.provide(testLayers(repo))),
@@ -1830,16 +1820,19 @@ describe("runCommand — refuses in a repository with no commits", () => {
1830
1820
  })
1831
1821
 
1832
1822
  describe("gtd summary", () => {
1833
- const SUMMARY_WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
1823
+ const SUMMARY_WORKFLOW = `import { agent, human, workflow } from "@pmelab/gtd/flows"
1834
1824
 
1835
- export default async () => {
1825
+ export default workflow(
1826
+ async () => {
1836
1827
  await human("idle", { message: "hi" })
1837
1828
  await agent("working", "go")
1838
1829
  await human("reviewing", { message: "check it" })
1839
1830
  await agent("polishing", "polish")
1840
- }
1841
-
1842
- export const summary = (c) => \`entry=\${c.entryCommit} tip=\${c.processTip} humans=\${c.humanCommits.length}\`
1831
+ },
1832
+ {
1833
+ summary: (c) => \`entry=\${c.entryCommit} tip=\${c.processTip} humans=\${c.humanCommits.length}\`,
1834
+ },
1835
+ )
1843
1836
  `
1844
1837
 
1845
1838
  const reviewed = async (): Promise<InMemRepo> => {
@@ -2105,7 +2098,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
2105
2098
  )
2106
2099
  expect(Exit.isFailure(exit)).toBe(true)
2107
2100
  if (Exit.isFailure(exit)) {
2108
- const failure = Cause.failureOption(exit.cause)
2101
+ const failure = Cause.findErrorOption(exit.cause)
2109
2102
  expect(failure._tag).toBe("Some")
2110
2103
  if (failure._tag === "Some") {
2111
2104
  expect(failure.value).toBeInstanceOf(SelectorUsageError)
@@ -2131,14 +2124,15 @@ describe("gtd judge / gtd judge answer", () => {
2131
2124
  const QUESTION = `{ id: "q1", primitive: "noul", instructions: "i", criteria: "c" }`
2132
2125
 
2133
2126
  const judgeWorkflow = (evidence: string, then: string, vars = "{}"): string =>
2134
- `import { agent, human, judge, read } from "@pmelab/gtd/flows"
2127
+ `import { agent, human, judge, read, workflow } from "@pmelab/gtd/flows"
2135
2128
 
2136
- export default async () => {
2129
+ export default workflow(
2130
+ async () => {
2137
2131
  await judge("idle", { questions: [${QUESTION}], evidence: ${evidence} })
2138
2132
  await ${then}
2139
- }
2140
-
2141
- export const defaults = ${vars}
2133
+ },
2134
+ { defaults: ${vars} },
2135
+ )
2142
2136
  `
2143
2137
 
2144
2138
  const seededRepo = (): InMemRepo =>
@@ -2311,8 +2305,8 @@ export const defaults = ${vars}
2311
2305
  describe("runCommand — the repository-root guard's messages", () => {
2312
2306
  const guarded = async (
2313
2307
  override: (
2314
- git: Context.Tag.Service<GitService>,
2315
- host: Context.Tag.Service<Host>,
2308
+ git: Context.Service.Shape<typeof GitService>,
2309
+ host: Context.Service.Shape<typeof Host>,
2316
2310
  ) => Effect.Effect<unknown, Error, CommandRequirements>,
2317
2311
  ): Promise<string> => {
2318
2312
  const exit = await Effect.runPromiseExit(