@pmelab/gtd 58.0.0 → 58.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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -2
  3. package/dist/chunks/{Schema-C6fLlQzx.mjs → Schema-B3o6eRNU.mjs} +637 -3957
  4. package/dist/chunks/{reserved-BzAFXuz_.mjs → reserved-lMYCaapY.mjs} +1 -1
  5. package/dist/chunks/{review-CVqRS0mc.mjs → review-Bfaror38.mjs} +2 -2
  6. package/dist/chunks/{tail-JQzB21Q3.mjs → tail-Bd7L940p.mjs} +1 -1
  7. package/dist/chunks/testkit-D1-auYau.mjs +10362 -0
  8. package/dist/gtd.bundle.mjs +8 -8
  9. package/dist/testkit/gherkin.mjs +255 -0
  10. package/dist/testkit.mjs +2 -15461
  11. package/dist/workflows/cleanup.mjs +2 -2
  12. package/dist/workflows/feature.mjs +3 -3
  13. package/dist/workflows/fix.mjs +2 -2
  14. package/dist/workflows/review.mjs +3 -3
  15. package/dist/workflows/rollout.mjs +1 -1
  16. package/package.json +8 -1
  17. package/src/cli/Cli.test.ts +1 -1
  18. package/src/cli/History.test.ts +1 -1
  19. package/src/cli/Metrics.test.ts +1 -1
  20. package/src/cli/Program.test.ts +1 -1
  21. package/src/cli/Strays.test.ts +1 -1
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/emit/Emit.ts +1 -1
  24. package/src/emit/GitScript.ts +1 -1
  25. package/src/emit/OutcomeScript.ts +1 -1
  26. package/src/{testkit/steps.ts → gherkin/index.ts} +20 -3
  27. package/src/gherkin/module-config.feature +24 -0
  28. package/src/gherkin/tiny.fixture.ts +8 -0
  29. package/src/platform/Git.test.ts +2 -2
  30. package/src/platform/RetainedHistory.test.ts +2 -2
  31. package/src/platform/Workspace.test.ts +2 -2
  32. package/src/platform/WorktreeState.test.ts +1 -1
  33. package/src/step/planStart.test.ts +1 -1
  34. package/src/testkit/TestRepo.ts +7 -4
  35. package/src/testkit/index.ts +15 -9
  36. package/src/{testkit → twins}/Layers.ts +2 -2
  37. package/src/twins/index.ts +8 -0
  38. package/src/workflow/discovery.test.ts +1 -1
  39. package/src/workflow/discovery.ts +3 -3
  40. package/src/workflow/load.ts +3 -3
  41. /package/src/{testkit → gherkin}/testkit.feature +0 -0
  42. /package/src/{testkit → twins}/EmittedScriptRecognizer.test.ts +0 -0
  43. /package/src/{testkit → twins}/EmittedScriptRecognizer.ts +0 -0
  44. /package/src/{testkit → twins}/EmittedScripts.test.ts +0 -0
  45. /package/src/{testkit → twins}/FakeGitOperations.ts +0 -0
  46. /package/src/{testkit → twins}/FlowScripts.test.ts +0 -0
  47. /package/src/{testkit → twins}/FlowScripts.ts +0 -0
  48. /package/src/{testkit → twins}/GitTiers.test.ts +0 -0
  49. /package/src/{testkit → twins}/GitTiers.ts +0 -0
  50. /package/src/{testkit → twins}/InMemRepo.ts +0 -0
  51. /package/src/{testkit → twins}/Layers.test.ts +0 -0
  52. /package/src/{testkit → twins}/StrictGitOperations.ts +0 -0
  53. /package/src/{testkit → twins}/cliIo.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { It as registryEntry, Lt as requireFlag, Pt as flagReferences, St as envDefaults, a as flagShipped, i as skills, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-BzAFXuz_.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-JQzB21Q3.mjs";
2
+ import { It as registryEntry, Lt as requireFlag, Pt as flagReferences, St as envDefaults, a as flagShipped, i as skills, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Bd7L940p.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,8 +1,8 @@
1
1
  import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
2
2
  import { lensEntriesOf } from "../workflow-kit.mjs";
3
- import { B as designSystem, Ct as testFileGlobs, D as planner, F as clearCriteriaMessage, I as clearCriteriaQuestion, J as implementBuildingPrompt, L as criterionLine, M as buildReviewCollectingPrompt, P as builderSystem, Q as naiveTryMessage, R as designGateAnswerMessage, S as collecting, St as envDefaults, V as designTriagePrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, at as refactorFixPrompt, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as startGateBlockedMessage, h as REFACTOR, ht as seamSteerMessage, i as skills, it as refactorDroppedRefusal, j as agentWithSkills, l as gate, mt as seamByHandMessage, n as summary, o as naiveTestOn, ot as refactorNotePrompt, p as NAIVE_TEST, pt as reviewerSystem, q as idleMessage, r as access, rt as refactorBuildingPrompt, s as shipped, st as refactorRules, t as steering, vt as unwindFailure, w as fixNits, x as coder, xt as defaults, y as answerReviewQuestions, z as designImpactPrompt } from "../chunks/reserved-BzAFXuz_.mjs";
4
- import { a as refusePendingHandOff } from "../chunks/tail-JQzB21Q3.mjs";
5
- import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-CVqRS0mc.mjs";
3
+ import { B as designSystem, Ct as testFileGlobs, D as planner, F as clearCriteriaMessage, I as clearCriteriaQuestion, J as implementBuildingPrompt, L as criterionLine, M as buildReviewCollectingPrompt, P as builderSystem, Q as naiveTryMessage, R as designGateAnswerMessage, S as collecting, St as envDefaults, V as designTriagePrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, at as refactorFixPrompt, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as startGateBlockedMessage, h as REFACTOR, ht as seamSteerMessage, i as skills, it as refactorDroppedRefusal, j as agentWithSkills, l as gate, mt as seamByHandMessage, n as summary, o as naiveTestOn, ot as refactorNotePrompt, p as NAIVE_TEST, pt as reviewerSystem, q as idleMessage, r as access, rt as refactorBuildingPrompt, s as shipped, st as refactorRules, t as steering, vt as unwindFailure, w as fixNits, x as coder, xt as defaults, y as answerReviewQuestions, z as designImpactPrompt } from "../chunks/reserved-lMYCaapY.mjs";
4
+ import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
5
+ import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
6
6
  import { posix } from "node:path";
7
7
  //#region src/workflows/shared/fences.ts
8
8
  const markFenced = (text) => {
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
2
  import { b as reviewNotes, x as reviewRisks } from "../chunks/steering-CCNvngSH.mjs";
3
- import { G as fixReviewNotesPrompt, St as envDefaults, W as finisherSystem, c as baseline, i as skills, j as agentWithSkills, n as summary, r as access, s as shipped, t as steering, v as REVIEW, x as coder, xt as defaults } from "../chunks/reserved-BzAFXuz_.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-JQzB21Q3.mjs";
3
+ import { G as fixReviewNotesPrompt, St as envDefaults, W as finisherSystem, c as baseline, i as skills, j as agentWithSkills, n as summary, r as access, s as shipped, t as steering, v as REVIEW, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Bd7L940p.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, start } from "../flows.mjs";
2
- import { St as envDefaults, W as finisherSystem, _ as REQUIREMENTS, dt as reviewGateBlockedMessage, i as skills, j as agentWithSkills, l as gate, n as summary, r as access, s as shipped, t as steering, ut as reviewFeedbackFixPrompt, x as coder, xt as defaults } from "../chunks/reserved-BzAFXuz_.mjs";
3
- import { a as refusePendingHandOff } from "../chunks/tail-JQzB21Q3.mjs";
4
- import { r as reUnwind, t as buildTail } from "../chunks/review-CVqRS0mc.mjs";
2
+ import { St as envDefaults, W as finisherSystem, _ as REQUIREMENTS, dt as reviewGateBlockedMessage, i as skills, j as agentWithSkills, l as gate, n as summary, r as access, s as shipped, t as steering, ut as reviewFeedbackFixPrompt, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
3
+ import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
4
+ import { r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
5
5
  //#region src/workflows/review/review.ts
6
6
  const fixFeedback = async () => {
7
7
  await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Ft as flipProblems, Lt as requireFlag, Nt as flagMentions, St as envDefaults, a as flagShipped, f as FEEDBACK, i as skills, l as gate, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-BzAFXuz_.mjs";
2
+ import { Ft as flipProblems, Lt as requireFlag, Nt as flagMentions, St as envDefaults, a as flagShipped, f as FEEDBACK, i as skills, l as gate, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "58.0.0",
3
+ "version": "58.1.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": {
@@ -66,6 +66,11 @@
66
66
  "types": "./src/testkit/index.ts",
67
67
  "source": "./src/testkit/index.ts",
68
68
  "default": "./dist/testkit.mjs"
69
+ },
70
+ "./testkit/gherkin": {
71
+ "types": "./src/gherkin/index.ts",
72
+ "source": "./src/gherkin/index.ts",
73
+ "default": "./dist/testkit/gherkin.mjs"
69
74
  }
70
75
  },
71
76
  "files": [
@@ -76,6 +81,8 @@
76
81
  "src/wire/",
77
82
  "src/steering/",
78
83
  "src/testkit/",
84
+ "src/twins/",
85
+ "src/gherkin/",
79
86
  "src/cli/",
80
87
  "src/edge/",
81
88
  "src/emit/",
@@ -25,7 +25,7 @@ import {
25
25
  import { EXIT_CODES, EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
26
26
  import { Host } from "../platform/index.js"
27
27
  import { type CommandRequirements } from "./Cli.js"
28
- import { InMemRepo, makeCapturingCliIo, testLayers } from "../testkit/index.js"
28
+ import { InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
29
29
  import { CONFIG_SCAFFOLD } from "../install/index.js"
30
30
 
31
31
  const FLAG_NAMES = [
@@ -1,6 +1,6 @@
1
1
  import { Effect, Exit } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
- import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../testkit/index.js"
3
+ import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
4
4
  import { runCli } from "./index.js"
5
5
  import { historyEntries, resolveHistoryRef, type HistoryEntry } from "./History.js"
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { Effect } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
- import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../testkit/index.js"
3
+ import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
4
4
  import { runCli } from "./index.js"
5
5
  import { processMetrics, readProcessMetrics, totalMetrics, type MetricCommit } from "./Metrics.js"
6
6
 
@@ -18,7 +18,7 @@ vi.mock("node:os", async (importOriginal) => ({
18
18
  import { runCli, type Command } from "./index.js"
19
19
  import { EXIT_USAGE_ERROR, stallDiagnosis, noopText, WIRE_SCHEMA } from "../wire/index.js"
20
20
  import { needsOf, runCommand, SelectorUsageError } from "./Program.js"
21
- import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../testkit/index.js"
21
+ import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../twins/index.js"
22
22
  import {
23
23
  commitAll,
24
24
  DID_NOT_RUN_COMMENT,
@@ -1,7 +1,7 @@
1
1
  import { Effect } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
3
  import { EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_STRAYS_FOUND } from "../wire/index.js"
4
- import { InMemRepo, makeCapturingCliIo } from "../testkit/index.js"
4
+ import { InMemRepo, makeCapturingCliIo } from "../twins/index.js"
5
5
  import { runCli } from "./index.js"
6
6
 
7
7
  const seed = (): InMemRepo => {
@@ -31,7 +31,7 @@ import {
31
31
  makeCapturingCliIo,
32
32
  makeInMemoryWorkspaceOps,
33
33
  testLayers,
34
- } from "../testkit/index.js"
34
+ } from "../twins/index.js"
35
35
 
36
36
  type Env = Readonly<Record<string, string | undefined>>
37
37
  type Files = Readonly<Record<string, string>>
package/src/emit/Emit.ts CHANGED
@@ -110,7 +110,7 @@ export const emitScripts = (
110
110
  optional: assembleScript(optional),
111
111
  })
112
112
 
113
- /** Exported so `src/testkit/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
113
+ /** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
114
114
  export const DID_NOT_RUN_COMMENT =
115
115
  "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn"
116
116
 
@@ -130,7 +130,7 @@ export const fetchHistoryRefs = (remote: string): string =>
130
130
  // No test asserts the two stay byte-identical; a drift between them is a
131
131
  // review-time risk this boundary accepts.
132
132
 
133
- /** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/testkit/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
133
+ /** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
134
134
  const OUTCOME_MARKER = "# gtd: outcome (print-only)"
135
135
 
136
136
  const printfLine = (fmt: string, args: readonly string[]): string =>
@@ -19,7 +19,7 @@ const printfLine = (fmt: string, args: readonly string[]): string =>
19
19
 
20
20
  /**
21
21
  * Marks an emitted block as print-only. Load-bearing beyond documentation:
22
- * `src/testkit/EmittedScriptRecognizer.ts` recognizes an outcome block by
22
+ * `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
23
23
  * this line, and a bare `printf` would be ambiguous with a workflow
24
24
  * `script:` command that happens to start the same way.
25
25
  */
@@ -11,7 +11,15 @@ import {
11
11
  } from "quickpickle"
12
12
  import assert from "node:assert"
13
13
  import type { TestContext } from "vitest"
14
- import { runsItsScript, testRepo, type Actor, type TestRepo } from "./TestRepo.js"
14
+ import { resolve } from "node:path"
15
+ import { pathToFileURL } from "node:url"
16
+ import {
17
+ runsItsScript,
18
+ testRepo,
19
+ type Actor,
20
+ type TestRepo,
21
+ type WorkflowModule,
22
+ } from "../testkit/index.js"
15
23
 
16
24
  export class TestkitWorld extends QuickPickleWorld {
17
25
  // Re-declared: under `nodenext` a subclass loses the base's members from TS's view.
@@ -21,7 +29,7 @@ export class TestkitWorld extends QuickPickleWorld {
21
29
  super(context, info)
22
30
  }
23
31
 
24
- private gtdConfig: string | undefined = undefined
32
+ private gtdConfig: WorkflowModule | undefined = undefined
25
33
  private env: Record<string, string> = {}
26
34
  /** Per step, what its scripted actor does, in the order the scenario declared it. */
27
35
  actions = new Map<string, Actor[]>()
@@ -33,7 +41,7 @@ export class TestkitWorld extends QuickPickleWorld {
33
41
  // The repository is created on first use and keeps the config and
34
42
  // environment it was created with, so a later change to either is refused
35
43
  // rather than silently ignored.
36
- configure(config: string): void {
44
+ configure(config: WorkflowModule): void {
37
45
  this.assertNotCreated("a gtd config")
38
46
  this.gtdConfig = config
39
47
  }
@@ -41,6 +49,8 @@ export class TestkitWorld extends QuickPickleWorld {
41
49
  extendConfig(tail: string): void {
42
50
  this.assertNotCreated("a gtd config")
43
51
  if (this.gtdConfig === undefined) throw new Error("no gtd config to extend")
52
+ if (typeof this.gtdConfig !== "string")
53
+ throw new Error("a gtd config from a module cannot be extended")
44
54
  this.gtdConfig += `\n${tail}`
45
55
  }
46
56
 
@@ -108,6 +118,13 @@ Given("a gtd config with:", (world: TestkitWorld, source: string) => {
108
118
  world.configure(withNewline(source))
109
119
  })
110
120
 
121
+ // Imported by the test runner itself, from the working directory: the module and
122
+ // the engine share one `@pmelab/gtd/flows`, and a config source string could
123
+ // import only files written into the in-memory repository.
124
+ Given("a gtd config from the module {string}", async (world: TestkitWorld, path: string) => {
125
+ world.configure({ ...(await import(pathToFileURL(resolve(path)).href)) })
126
+ })
127
+
111
128
  Given("the gtd config ends with:", (world: TestkitWorld, source: string) => {
112
129
  world.extendConfig(withNewline(source))
113
130
  })
@@ -0,0 +1,24 @@
1
+ @inmem
2
+ Feature: a scenario runs a workflow module from disk
3
+
4
+ The test runner imports the module, so it shares the engine's runtime and
5
+ may import anything on disk. A config written as source can import only
6
+ files inside the in-memory repository.
7
+
8
+ Scenario: the module's default export starts on a change in the tree
9
+ Given a gtd config from the module "src/gherkin/tiny.fixture.ts"
10
+ And a file "IDEA.md" with:
11
+ """
12
+ a calculator
13
+ """
14
+ And step "plan" writes "PLAN.md" with:
15
+ """
16
+ add two numbers
17
+ """
18
+ When the process is driven
19
+ Then the process rests at "sign-off"
20
+ And the commit subjects end with:
21
+ """
22
+ gtd(agent): plan → sign-off
23
+ """
24
+
@@ -0,0 +1,8 @@
1
+ import { agent, human } from "../flows/index.js"
2
+
3
+ export const tiny = async (): Promise<void> => {
4
+ await agent("plan", "Write PLAN.md.")
5
+ await human("sign-off")
6
+ }
7
+
8
+ export default tiny
@@ -7,11 +7,11 @@ import { Cause, Effect, Exit, Option } from "effect"
7
7
  import { describe, expect, it } from "vitest"
8
8
  import { GitService, InvalidRefError, resolvedRefOrCorrupted } from "./Git.js"
9
9
  import { Host } from "./index.js"
10
- import { gitTiers, runGitServiceContract, runGitScriptContract } from "../testkit/index.js"
10
+ import { gitTiers, runGitServiceContract, runGitScriptContract } from "../twins/index.js"
11
11
 
12
12
  /**
13
13
  * The `GitOperations` contract, run identically against both the Live and
14
- * InMemory tiers (`src/testkit/GitTiers.ts`) — 20 operations × 2 tiers.
14
+ * InMemory tiers (`src/twins/GitTiers.ts`) — 20 operations × 2 tiers.
15
15
  */
16
16
  for (const makeTier of gitTiers) {
17
17
  const name = makeTier().name
@@ -1,6 +1,6 @@
1
1
  import { Effect, Option } from "effect"
2
2
  import { describe, expect, it, vi } from "vitest"
3
- import { strictGitOperations as stubGit } from "../testkit/index.js"
3
+ import { strictGitOperations as stubGit } from "../twins/index.js"
4
4
  import {
5
5
  clearRetainedHistory,
6
6
  HISTORY_REF,
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  /**
15
15
  * Unit coverage for the retention edge module (`src/platform/RetainedHistory.ts`),
16
- * using `strictGitOperations` (`src/testkit/StrictGitOperations.ts`) as its
16
+ * using `strictGitOperations` (`src/twins/StrictGitOperations.ts`) as its
17
17
  * `GitOperations` double — a Proxy failing on any method a test didn't
18
18
  * override, so an unexpected call fails loudly instead of silently succeeding.
19
19
  */
@@ -15,7 +15,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"
15
15
  import { Effect, Layer } from "effect"
16
16
  import { NodeContext } from "@effect/platform-node"
17
17
  import { GitService, Host, Workspace, type WorkspaceOps } from "./index.js"
18
- import { InMemRepo, makeInMemoryWorkspaceOps } from "../testkit/index.js"
18
+ import { InMemRepo, makeInMemoryWorkspaceOps } from "../twins/index.js"
19
19
 
20
20
  // `execFileSync` is a named export of the built-in `node:child_process` ESM
21
21
  // module — not configurable, so `vi.spyOn` can't wrap it directly. `vi.mock`
@@ -31,7 +31,7 @@ vi.mock("node:child_process", async (importOriginal) => {
31
31
  })
32
32
 
33
33
  /**
34
- * One `Workspace` tier, in the shape `src/testkit/GitTiers.ts` establishes
34
+ * One `Workspace` tier, in the shape `src/twins/GitTiers.ts` establishes
35
35
  * for `GitOperations`: a `root`, a way to seed working-tree/committed
36
36
  * content, and `provide` to run an Effect against this tier's `Workspace`.
37
37
  */
@@ -16,7 +16,7 @@ import { NodeContext } from "@effect/platform-node"
16
16
  import { describe, expect, it } from "vitest"
17
17
  import { loopLogPath, worktreeGitDir } from "./WorktreeState.js"
18
18
  import { GitService, Host, Workspace } from "./index.js"
19
- import { InMemRepo, testLayers } from "../testkit/index.js"
19
+ import { InMemRepo, testLayers } from "../twins/index.js"
20
20
 
21
21
  const provide = <A>(
22
22
  eff: Effect.Effect<A, never, Host | Workspace>,
@@ -1,7 +1,7 @@
1
1
  import { Effect } from "effect"
2
2
  import { describe, expect, it } from "vitest"
3
3
  import { planStart, type StartOutcome } from "./planStart.js"
4
- import { InMemRepo, testLayers } from "../testkit/index.js"
4
+ import { InMemRepo, testLayers } from "../twins/index.js"
5
5
  import { ConfigService } from "../workflow/index.js"
6
6
 
7
7
  const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
@@ -2,10 +2,13 @@ import { Effect } from "effect"
2
2
  import { PassThrough } from "node:stream"
3
3
  import { runCli } from "../cli/index.js"
4
4
  import { EXIT_OK, type BeatDocument } from "../wire/index.js"
5
- import { makeCapturingCliIo, type CapturedCliResult } from "./cliIo.js"
6
- import { applyEmittedScript } from "./EmittedScriptRecognizer.js"
7
- import { applyFlowScript } from "./FlowScripts.js"
8
- import { InMemRepo } from "./InMemRepo.js"
5
+ import {
6
+ applyEmittedScript,
7
+ applyFlowScript,
8
+ InMemRepo,
9
+ makeCapturingCliIo,
10
+ type CapturedCliResult,
11
+ } from "../twins/index.js"
9
12
 
10
13
  /**
11
14
  * The repository's `gtd.config.ts`: its source, or the module itself. A module
@@ -1,9 +1,15 @@
1
- export { runsItsScript, testRepo, type Actor, type TestRepo } from "./TestRepo.js"
2
- export { InMemRepo } from "./InMemRepo.js"
3
- export { makeInMemoryWorkspaceOps, testLayers, type ScriptedCommand } from "./Layers.js"
4
- export { makeCapturingCliIo } from "./cliIo.js"
5
- export { applyEmittedScript } from "./EmittedScriptRecognizer.js"
6
- export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
7
- export { strictGitOperations } from "./StrictGitOperations.js"
8
- export { fakeGitOperations } from "./FakeGitOperations.js"
9
- export type { TestkitWorld } from "./steps.js"
1
+ // The public testkit: runs any workflow against the real engine in an in-memory
2
+ // repository. It imports no test runner, so any runner can use it; the
3
+ // gherkin steps are the opt-in \`@pmelab/gtd/testkit/gherkin\`.
4
+ export {
5
+ runsItsScript,
6
+ testRepo,
7
+ type Actor,
8
+ type Actors,
9
+ type DriveOptions,
10
+ type TestRepo,
11
+ type TestRepoOptions,
12
+ type WorkflowModule,
13
+ } from "./TestRepo.js"
14
+ export { InMemRepo, type CapturedCliResult } from "../twins/index.js"
15
+ export type { TestkitWorld } from "../gherkin/index.js"
@@ -4,7 +4,7 @@
4
4
  // gtd itself spawns no subprocess at all any more (`gtd renders; the driver
5
5
  // executes`) — a mode's `format:`/`validate:` command is emitted text, never
6
6
  // run in-process. `ScriptedCommand` below still exists for the world's own
7
- // fake-shell interpreter (`src/testkit/EmittedScriptRecognizer.ts`), which
7
+ // fake-shell interpreter (`src/twins/EmittedScriptRecognizer.ts`), which
8
8
  // simulates a driver running gtd's emitted scripts against the in-memory
9
9
  // worktree — a wholly separate mechanism from any gtd-internal layer.
10
10
 
@@ -193,7 +193,7 @@ export interface TestWorldOptions {
193
193
  * Effect test has no `--verbose` flag of its own to gate on.
194
194
  */
195
195
  readonly narrate?: (line: string) => void
196
- /** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/testkit/cliIo.ts` overrides this with the real decoded flag. */
196
+ /** Gates `narrate` above — defaults to `true` (fires whenever a sink is given) since these tests aren't exercising `Cli.ts`'s own `--verbose` gate. `src/twins/cliIo.ts` overrides this with the real decoded flag. */
197
197
  readonly verbose?: boolean
198
198
  /** Real or fake driver and tool commands — each one left out fails when called. */
199
199
  readonly tools?: Partial<ToolCommands["Type"]>
@@ -0,0 +1,8 @@
1
+ export { InMemRepo } from "./InMemRepo.js"
2
+ export { makeInMemoryWorkspaceOps, testLayers, type ScriptedCommand } from "./Layers.js"
3
+ export { makeCapturingCliIo, type CapturedCliResult } from "./cliIo.js"
4
+ export { applyEmittedScript } from "./EmittedScriptRecognizer.js"
5
+ export { applyFlowScript } from "./FlowScripts.js"
6
+ export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
7
+ export { strictGitOperations } from "./StrictGitOperations.js"
8
+ export { fakeGitOperations } from "./FakeGitOperations.js"
@@ -6,7 +6,7 @@ import { NodeContext } from "@effect/platform-node"
6
6
  import { Effect, Layer } from "effect"
7
7
  import { afterEach, describe, expect, it } from "vitest"
8
8
  import { GitService, Host, Narrator, Workspace } from "../platform/index.js"
9
- import { InMemRepo, testLayers } from "../testkit/index.js"
9
+ import { InMemRepo, testLayers } from "../twins/index.js"
10
10
  import { ConfigDiscovery, ConfigService } from "./index.js"
11
11
 
12
12
  // A config and the local modules it imports load the same from disk, where
@@ -38,7 +38,7 @@ export const WORKFLOW_MODULE = "gtd.config.ts"
38
38
  * The file names gtd read repository settings from before `gtd.config.ts` took
39
39
  * them over. Still looked for, only to refuse them: a leftover one would
40
40
  * otherwise be silently ignored. Shared with the in-memory counterpart in
41
- * `src/testkit/Layers.ts`, so both tiers refuse the same names.
41
+ * `src/twins/Layers.ts`, so both tiers refuse the same names.
42
42
  */
43
43
  export const LEGACY_RC_FILES = [
44
44
  ".gtdrc",
@@ -71,7 +71,7 @@ const legacyRcFileAt = (path: string): LegacyRcFile | undefined => {
71
71
  * the user's home dir (inclusive, when it is an ancestor) or after reaching the
72
72
  * filesystem root — whichever comes first. Returned innermost→outermost.
73
73
  * Exported (via the barrel) for the same reason `SEARCH_PLACES` is — one
74
- * shared implementation, not a second copy in `src/testkit/Layers.ts` that
74
+ * shared implementation, not a second copy in `src/twins/Layers.ts` that
75
75
  * could silently diverge.
76
76
  */
77
77
  export const walkUp = (from: string, home: string): ReadonlyArray<string> => {
@@ -115,7 +115,7 @@ const workflowModule = (
115
115
  * reads; discovery is effectful and environment-dependent: `.Live` reads
116
116
  * REAL disk, so an `@inmem` scenario's fake
117
117
  * `Workspace` — a pure in-memory `Map`, no real files anywhere on disk —
118
- * cannot back it. `src/testkit/Layers.ts` provides the in-memory
118
+ * cannot back it. `src/twins/Layers.ts` provides the in-memory
119
119
  * counterpart instead, mirroring how `Workspace`/`Host`/`GitService` already
120
120
  * each have a production and a fake layer behind one Tag.
121
121
  */
@@ -357,14 +357,14 @@ const shippedModules = (): Promise<ShippedModules> =>
357
357
  return { modules, missing }
358
358
  })())
359
359
 
360
- /** The file each subpath a config may import serves: all but `flows`, the engine's own, and `testkit`. */
360
+ /** The file each subpath a config may import serves: all but `flows`, the engine's own, and the testkit's. */
361
361
  const subpathFiles = (condition: string): ReadonlyMap<string, string> => {
362
362
  const { root, pkg } = ownPackage()
363
363
  const files = new Map<string, string>()
364
364
  for (const [subpath, target] of Object.entries(pkg.exports)) {
365
365
  const file = typeof target === "string" ? target : target[condition]
366
- // testkit imports the consumer's test runner, and no config runs one.
367
- if (subpath === "./flows" || subpath === "./testkit" || file === undefined) continue
366
+ // A config runs no tests, and the gherkin steps import the consumer's test runner.
367
+ if (subpath === "./flows" || subpath.startsWith("./testkit") || file === undefined) continue
368
368
  files.set(`${PACKAGE}/${subpath.replace(/^\.\//, "")}`, join(root, file))
369
369
  }
370
370
  return files
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes