@pmelab/gtd 59.0.0 → 60.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.
Files changed (90) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +42 -38
  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-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
  7. package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +1101 -1025
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +371 -82
  12. package/dist/testkit.mjs +3 -2
  13. package/dist/web/main.js +234 -232
  14. package/dist/wire.mjs +2 -2
  15. package/dist/workflow-kit.mjs +3 -3
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +68 -61
  18. package/src/cli/Cli.test.ts +311 -229
  19. package/src/cli/Cli.ts +486 -585
  20. package/src/cli/Help.test.ts +47 -0
  21. package/src/cli/Help.ts +70 -0
  22. package/src/cli/History.ts +9 -4
  23. package/src/cli/Judges.ts +1 -1
  24. package/src/cli/Output.test.ts +35 -0
  25. package/src/cli/Output.ts +31 -1
  26. package/src/cli/Program.test.ts +131 -82
  27. package/src/cli/Program.ts +197 -111
  28. package/src/edge/Edge.test.ts +64 -56
  29. package/src/edge/Edge.ts +4 -4
  30. package/src/emit/Emit.test.ts +1 -1
  31. package/src/emit/Emit.ts +1 -1
  32. package/src/flows/factory.test.ts +14 -0
  33. package/src/flows/factory.ts +25 -6
  34. package/src/flows/helpers.test.ts +35 -0
  35. package/src/flows/helpers.ts +19 -10
  36. package/src/flows/index.test.ts +66 -0
  37. package/src/flows/index.ts +109 -5
  38. package/src/flows/internal.ts +7 -0
  39. package/src/flows/runtime.test.ts +192 -1
  40. package/src/flows/runtime.ts +188 -54
  41. package/src/gherkin/git-control.feature +6 -6
  42. package/src/gherkin/index.test.ts +127 -0
  43. package/src/gherkin/index.ts +481 -98
  44. package/src/gherkin/messages.test.ts +124 -0
  45. package/src/gherkin/messages.ts +101 -0
  46. package/src/gherkin/more-steps.feature +170 -0
  47. package/src/gherkin/scenario-end.feature +101 -0
  48. package/src/gherkin/testkit.feature +83 -7
  49. package/src/install/Install.test.ts +7 -7
  50. package/src/install/Install.ts +8 -8
  51. package/src/replay/Replay.test.ts +13 -2
  52. package/src/replay/Replay.ts +19 -30
  53. package/src/replay/judgeDiffShape.test.ts +1 -1
  54. package/src/steering/Footnotes.test.ts +1 -1
  55. package/src/steering/SteeringFormats.test.ts +1 -1
  56. package/src/steering/freeform.test.ts +1 -1
  57. package/src/steering/index.test.ts +27 -1
  58. package/src/steering/index.ts +6 -104
  59. package/src/steering/internal.ts +11 -0
  60. package/src/steering/qa.test.ts +7 -2
  61. package/src/steering/reads.test.ts +42 -0
  62. package/src/steering/reads.ts +43 -0
  63. package/src/steering/registry.ts +93 -0
  64. package/src/steering/review.test.ts +2 -2
  65. package/src/step/planStart.test.ts +18 -16
  66. package/src/step/planStart.ts +9 -9
  67. package/src/testkit/TestRepo.test.ts +289 -6
  68. package/src/testkit/TestRepo.ts +317 -41
  69. package/src/testkit/imports.test.ts +58 -0
  70. package/src/testkit/imports.ts +102 -0
  71. package/src/testkit/index.test.ts +28 -0
  72. package/src/testkit/index.ts +14 -2
  73. package/src/twins/EmittedScriptRecognizer.ts +7 -14
  74. package/src/twins/EmittedScripts.test.ts +4 -2
  75. package/src/twins/InMemRepo.ts +1 -0
  76. package/src/wire/Land.ts +1 -1
  77. package/src/wire/Script.ts +11 -0
  78. package/src/wire/WireSchema.test.ts +3 -0
  79. package/src/wire/golden/1/script.json +4 -0
  80. package/src/wire/index.ts +1 -0
  81. package/src/workflow/SteeringFormats.test.ts +1 -1
  82. package/src/workflow/compile.ts +2 -2
  83. package/src/workflow/discovery.test.ts +3 -3
  84. package/src/workflow/load.test.ts +256 -116
  85. package/src/workflow/load.ts +175 -152
  86. package/src/workflow/skills.test.ts +5 -5
  87. package/src/workflow/skills.ts +1 -1
  88. package/src/workflow-kit/index.test.ts +25 -0
  89. package/src/workflow-kit/index.ts +37 -4
  90. package/src/workflow-kit/review.ts +2 -2
@@ -3,25 +3,28 @@ import { describe, expect, it } from "vitest"
3
3
  import { planStart, type StartOutcome } from "./planStart.js"
4
4
  import { InMemRepo, testLayers } from "../twins/index.js"
5
5
  import { ConfigService } from "../workflow/index.js"
6
+ import { GtdUsageError } from "../platform/index.js"
6
7
 
7
- const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
8
+ const WORKFLOW = `import { agent, human, workflow } from "@pmelab/gtd/flows"
8
9
 
9
10
  export default async () => {
10
11
  await human("idle", { message: "hi" })
11
12
  await agent("work", "work-prompt")
12
13
  }
13
14
 
14
- export const working = async () => {
15
- await agent("work", "work-prompt")
16
- }
17
-
18
- export const reviewcheck = async () => {
19
- await agent("work", "work-prompt")
20
- }
21
-
22
- export const defaults = { base: "" }
15
+ export const working = workflow(
16
+ async () => {
17
+ await agent("work", "work-prompt")
18
+ },
19
+ { defaults: { base: "" } },
20
+ )
23
21
 
24
- export const base = (workflow, vars) => (workflow === "reviewcheck" ? (vars.base ?? "") : undefined)
22
+ export const reviewcheck = workflow(
23
+ async () => {
24
+ await agent("work", "work-prompt")
25
+ },
26
+ { defaults: { base: "" }, base: (_workflow, vars) => vars.base ?? "" },
27
+ )
25
28
  `
26
29
 
27
30
  const repoAt = (): InMemRepo => {
@@ -85,15 +88,14 @@ describe("planStart", () => {
85
88
  })
86
89
  })
87
90
 
88
- it("refuses an undeclared --var name", async () => {
91
+ it("fails an undeclared --var name as a usage error", async () => {
89
92
  const repo = repoAt()
90
- const plan = await start(repo, "idle", "human", {
93
+ const failure = await start(repo, "idle", "human", {
91
94
  workflow: "working",
92
95
  commandLabel: "gtd test",
93
96
  vars: { nope: "x" },
94
- })
95
- expect(plan.kind).toBe("refusal")
96
- expect(plan.kind === "refusal" && plan.message).toContain("not declared by this workflow")
97
+ }).catch((e: unknown) => e)
98
+ expect(String(failure)).toContain(`${GtdUsageError.name}: gtd test: --var name(s) not declared`)
97
99
  })
98
100
 
99
101
  describe("a workflow's diff base", () => {
@@ -40,7 +40,7 @@ const checkOverrides = (
40
40
  varOverrides: Record<string, string>,
41
41
  declaredNames: readonly string[],
42
42
  envNames: readonly string[],
43
- ): Effect.Effect<StartOutcome | undefined, Error> =>
43
+ ): Effect.Effect<void, GtdUsageError> =>
44
44
  Effect.gen(function* () {
45
45
  const environmentOnly = Object.keys(varOverrides).filter(
46
46
  (n) => !declaredNames.includes(n) && envNames.includes(n),
@@ -56,17 +56,18 @@ const checkOverrides = (
56
56
  }
57
57
  const undeclared = Object.keys(varOverrides).filter((n) => !declaredNames.includes(n))
58
58
  if (undeclared.length > 0) {
59
- return refusal(
60
- `${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(
61
- ", ",
62
- )} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`,
59
+ return yield* Effect.fail(
60
+ new GtdUsageError(
61
+ `${commandLabel}: --var name(s) not declared by this workflow: ${undeclared.join(
62
+ ", ",
63
+ )} — declared: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}`,
64
+ ),
63
65
  )
64
66
  }
65
- return undefined
66
67
  })
67
68
 
68
69
  /**
69
- * `gtd --workflow <name>`: start a brand NEW process on a workflow, writing an
70
+ * `gtd start <name>`: start a brand NEW process on a workflow, writing an
70
71
  * opening commit that names the workflow's first step and carries a
71
72
  * `Gtd-Workflow:` trailer, every process setting as a `Gtd-Var:` trailer, plus
72
73
  * a `Gtd-Review-Base:` trailer when the workflow fixes the process's diff base,
@@ -102,13 +103,12 @@ export const planStart = (
102
103
  if ("refusal" in first) return refusal(`${commandLabel}: ${first.refusal}`)
103
104
  const { firstStep } = first
104
105
 
105
- const overrideRefusal = yield* checkOverrides(
106
+ yield* checkOverrides(
106
107
  commandLabel,
107
108
  varOverrides,
108
109
  Object.keys(workflow.vars),
109
110
  Object.keys(workflow.env),
110
111
  )
111
- if (overrideRefusal !== undefined) return overrideRefusal
112
112
 
113
113
  const baseOf = workflow.def.base
114
114
  const flags = yield* StartFlags
@@ -15,14 +15,27 @@ import {
15
15
  import {
16
16
  agent,
17
17
  check,
18
+ envSettings as env,
18
19
  human,
20
+ judge,
19
21
  moveScript,
20
22
  read,
21
23
  removeScript,
22
24
  run,
23
25
  showScript,
26
+ settings as vars,
27
+ workflow,
24
28
  } from "../flows/index.js"
25
- import { runsItsScript, testRepo } from "./TestRepo.js"
29
+ import {
30
+ inTurn,
31
+ runsItsScript,
32
+ testRepo,
33
+ verdict,
34
+ verdictWhen,
35
+ whenPrompted,
36
+ type ScriptedActor,
37
+ type TestRepo,
38
+ } from "./TestRepo.js"
26
39
 
27
40
  const tiny = async (): Promise<void> => {
28
41
  await agent("plan", "Write PLAN.md.")
@@ -57,8 +70,8 @@ describe("testRepo", () => {
57
70
  it("runs a workflow given as gtd.config.ts source", async () => {
58
71
  const repo = testRepo({
59
72
  workflow: [
60
- `import { agent, human } from "@pmelab/gtd/flows"`,
61
- `export const chores = async () => { await agent("sweep", "Sweep."); await human("done") }`,
73
+ `import { agent, human, workflow } from "@pmelab/gtd/flows"`,
74
+ `export const chores = workflow(async () => { await agent("sweep", "Sweep."); await human("done") })`,
62
75
  `export default chores`,
63
76
  ].join("\n"),
64
77
  })
@@ -75,7 +88,7 @@ describe("testRepo", () => {
75
88
  await human("done")
76
89
  }
77
90
  const repo = testRepo({
78
- workflow: { default: tidy, tidy },
91
+ workflow: { default: tidy, tidy: workflow(tidy) },
79
92
  files: { "NOTES.md": "notes\n", "scratch.txt": "x\n" },
80
93
  })
81
94
  await repo.start("tidy")
@@ -201,12 +214,56 @@ describe("testRepo", () => {
201
214
  let n = 0
202
215
  await expect(
203
216
  repo.drive({ spin: (_, r) => r.write("n", `${++n}\n`) }, { maxBeats: 5 }),
204
- ).rejects.toThrow(/did not rest within 5 beats/)
217
+ ).rejects.toThrow(
218
+ "did not rest within 5 beats; it passed through:\nspin → spin → spin → spin → spin",
219
+ )
220
+ })
221
+
222
+ describe("judge verdicts", () => {
223
+ const judged = async (): Promise<void> => {
224
+ await agent("plan", "Plan.")
225
+ const { answers } = await judge("ready", {
226
+ questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
227
+ evidence: { plan: "p" },
228
+ })
229
+ await human(answers.ready?.answer === "yes" ? "ship" : "rework")
230
+ }
231
+ const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
232
+ const template =
233
+ 'expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:\n[{ "id": "ready", "answer": true, "p": 0.9 }]'
234
+
235
+ it("echoes a refused verdict and the shape it should have", async () => {
236
+ const repo = testRepo({ workflow: { default: judged } })
237
+ repo.write("go", "\n")
238
+ const error = await repo
239
+ .drive({ plan, ready: () => '{"ready": true}' })
240
+ .catch((e: Error) => e)
241
+ expect((error as Error).message).toContain(
242
+ `the actor for judge step "ready" sent a verdict gtd refused:\n{"ready": true}\n${template}\ngtd judge answer exited 2\n`,
243
+ )
244
+ })
245
+
246
+ it("says an actor returned an object rather than no verdict", async () => {
247
+ const repo = testRepo({ workflow: { default: judged } })
248
+ repo.write("go", "\n")
249
+ const actor = (() => [{ id: "ready", answer: true, p: 0.9 }]) as unknown as ScriptedActor
250
+ await expect(repo.drive({ plan, ready: actor })).rejects.toThrow(
251
+ `the actor for judge step "ready" returned an array, not a JSON string — JSON.stringify it\n${template}`,
252
+ )
253
+ })
254
+
255
+ it("says an actor returned nothing", async () => {
256
+ const repo = testRepo({ workflow: { default: judged } })
257
+ repo.write("go", "\n")
258
+ await expect(repo.drive({ plan, ready: () => undefined })).rejects.toThrow(
259
+ `the actor for judge step "ready" returned no verdict\n${template}`,
260
+ )
261
+ })
205
262
  })
206
263
 
207
264
  describe("canned commands", () => {
208
265
  const checked = async (): Promise<void> => {
209
- const green = await check("test", "npm test", { report: ".gtd/FEEDBACK.md" })
266
+ const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
210
267
  await human(green ? "sign-off" : "red")
211
268
  }
212
269
 
@@ -310,4 +367,230 @@ describe("testRepo", () => {
310
367
  expect(() => repo.ref("refs/tags/v1", "nowhere")).toThrow("resolves to no commit")
311
368
  })
312
369
  })
370
+
371
+ describe("modules a config imports", () => {
372
+ it("serves a specifier the config's source imports, read off the module the test imported", async () => {
373
+ const shipped = { tiny, chores: tiny }
374
+ const repo = testRepo({
375
+ workflow: [
376
+ `import { workflow } from "@pmelab/gtd/flows"`,
377
+ `import tidy, * as all from "#workflow"`,
378
+ `export { chores } from "#workflow"`,
379
+ `export const both = all.tiny === tidy ? workflow(() => tidy()) : undefined`,
380
+ `export default tidy`,
381
+ ].join("\n"),
382
+ modules: { "#workflow": { ...shipped, default: tiny } },
383
+ })
384
+ await repo.start("both")
385
+ expect((await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })).state).toBe("build")
386
+ expect(repo.read("gtd.config.ts")).not.toContain('from "#workflow"')
387
+ })
388
+ })
389
+
390
+ describe("strict drives", () => {
391
+ const signOff = async (): Promise<void> => {
392
+ await agent("plan", "Plan.")
393
+ await human("sign-off")
394
+ await agent("ship", "Ship.")
395
+ }
396
+ const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
397
+
398
+ it("fails on an actor whose step the drive never reached", async () => {
399
+ const repo = testRepo({ workflow: { default: signOff } })
400
+ repo.write("go", "\n")
401
+ await expect(repo.drive({ plan, ship: () => undefined })).rejects.toThrow(
402
+ 'strict drive: the process rests at "sign-off", and never reached the actors for "ship"',
403
+ )
404
+ })
405
+
406
+ it("leaves only the unreached-actor check out with unreachedActors: pass", async () => {
407
+ const repo = testRepo({ workflow: { default: signOff } })
408
+ repo.write("go", "\n")
409
+ expect(
410
+ (await repo.drive({ plan, ship: () => undefined }, { unreachedActors: "pass" })).state,
411
+ ).toBe("sign-off")
412
+ })
413
+
414
+ it("lets the same drive rest when it is not strict", async () => {
415
+ const repo = testRepo({ workflow: { default: signOff } })
416
+ repo.write("go", "\n")
417
+ expect((await repo.drive({ plan, ship: () => undefined }, { strict: false })).state).toBe(
418
+ "sign-off",
419
+ )
420
+ })
421
+
422
+ it("counts a step an earlier drive reached as reached", async () => {
423
+ const twice = async (): Promise<void> => {
424
+ await signOff()
425
+ await human("done")
426
+ }
427
+ const repo = testRepo({ workflow: { default: twice } })
428
+ repo.write("go", "\n")
429
+ const actors: Record<string, ScriptedActor> = { plan }
430
+ expect((await repo.drive(actors)).state).toBe("sign-off")
431
+ repo.write("signed", "\n")
432
+ await repo.land()
433
+ actors["ship"] = (_, r) => r.write("shipped", "\n")
434
+ expect((await repo.drive(actors)).state).toBe("done")
435
+ })
436
+
437
+ it("fails on a human step whose actor changes nothing, instead of repeating it", async () => {
438
+ const repo = testRepo({ workflow: { default: signOff } })
439
+ repo.write("go", "\n")
440
+ await expect(repo.drive({ plan, "sign-off": () => undefined })).rejects.toThrow(
441
+ 'strict drive: the actor for human step "sign-off" changed nothing, so nothing landed',
442
+ )
443
+ })
444
+
445
+ it("fails on a verdict returned at a step that is no judge", async () => {
446
+ const repo = testRepo({ workflow: { default: signOff } })
447
+ repo.write("go", "\n")
448
+ await expect(repo.drive({ plan: (beat, r) => (plan(beat, r), "[]") })).rejects.toThrow(
449
+ 'the actor for agent step "plan" returned a verdict',
450
+ )
451
+ })
452
+
453
+ it("fails on a check step without an actor, unless checks run themselves", async () => {
454
+ const checked = async (): Promise<void> => {
455
+ const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
456
+ await human(green ? "sign-off" : "red")
457
+ }
458
+ const strictRepo = testRepo({ workflow: { default: checked } })
459
+ strictRepo.write("go", "\n")
460
+ await expect(strictRepo.drive({})).rejects.toThrow(
461
+ 'strict drive: check step "test" has no actor',
462
+ )
463
+ const repo = testRepo({
464
+ workflow: { default: checked },
465
+ commands: [{ run: "npm test", exit: 1, output: "1 failing" }],
466
+ })
467
+ repo.write("go", "\n")
468
+ expect((await repo.drive({}, { checks: "run" })).state).toBe("red")
469
+ expect(repo.read(".gtd/FEEDBACK.md")).toContain("1 failing")
470
+ })
471
+ })
472
+
473
+ describe("actor helpers", () => {
474
+ const judged = async (): Promise<void> => {
475
+ await agent("plan", "Plan.")
476
+ const { answers } = await judge("ready", {
477
+ questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
478
+ evidence: { plan: "p" },
479
+ })
480
+ await human(answers.ready?.answer === "yes" ? "ship" : "rework")
481
+ }
482
+ const plan: ScriptedActor = (_, r) => r.write("PLAN.md", "p\n")
483
+
484
+ it("builds a verdict keyed by question id, p defaulting to 0.9", () => {
485
+ expect(verdict({ ready: true, tier: { answer: "low", p: 0.6 } })).toEqual({
486
+ answers: [
487
+ { id: "ready", answer: true, p: 0.9 },
488
+ { id: "tier", answer: "low", p: 0.6 },
489
+ ],
490
+ })
491
+ })
492
+
493
+ it("answers a judge with a verdict object", async () => {
494
+ const repo = testRepo({ workflow: { default: judged } })
495
+ repo.write("go", "\n")
496
+ expect((await repo.drive({ plan, ready: () => verdict({ ready: false }) })).state).toBe(
497
+ "rework",
498
+ )
499
+ })
500
+
501
+ it("answers by what the pending questions contain", async () => {
502
+ const repo = testRepo({ workflow: { default: judged } })
503
+ repo.write("go", "\n")
504
+ const ready = inTurn([
505
+ verdictWhen("Unrelated?", verdict({ ready: false })),
506
+ verdictWhen("Ready?", verdict({ ready: true })),
507
+ ])
508
+ expect((await repo.drive({ plan, ready })).state).toBe("ship")
509
+ })
510
+
511
+ it("runs an actor only at a visit prompted with the text, and every actor in turn", async () => {
512
+ const repo = testRepo({ workflow: { default: judged } })
513
+ repo.write("go", "\n")
514
+ const seen: string[] = []
515
+ await repo.drive({
516
+ plan: inTurn([
517
+ whenPrompted("Plan.", plan),
518
+ whenPrompted("Something else.", () => {
519
+ seen.push("else")
520
+ }),
521
+ () => {
522
+ seen.push("always")
523
+ },
524
+ ]),
525
+ })
526
+ expect(repo.read("PLAN.md")).toBe("p\n")
527
+ expect(seen).toEqual(["always"])
528
+ })
529
+
530
+ it("runs a script with commands of its own before the canned ones", () => {
531
+ const repo = testRepo({ workflow: { default: tiny }, commands: [{ run: /.*/, exit: 1 }] })
532
+ repo.runScript("make docs\n", {
533
+ commands: [{ run: "make docs", effect: (r) => r.write("docs.html", "<p>\n") }],
534
+ })
535
+ expect(repo.read("docs.html")).toBe("<p>\n")
536
+ })
537
+ })
538
+
539
+ describe("commits and the environment", () => {
540
+ it("lists every commit with its subject, message and trailers", async () => {
541
+ const repo = testRepo({ workflow: { default: tiny } })
542
+ repo.write("IDEA.md", "x\n")
543
+ repo.commit("feat: x\n\nA body.\n\nRefs: #1\nRefs: #2")
544
+ const [initial, feat] = repo.commits()
545
+ expect(initial).toMatchObject({ subject: "chore: initial commit", trailers: {} })
546
+ expect(feat).toEqual({
547
+ hash: repo.hashes()[1],
548
+ subject: "feat: x",
549
+ message: repo.messages()[1],
550
+ trailers: { Refs: ["#1", "#2"] },
551
+ })
552
+ await repo.drive({ plan: (_, r) => r.write("PLAN.md", "p\n") })
553
+ expect(repo.commits().at(-1)!.trailers["Gtd-Step"]).toEqual(["plan#1"])
554
+ })
555
+
556
+ const greeter = {
557
+ default: workflow(
558
+ async (): Promise<void> => {
559
+ await agent("wake", "Wake up.")
560
+ await agent("greet", `Say ${env["greeting"]}, flag ${vars["flag_probe"]}.`)
561
+ },
562
+ { envDefaults: { greeting: "hi" }, defaults: { flag_probe: "0" } },
563
+ ),
564
+ }
565
+ const greet = async (repo: TestRepo): Promise<string> => {
566
+ const rest = await repo.drive({ wake: (_, r) => r.write("awake", "\n") })
567
+ return rest.content
568
+ }
569
+
570
+ it("changes a variable gtd sees from the next call on", async () => {
571
+ const repo = testRepo({ workflow: greeter })
572
+ repo.write("go", "\n")
573
+ expect(await greet(repo)).toContain("Say hi")
574
+ repo.setEnv("GTD_GREETING", "hello")
575
+ expect((await repo.next()).content).toContain("Say hello")
576
+ repo.setEnv("GTD_GREETING", undefined)
577
+ expect((await repo.next()).content).toContain("Say hi")
578
+ })
579
+
580
+ it("keeps the runner's GTD_FLAG_* out when ambient flags are off", async () => {
581
+ const saved = process.env["GTD_FLAG_PROBE"]
582
+ process.env["GTD_FLAG_PROBE"] = "1"
583
+ try {
584
+ const ambient = testRepo({ workflow: greeter })
585
+ ambient.write("go", "\n")
586
+ expect(await greet(ambient)).toContain("flag 1")
587
+ const isolated = testRepo({ workflow: greeter, ambientFlags: false })
588
+ isolated.write("go", "\n")
589
+ expect(await greet(isolated)).toContain("flag 0")
590
+ } finally {
591
+ if (saved === undefined) delete process.env["GTD_FLAG_PROBE"]
592
+ else process.env["GTD_FLAG_PROBE"] = saved
593
+ }
594
+ })
595
+ })
313
596
  })