@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
@@ -105,8 +105,8 @@ const land = async (repo: InMemRepo, files: Files = {}): Promise<string> => {
105
105
 
106
106
  const enter = async (repo: InMemRepo, workflow: string, ...vars: string[]): Promise<string> => {
107
107
  const flags = vars.flatMap((v) => ["--var", v])
108
- const { stdout, stderr, exitCode } = await cli(repo, "--workflow", workflow, ...flags)
109
- if (exitCode !== 0) throw new Error(`gtd --workflow exited ${exitCode}: ${stderr}`)
108
+ const { stdout, stderr, exitCode } = await cli(repo, "start", workflow, ...flags)
109
+ if (exitCode !== 0) throw new Error(`gtd start exited ${exitCode}: ${stderr}`)
110
110
  return applied(repo, stdout)
111
111
  }
112
112
 
@@ -117,26 +117,26 @@ const commit = (repo: InMemRepo, spec: CommitSpec, files: Files = {}): string =>
117
117
  return headOf(repo)
118
118
  }
119
119
 
120
- const LINEAR = `import { agent, human } from "@pmelab/gtd/flows"
120
+ const LINEAR = `import { agent, human, workflow } from "@pmelab/gtd/flows"
121
121
 
122
- export default async () => {
122
+ const defaults = { base: "", reviewer: "nobody" }
123
+ const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
124
+
125
+ export default workflow(async () => {
123
126
  await human("idle", { message: "idle-message" })
124
127
  await agent("building", "build-prompt")
125
128
  await agent("checking", "check-prompt")
126
- }
129
+ }, { defaults, base })
127
130
 
128
- export const side = async () => {
131
+ export const side = workflow(async () => {
129
132
  await agent("fixing", "fix-prompt")
130
133
  await agent("tidying", "tidy-prompt", { allowEmpty: true })
131
- }
134
+ }, { defaults, base })
132
135
 
133
- export const review = async () => {
136
+ export const review = workflow(async () => {
134
137
  await agent("fixing", "fix-prompt")
135
- }
138
+ }, { defaults, base })
136
139
 
137
- export const defaults = { base: "", reviewer: "nobody" }
138
-
139
- export const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
140
140
  `
141
141
 
142
142
  describe("currentRun", () => {
@@ -220,18 +220,18 @@ describe("currentRun", () => {
220
220
  })
221
221
 
222
222
  it("a workflow starting at the default's initial step still opens its own episode, not the empty tree", async () => {
223
- const STARTS_AT_INITIAL = `import { agent, human } from "@pmelab/gtd/flows"
223
+ const STARTS_AT_INITIAL = `import { agent, human, workflow } from "@pmelab/gtd/flows"
224
224
 
225
225
  export default async () => {
226
226
  await human("idle", { message: "idle-message" })
227
227
  await agent("building", "build-prompt")
228
228
  }
229
229
 
230
- export const again = async () => {
230
+ export const again = workflow(async () => {
231
231
  await human("idle", { message: "idle-message" })
232
232
  await agent("fixing", "fix-prompt")
233
233
  await agent("tidying", "tidy-prompt", { allowEmpty: true })
234
- }
234
+ })
235
235
  `
236
236
  const repo = repoWith(STARTS_AT_INITIAL)
237
237
  const before = headOf(repo)
@@ -469,12 +469,14 @@ describe("history reads stay bounded by the episode, not the repository", () =>
469
469
  const raceyGit: GitOperations = {
470
470
  ...base,
471
471
  subjectHistory: (pageSize, skip, head) =>
472
- Effect.tap(base.subjectHistory(pageSize, skip, head), () => {
473
- if (!moved) {
474
- moved = true
475
- repo.hardResetTo(boundary)
476
- }
477
- }),
472
+ Effect.tap(base.subjectHistory(pageSize, skip, head), () =>
473
+ Effect.sync(() => {
474
+ if (!moved) {
475
+ moved = true
476
+ repo.hardResetTo(boundary)
477
+ }
478
+ }),
479
+ ),
478
480
  }
479
481
 
480
482
  const run = await provideWithGit(currentRun, raceyGit, repo)
@@ -539,9 +541,10 @@ describe("summaryRun", () => {
539
541
  })
540
542
 
541
543
  describe("process marks", () => {
542
- const SUMMARISED = `${LINEAR}
543
- export const summary = (it) => it.marks.map((m) => m.name + "=" + m.value).join("|")
544
- `
544
+ const SUMMARISED = LINEAR.replace(
545
+ "export default",
546
+ `const summary = (it) => it.marks.map((m) => m.name + "=" + m.value).join("|")\n\nexport default`,
547
+ ).replaceAll("{ defaults, base }", "{ defaults, base, summary }")
545
548
  const opening = (marks: readonly Mark[]): CommitSpec => ({
546
549
  actor: "human",
547
550
  from: "idle",
@@ -609,17 +612,18 @@ const withTipMessage = (repo: InMemRepo, edit: (message: string) => string): Git
609
612
  }
610
613
 
611
614
  describe("summaryFor", () => {
612
- const SUMMARISED_FLOW = `import { agent, human, mark } from "@pmelab/gtd/flows"
615
+ const SUMMARISED_FLOW = `import { agent, human, mark, workflow } from "@pmelab/gtd/flows"
613
616
 
614
- export default async () => {
617
+ const summary = (it) => JSON.stringify(it)
618
+
619
+ export default workflow(async () => {
615
620
  await human("idle", { message: "idle-message" })
616
621
  mark("risk-tier", "high")
617
622
  await agent("building", "build-prompt")
618
623
  await human("gate", { message: "gate-message" })
619
624
  await agent("checking", "check-prompt")
620
- }
625
+ }, { summary })
621
626
 
622
- export const summary = (it) => JSON.stringify(it)
623
627
  `
624
628
  const landCost = async (repo: InMemRepo, files: Files, cost: number, model: string) => {
625
629
  write(repo, files)
@@ -678,7 +682,7 @@ export const summary = (it) => JSON.stringify(it)
678
682
  it("returns no prompt, not a failure, when an edited config no longer exports a summary", async () => {
679
683
  const { repo } = await finished()
680
684
  const run = await provide(summaryRun, repo)
681
- repo.writeFile("gtd.config.ts", SUMMARISED_FLOW.replace(/export const summary.*\n/, ""))
685
+ repo.writeFile("gtd.config.ts", SUMMARISED_FLOW.replace(", { summary })", ")"))
682
686
  expect(await provide(summaryFor(run), repo)).toEqual({ prompt: undefined })
683
687
  })
684
688
 
@@ -773,24 +777,24 @@ export default async () => {
773
777
  })
774
778
 
775
779
  describe("reviewBaseFor", () => {
776
- const LOOP = `import { agent, head, human, read } from "@pmelab/gtd/flows"
780
+ const LOOP = `import { agent, head, human, read, workflow } from "@pmelab/gtd/flows"
777
781
 
778
- export default async () => {
782
+ const defaults = { base: "" }
783
+ const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
784
+
785
+ export default workflow(async () => {
779
786
  await human("idle")
780
787
  while (read("DONE") === undefined) {
781
788
  const base = head()
782
789
  await human("checkpoint", { base })
783
790
  await agent("building", "build-prompt", { base })
784
791
  }
785
- }
792
+ }, { defaults, base })
786
793
 
787
- export const review = async () => {
794
+ export const review = workflow(async () => {
788
795
  await agent("fixing", "fix-prompt")
789
- }
796
+ }, { defaults, base })
790
797
 
791
- export const defaults = { base: "" }
792
-
793
- export const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
794
798
  `
795
799
 
796
800
  it("is the process's diff base at a step that names no base", async () => {
@@ -906,17 +910,18 @@ export default async () => {
906
910
  })
907
911
 
908
912
  describe("vars", () => {
909
- const VARS = `import { agent, human } from "@pmelab/gtd/flows"
913
+ const VARS = `import { agent, human, workflow } from "@pmelab/gtd/flows"
910
914
 
911
- export default async () => {
915
+ const defaults = { deployTarget: "npm test", reviewer: "nobody" }
916
+
917
+ export default workflow(async () => {
912
918
  await human("idle")
913
- }
919
+ }, { defaults })
914
920
 
915
- export const side = async () => {
921
+ export const side = workflow(async () => {
916
922
  await agent("working", "w")
917
- }
923
+ }, { defaults })
918
924
 
919
- export const defaults = { deployTarget: "npm test", reviewer: "nobody" }
920
925
  `
921
926
  const seeded = () => repoWith(VARS)
922
927
 
@@ -960,17 +965,18 @@ export const defaults = { deployTarget: "npm test", reviewer: "nobody" }
960
965
  })
961
966
 
962
967
  describe("StartFlags", () => {
963
- const FLAGGED = `import { agent, human } from "@pmelab/gtd/flows"
968
+ const FLAGGED = `import { agent, human, workflow } from "@pmelab/gtd/flows"
964
969
 
965
- export default async () => {
970
+ const defaults = { flag_risk_tier: "0", other: "x" }
971
+
972
+ export default workflow(async () => {
966
973
  await human("idle")
967
- }
974
+ }, { defaults })
968
975
 
969
- export const side = async () => {
976
+ export const side = workflow(async () => {
970
977
  await agent("working", "w")
971
- }
978
+ }, { defaults })
972
979
 
973
- export const defaults = { flag_risk_tier: "0", other: "x" }
974
980
  `
975
981
  const withFlags = <A>(
976
982
  eff: Effect.Effect<A, Error, RestRequirements>,
@@ -1004,15 +1010,16 @@ export const defaults = { flag_risk_tier: "0", other: "x" }
1004
1010
  })
1005
1011
 
1006
1012
  describe("environment settings", () => {
1007
- const ENVV = `import { agent, human } from "@pmelab/gtd/flows"
1013
+ const ENVV = `import { agent, human, workflow } from "@pmelab/gtd/flows"
1008
1014
 
1009
- export default async () => {
1015
+ const defaults = { floor: "0.7" }
1016
+ const envDefaults = { testCommand: "npm test" }
1017
+
1018
+ export default workflow(async () => {
1010
1019
  await human("idle")
1011
1020
  await agent("working", "w")
1012
- }
1021
+ }, { defaults, envDefaults })
1013
1022
 
1014
- export const defaults = { floor: "0.7" }
1015
- export const envDefaults = { testCommand: "npm test" }
1016
1023
  `
1017
1024
  const seeded = () => repoWith(ENVV)
1018
1025
 
@@ -1119,18 +1126,21 @@ describe("noProcessUnderway / restIsIdle", () => {
1119
1126
  })
1120
1127
 
1121
1128
  describe("judge rests", () => {
1122
- const JUDGED = (budget: string) => `import { human, judge, read } from "@pmelab/gtd/flows"
1129
+ const JUDGED = (
1130
+ budget: string,
1131
+ ) => `import { human, judge, read, workflow } from "@pmelab/gtd/flows"
1123
1132
 
1124
- export default async () => {
1133
+ const defaults = { judgeBudgetBytes: ${JSON.stringify(budget)} }
1134
+
1135
+ export default workflow(async () => {
1125
1136
  await human("idle")
1126
1137
  await judge("verdict", {
1127
1138
  questions: [{ id: "q", primitive: "noul", instructions: "i", criteria: "c" }],
1128
1139
  evidence: { log: read("LOG.md") ?? "" },
1129
1140
  message: "judge-message",
1130
1141
  })
1131
- }
1142
+ }, { defaults })
1132
1143
 
1133
- export const defaults = { judgeBudgetBytes: ${JSON.stringify(budget)} }
1134
1144
  `
1135
1145
  const LOG = Array.from({ length: 10 }, (_, i) => `line ${i} of the log`).join("\n") + "\n"
1136
1146
 
@@ -1315,17 +1325,17 @@ export default async () => {
1315
1325
  expect(landing.spec.branch).toBeUndefined()
1316
1326
  })
1317
1327
 
1318
- it("a --workflow process's entry is its opening commit", async () => {
1319
- const repo = repoWith(`import { agent, human } from "@pmelab/gtd/flows"
1328
+ it("a gtd start process's entry is its opening commit", async () => {
1329
+ const repo = repoWith(`import { agent, human, workflow } from "@pmelab/gtd/flows"
1320
1330
 
1321
1331
  export default async () => {
1322
1332
  await human("idle")
1323
1333
  await agent("building", "build-prompt")
1324
1334
  }
1325
1335
 
1326
- export const side = async () => {
1336
+ export const side = workflow(async () => {
1327
1337
  await agent("fixing", "fix-prompt")
1328
- }
1338
+ })
1329
1339
  `)
1330
1340
  const opening = await enter(repo, "side")
1331
1341
  repo.writeFile("a.txt", "a\n")
package/src/edge/Edge.ts CHANGED
@@ -83,14 +83,14 @@ interface EpisodeLocation {
83
83
  readonly legacyOpening: boolean
84
84
  /** The commit replay starts reading from, or -1 for the empty tree before history. */
85
85
  readonly baseIndex: number
86
- /** The process's first commit — a `--workflow` process's opening commit, else the one after the base. */
86
+ /** The process's first commit — a `gtd start` process's opening commit, else the one after the base. */
87
87
  readonly processStart: number
88
88
  }
89
89
 
90
90
  /**
91
91
  * Where the episode HEAD belongs to begins, read off subjects alone. Walking
92
92
  * back from HEAD: a bare `gtd(human): <step>` commit with no `Gtd-Step` is what
93
- * `gtd --workflow` writes — no landing ever produces one — and opens an
93
+ * `gtd start` writes — no landing ever produces one — and opens an
94
94
  * episode as its first commit (its `Gtd-Workflow` trailer names the workflow;
95
95
  * a legacy `--entry` opening has none); a commit that is not a gtd step commit, or one
96
96
  * entering the flow's first step (a finished episode), bounds the episode
@@ -1039,7 +1039,7 @@ type EndedOutcome = Extract<ReplayOutcome, { kind: "ended" }>
1039
1039
  type UnendedOutcome = Exclude<ReplayOutcome, { kind: "ended" }>
1040
1040
 
1041
1041
  /**
1042
- * Whether `gtd --workflow <name>` can start a process here: the workflow must
1042
+ * Whether `gtd start <name>` can start a process here: the workflow must
1043
1043
  * reach a step from the tree the opening commit would capture. That step is
1044
1044
  * what the opening commit's subject names.
1045
1045
  */
@@ -1319,7 +1319,7 @@ const decideLanding = (
1319
1319
  export const previewLanding = (rest: Rest): Effect.Effect<StateName | undefined> =>
1320
1320
  decideLanding(rest, undefined, Option.none(), Option.none()).pipe(
1321
1321
  Effect.map((landing) => (landing.kind === "commit" ? landing.to : undefined)),
1322
- Effect.catchAll(() => Effect.succeed(undefined)),
1322
+ Effect.catch(() => Effect.succeed(undefined)),
1323
1323
  )
1324
1324
 
1325
1325
  /** Every fact the pure landing planner needs, read once: the rest, its steering file, and the landing decision. */
@@ -1432,8 +1432,8 @@ const configRefusal = (
1432
1432
  )
1433
1433
 
1434
1434
  const differingConfigPaths = (
1435
- git: GitService["Type"],
1436
- workspace: Workspace["Type"],
1435
+ git: GitService["Service"],
1436
+ workspace: Workspace["Service"],
1437
1437
  root: string,
1438
1438
  config: { readonly fingerprint: ConfigFingerprint },
1439
1439
  run: ProcessRun,
@@ -86,6 +86,20 @@ describe("defineWorkflow", () => {
86
86
  })
87
87
  })
88
88
 
89
+ it("hands on the skills, access, steering and summary a build carries", () => {
90
+ const carried = {
91
+ flow,
92
+ skills: (vars: Readonly<Record<string, string>>) => ({ work: [vars.skill ?? "x"] }),
93
+ access: { work: { write: ["out/**"] } },
94
+ steering: { ".gtd/PLAN.md": "qa" },
95
+ summary: () => "done",
96
+ }
97
+ expect(configure(defineWorkflow(greetingSchema, () => carried))).toEqual({
98
+ ok: true,
99
+ build: carried,
100
+ })
101
+ })
102
+
89
103
  it("throws when a call's result is awaited as if it were a flow", async () => {
90
104
  await expect(Promise.resolve(greeter() as unknown as PromiseLike<void>)).rejects.toThrow(
91
105
  /not a flow/,
@@ -1,20 +1,37 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec"
2
- import type { Flow, WorkflowBase, WorkflowDoors } from "./runtime.js"
2
+ import type {
3
+ AccessExport,
4
+ Flow,
5
+ SkillsExport,
6
+ Steering,
7
+ Summary,
8
+ WorkflowBase,
9
+ WorkflowDoors,
10
+ } from "./runtime.js"
3
11
 
4
12
  // Registered, so a factory made by any copy of this file is recognised by the
5
13
  // engine's copy — the same reason the replay context is.
6
14
  const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory")
7
15
 
16
+ /** What a `defineWorkflow` build returns; `workflow(flow, options)` takes the same keys but `flow`. */
8
17
  export interface WorkflowBuild {
9
18
  readonly flow: Flow
10
- /** Process settings this workflow declares, over its file's `defaults` export. */
19
+ /** Process settings this workflow declares. */
11
20
  readonly defaults?: Readonly<Record<string, string>> | undefined
12
- /** Environment settings this workflow declares, over its file's `envDefaults` export. */
21
+ /** Environment settings this workflow declares. */
13
22
  readonly envDefaults?: Readonly<Record<string, string>> | undefined
14
- /** Doors onto this workflow. Two workflows building the same door is a load error unless the config's `doors` export declares that name; a declared door always wins. */
23
+ /** Doors onto this workflow. Two workflows building the same door build none unless one is exported under the door's name. */
15
24
  readonly doors?: WorkflowDoors | undefined
16
- /** This workflow's diff base; the config's `base` export wins when it answers, `undefined` falls through to this. */
25
+ /** This workflow's diff base. */
17
26
  readonly base?: WorkflowBase | undefined
27
+ /** This workflow's scope skill lists; a `config.skills` entry wins over the same scope here. */
28
+ readonly skills?: SkillsExport | undefined
29
+ /** This workflow's scope access; a `config.access` entry wins over the same scope here. */
30
+ readonly access?: AccessExport | undefined
31
+ /** This workflow's steering files, path -> mode. */
32
+ readonly steering?: Steering | undefined
33
+ /** `gtd summary`'s prompt for this workflow. */
34
+ readonly summary?: Summary | undefined
18
35
  }
19
36
 
20
37
  interface FactoryInternals {
@@ -0,0 +1,35 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { check } from "./helpers.js"
3
+ import { installContext, type FlowContext, type StepRequest } from "./internal.js"
4
+
5
+ afterEach(() => installContext(undefined))
6
+
7
+ const recording = () => {
8
+ const requests: StepRequest[] = []
9
+ installContext({
10
+ step: async (request: StepRequest) => {
11
+ requests.push(request)
12
+ },
13
+ head: () => "abcdef1234",
14
+ changes: () => [],
15
+ matches: (path: string, pattern: string) => path === pattern,
16
+ } as unknown as FlowContext)
17
+ return requests
18
+ }
19
+
20
+ describe("check()", () => {
21
+ it("takes the report positionally and renders a run step", async () => {
22
+ const requests = recording()
23
+ expect(await check("suite", "npm test", ".gtd/FEEDBACK.md", { label: "Tests" })).toBe(true)
24
+ expect(requests).toHaveLength(1)
25
+ expect(requests[0]).toMatchObject({ kind: "run", name: "suite", options: { label: "Tests" } })
26
+ })
27
+
28
+ it("refuses the removed options form, which an untyped config can still write", async () => {
29
+ recording()
30
+ const removed = check as unknown as (n: string, c: string, o: object) => Promise<boolean>
31
+ await expect(removed("suite", "npm test", { report: ".gtd/FEEDBACK.md" })).rejects.toThrow(
32
+ 'gtd: check("suite"): the report path is the third argument — check(name, command, report, options?)',
33
+ )
34
+ })
35
+ })
@@ -9,8 +9,8 @@ import {
9
9
  read,
10
10
  refuse,
11
11
  run,
12
+ settings,
12
13
  threads,
13
- vars,
14
14
  type Change,
15
15
  type JudgeAnswer,
16
16
  type Judgment,
@@ -33,9 +33,8 @@ export const numeric = (value: string | undefined, fallback: number): number =>
33
33
  /** Whether the last step added or rewrote `path`. */
34
34
  export const wrote = (path: string): boolean => changes(path).some((c) => c.status !== "deleted")
35
35
 
36
+ /** `check()`'s options beside its report path. */
36
37
  export interface CheckOptions {
37
- /** Where a failing run leaves its output. */
38
- readonly report: string
39
38
  readonly label?: string | undefined
40
39
  /** Paths removed before the command runs. */
41
40
  readonly sweep?: readonly string[] | undefined
@@ -45,17 +44,27 @@ export interface CheckOptions {
45
44
  readonly preamble?: readonly string[] | undefined
46
45
  }
47
46
 
47
+ export type Check = (
48
+ name: string,
49
+ command: string,
50
+ /** Where a failing run leaves its output. */
51
+ report: string,
52
+ options?: CheckOptions,
53
+ ) => Promise<boolean>
54
+
48
55
  /**
49
56
  * A step that runs `command` through the driver and keeps its outcome in the
50
57
  * tree: a failure writes `report`, a pass removes it. Resolves `true` when the
51
58
  * command passed.
52
59
  */
53
- export const check = async (
54
- name: string,
55
- command: string,
56
- options: CheckOptions,
57
- ): Promise<boolean> => {
58
- const { report, label, sweep, sweepOnGreen, preamble } = options
60
+ export const check: Check = async (name, command, report, options = {}) => {
61
+ // A config is evaluated without a type check, so the removed form must fail loudly.
62
+ if (typeof report !== "string") {
63
+ throw new Error(
64
+ `gtd: check("${name}"): the report path is the third argument — check(name, command, report, options?)`,
65
+ )
66
+ }
67
+ const { label, sweep, sweepOnGreen, preamble } = options
59
68
  await run(
60
69
  name,
61
70
  checkScript(command, { report, stamp: head().slice(0, 7), sweep, sweepOnGreen, preamble }),
@@ -220,7 +229,7 @@ export const judgeDiffShape = async (
220
229
 
221
230
  const minP =
222
231
  options.minP ??
223
- (vars.judgeShapeMinP === undefined ? 0.7 : numeric(vars.judgeShapeMinP, Infinity))
232
+ (settings.judgeShapeMinP === undefined ? 0.7 : numeric(settings.judgeShapeMinP, Infinity))
224
233
  const judgment = await judge(name, {
225
234
  questions: [
226
235
  {
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import * as published from "./index.js"
3
+
4
+ describe("@pmelab/gtd/flows", () => {
5
+ it("publishes exactly the authoring surface, no engine-only name among it", () => {
6
+ expect(Object.keys(published).sort()).toEqual(
7
+ [
8
+ "FLOWS_PROTOCOL",
9
+ "FlowsProtocolError",
10
+ "accessFor",
11
+ "agent",
12
+ "answered",
13
+ "applyScript",
14
+ "changes",
15
+ "changesSince",
16
+ "check",
17
+ "checkScript",
18
+ "codeChanges",
19
+ "codeThreads",
20
+ "defineConfig",
21
+ "defineWorkflow",
22
+ "displayPath",
23
+ "envSettings",
24
+ "envSettingsOf",
25
+ "glob",
26
+ "hasThreadFor",
27
+ "head",
28
+ "human",
29
+ "judge",
30
+ "judgeDiffShape",
31
+ "kept",
32
+ "mark",
33
+ "matches",
34
+ "moveScript",
35
+ "numeric",
36
+ "openQuestions",
37
+ "quote",
38
+ "read",
39
+ "refuse",
40
+ "removeScript",
41
+ "requireAnswers",
42
+ "requireProgress",
43
+ "requireReplies",
44
+ "requireRevert",
45
+ "requireThreadsClosed",
46
+ "restart",
47
+ "restoreScript",
48
+ "revertScript",
49
+ "run",
50
+ "scope",
51
+ "seamGate",
52
+ "sectionBodies",
53
+ "sections",
54
+ "settings",
55
+ "settingsOf",
56
+ "showScript",
57
+ "skillsFor",
58
+ "start",
59
+ "threads",
60
+ "workflow",
61
+ "writeScript",
62
+ "wrote",
63
+ ].sort(),
64
+ )
65
+ })
66
+ })
@@ -1,5 +1,109 @@
1
- export * from "./runtime.js"
2
- export * from "./helpers.js"
3
- export * from "./scripts.js"
4
- export * from "./factory.js"
5
- export * from "./config.js"
1
+ // `@pmelab/gtd/flows`: the authoring surface, listed name by name. gtd's own
2
+ // code imports `./internal.js`, which adds what the engine needs.
3
+
4
+ export {
5
+ accessFor,
6
+ agent,
7
+ changes,
8
+ changesSince,
9
+ codeThreads,
10
+ envSettings,
11
+ envSettingsOf,
12
+ FLOWS_PROTOCOL,
13
+ FlowsProtocolError,
14
+ glob,
15
+ head,
16
+ human,
17
+ judge,
18
+ kept,
19
+ mark,
20
+ matches,
21
+ openQuestions,
22
+ read,
23
+ refuse,
24
+ restart,
25
+ run,
26
+ scope,
27
+ sectionBodies,
28
+ sections,
29
+ settings,
30
+ settingsOf,
31
+ skillsFor,
32
+ start,
33
+ threads,
34
+ workflow,
35
+ type AccessDef,
36
+ type AccessExport,
37
+ type AgentOptions,
38
+ type Change,
39
+ type Changes,
40
+ type CodeThreadInfo,
41
+ type Door,
42
+ type Flow,
43
+ type Human,
44
+ type HumanOptions,
45
+ type JudgeAnswer,
46
+ type JudgePrimitive,
47
+ type JudgeQuestion,
48
+ type JudgeQuestionInput,
49
+ type JudgeSpec,
50
+ type Judgment,
51
+ type KeptTip,
52
+ type Mark,
53
+ type ModeName,
54
+ type OpenQuestion,
55
+ type RunOptions,
56
+ type ScopeAccess,
57
+ type ScopeOptions,
58
+ type Section,
59
+ type Settings,
60
+ type SkillsExport,
61
+ type Steering,
62
+ type SteeringOptions,
63
+ type Summary,
64
+ type SummaryContext,
65
+ type ThreadInfo,
66
+ type WorkflowBase,
67
+ type WorkflowDoors,
68
+ type WorkflowOptions,
69
+ } from "./runtime.js"
70
+ export {
71
+ answered,
72
+ check,
73
+ codeChanges,
74
+ displayPath,
75
+ hasThreadFor,
76
+ judgeDiffShape,
77
+ numeric,
78
+ requireAnswers,
79
+ requireProgress,
80
+ requireReplies,
81
+ requireRevert,
82
+ requireThreadsClosed,
83
+ seamGate,
84
+ wrote,
85
+ type Check,
86
+ type CheckOptions,
87
+ type DiffShape,
88
+ type DiffShapeOptions,
89
+ type DiffShapeResult,
90
+ } from "./helpers.js"
91
+ export {
92
+ applyScript,
93
+ checkScript,
94
+ moveScript,
95
+ quote,
96
+ removeScript,
97
+ restoreScript,
98
+ revertScript,
99
+ showScript,
100
+ writeScript,
101
+ type CheckScriptOptions,
102
+ } from "./scripts.js"
103
+ export {
104
+ defineWorkflow,
105
+ type ConfiguredWorkflow,
106
+ type WorkflowBuild,
107
+ type WorkflowFactory,
108
+ } from "./factory.js"
109
+ export { defineConfig, type GtdConfig, type ModeCommands, type UiSettings } from "./config.js"