@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +48 -32
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +11285 -19864
- package/dist/gtd.core.mjs +593 -0
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +246 -37
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +7 -7
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +185 -184
- package/src/cli/Cli.ts +219 -224
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +34 -3
- package/src/cli/Program.test.ts +78 -84
- package/src/cli/Program.ts +192 -128
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +72 -62
- package/src/edge/Edge.ts +6 -6
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +189 -55
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/install/Version.ts +9 -3
- package/src/install/index.ts +1 -1
- package/src/platform/CommandRunner.ts +13 -11
- package/src/platform/Commentary.ts +2 -2
- package/src/platform/Git.test.ts +4 -5
- package/src/platform/Git.ts +42 -38
- package/src/platform/Host.ts +1 -1
- package/src/platform/NodeServices.ts +11 -0
- package/src/platform/Workspace.test.ts +4 -5
- package/src/platform/Workspace.ts +3 -3
- package/src/platform/WorktreeState.test.ts +2 -3
- package/src/platform/WorktreeState.ts +1 -1
- package/src/platform/index.ts +2 -1
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +12 -12
- package/src/testkit/TestRepo.test.ts +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +2 -6
- package/src/twins/EmittedScripts.test.ts +5 -3
- package/src/twins/GitTiers.ts +6 -7
- package/src/twins/InMemRepo.ts +1 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/cliIo.ts +9 -4
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +27 -10
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +316 -119
- package/src/workflow/load.ts +478 -305
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
- package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
package/src/edge/Edge.test.ts
CHANGED
|
@@ -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, "
|
|
109
|
-
if (exitCode !== 0) throw new Error(`gtd
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
moved
|
|
475
|
-
|
|
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 =
|
|
543
|
-
export
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
1129
|
+
const JUDGED = (
|
|
1130
|
+
budget: string,
|
|
1131
|
+
) => `import { human, judge, read, workflow } from "@pmelab/gtd/flows"
|
|
1123
1132
|
|
|
1124
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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["
|
|
1436
|
-
workspace: Workspace["
|
|
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/,
|
package/src/flows/factory.ts
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
|
-
import type {
|
|
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
|
|
19
|
+
/** Process settings this workflow declares. */
|
|
11
20
|
readonly defaults?: Readonly<Record<string, string>> | undefined
|
|
12
|
-
/** Environment settings this workflow declares
|
|
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
|
|
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
|
|
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
|
+
})
|
package/src/flows/helpers.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
):
|
|
58
|
-
|
|
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
|
-
(
|
|
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
|
+
})
|
package/src/flows/index.ts
CHANGED
|
@@ -1,5 +1,109 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
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"
|