@pmelab/gtd 59.0.1 → 60.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +37 -33
- 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-07Z_ENch.mjs → testkit-BvecVjQ0.mjs} +570 -390
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +751 -645
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +245 -36
- 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 +1 -1
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +183 -182
- package/src/cli/Cli.ts +164 -186
- 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 +31 -1
- package/src/cli/Program.test.ts +65 -79
- package/src/cli/Program.ts +157 -118
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +3 -3
- 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 +188 -54
- 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/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 +9 -9
- 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 +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- 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 +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +169 -146
- 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/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)
|
|
@@ -539,9 +539,10 @@ describe("summaryRun", () => {
|
|
|
539
539
|
})
|
|
540
540
|
|
|
541
541
|
describe("process marks", () => {
|
|
542
|
-
const SUMMARISED =
|
|
543
|
-
export
|
|
544
|
-
`
|
|
542
|
+
const SUMMARISED = LINEAR.replace(
|
|
543
|
+
"export default",
|
|
544
|
+
`const summary = (it) => it.marks.map((m) => m.name + "=" + m.value).join("|")\n\nexport default`,
|
|
545
|
+
).replaceAll("{ defaults, base }", "{ defaults, base, summary }")
|
|
545
546
|
const opening = (marks: readonly Mark[]): CommitSpec => ({
|
|
546
547
|
actor: "human",
|
|
547
548
|
from: "idle",
|
|
@@ -609,17 +610,18 @@ const withTipMessage = (repo: InMemRepo, edit: (message: string) => string): Git
|
|
|
609
610
|
}
|
|
610
611
|
|
|
611
612
|
describe("summaryFor", () => {
|
|
612
|
-
const SUMMARISED_FLOW = `import { agent, human, mark } from "@pmelab/gtd/flows"
|
|
613
|
+
const SUMMARISED_FLOW = `import { agent, human, mark, workflow } from "@pmelab/gtd/flows"
|
|
613
614
|
|
|
614
|
-
|
|
615
|
+
const summary = (it) => JSON.stringify(it)
|
|
616
|
+
|
|
617
|
+
export default workflow(async () => {
|
|
615
618
|
await human("idle", { message: "idle-message" })
|
|
616
619
|
mark("risk-tier", "high")
|
|
617
620
|
await agent("building", "build-prompt")
|
|
618
621
|
await human("gate", { message: "gate-message" })
|
|
619
622
|
await agent("checking", "check-prompt")
|
|
620
|
-
}
|
|
623
|
+
}, { summary })
|
|
621
624
|
|
|
622
|
-
export const summary = (it) => JSON.stringify(it)
|
|
623
625
|
`
|
|
624
626
|
const landCost = async (repo: InMemRepo, files: Files, cost: number, model: string) => {
|
|
625
627
|
write(repo, files)
|
|
@@ -678,7 +680,7 @@ export const summary = (it) => JSON.stringify(it)
|
|
|
678
680
|
it("returns no prompt, not a failure, when an edited config no longer exports a summary", async () => {
|
|
679
681
|
const { repo } = await finished()
|
|
680
682
|
const run = await provide(summaryRun, repo)
|
|
681
|
-
repo.writeFile("gtd.config.ts", SUMMARISED_FLOW.replace(
|
|
683
|
+
repo.writeFile("gtd.config.ts", SUMMARISED_FLOW.replace(", { summary })", ")"))
|
|
682
684
|
expect(await provide(summaryFor(run), repo)).toEqual({ prompt: undefined })
|
|
683
685
|
})
|
|
684
686
|
|
|
@@ -773,24 +775,24 @@ export default async () => {
|
|
|
773
775
|
})
|
|
774
776
|
|
|
775
777
|
describe("reviewBaseFor", () => {
|
|
776
|
-
const LOOP = `import { agent, head, human, read } from "@pmelab/gtd/flows"
|
|
778
|
+
const LOOP = `import { agent, head, human, read, workflow } from "@pmelab/gtd/flows"
|
|
777
779
|
|
|
778
|
-
|
|
780
|
+
const defaults = { base: "" }
|
|
781
|
+
const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
|
|
782
|
+
|
|
783
|
+
export default workflow(async () => {
|
|
779
784
|
await human("idle")
|
|
780
785
|
while (read("DONE") === undefined) {
|
|
781
786
|
const base = head()
|
|
782
787
|
await human("checkpoint", { base })
|
|
783
788
|
await agent("building", "build-prompt", { base })
|
|
784
789
|
}
|
|
785
|
-
}
|
|
790
|
+
}, { defaults, base })
|
|
786
791
|
|
|
787
|
-
export const review = async () => {
|
|
792
|
+
export const review = workflow(async () => {
|
|
788
793
|
await agent("fixing", "fix-prompt")
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export const defaults = { base: "" }
|
|
794
|
+
}, { defaults, base })
|
|
792
795
|
|
|
793
|
-
export const base = (workflow, vars) => (workflow === "review" ? (vars.base ?? "") : undefined)
|
|
794
796
|
`
|
|
795
797
|
|
|
796
798
|
it("is the process's diff base at a step that names no base", async () => {
|
|
@@ -906,17 +908,18 @@ export default async () => {
|
|
|
906
908
|
})
|
|
907
909
|
|
|
908
910
|
describe("vars", () => {
|
|
909
|
-
const VARS = `import { agent, human } from "@pmelab/gtd/flows"
|
|
911
|
+
const VARS = `import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
910
912
|
|
|
911
|
-
|
|
913
|
+
const defaults = { deployTarget: "npm test", reviewer: "nobody" }
|
|
914
|
+
|
|
915
|
+
export default workflow(async () => {
|
|
912
916
|
await human("idle")
|
|
913
|
-
}
|
|
917
|
+
}, { defaults })
|
|
914
918
|
|
|
915
|
-
export const side = async () => {
|
|
919
|
+
export const side = workflow(async () => {
|
|
916
920
|
await agent("working", "w")
|
|
917
|
-
}
|
|
921
|
+
}, { defaults })
|
|
918
922
|
|
|
919
|
-
export const defaults = { deployTarget: "npm test", reviewer: "nobody" }
|
|
920
923
|
`
|
|
921
924
|
const seeded = () => repoWith(VARS)
|
|
922
925
|
|
|
@@ -960,17 +963,18 @@ export const defaults = { deployTarget: "npm test", reviewer: "nobody" }
|
|
|
960
963
|
})
|
|
961
964
|
|
|
962
965
|
describe("StartFlags", () => {
|
|
963
|
-
const FLAGGED = `import { agent, human } from "@pmelab/gtd/flows"
|
|
966
|
+
const FLAGGED = `import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
964
967
|
|
|
965
|
-
|
|
968
|
+
const defaults = { flag_risk_tier: "0", other: "x" }
|
|
969
|
+
|
|
970
|
+
export default workflow(async () => {
|
|
966
971
|
await human("idle")
|
|
967
|
-
}
|
|
972
|
+
}, { defaults })
|
|
968
973
|
|
|
969
|
-
export const side = async () => {
|
|
974
|
+
export const side = workflow(async () => {
|
|
970
975
|
await agent("working", "w")
|
|
971
|
-
}
|
|
976
|
+
}, { defaults })
|
|
972
977
|
|
|
973
|
-
export const defaults = { flag_risk_tier: "0", other: "x" }
|
|
974
978
|
`
|
|
975
979
|
const withFlags = <A>(
|
|
976
980
|
eff: Effect.Effect<A, Error, RestRequirements>,
|
|
@@ -1004,15 +1008,16 @@ export const defaults = { flag_risk_tier: "0", other: "x" }
|
|
|
1004
1008
|
})
|
|
1005
1009
|
|
|
1006
1010
|
describe("environment settings", () => {
|
|
1007
|
-
const ENVV = `import { agent, human } from "@pmelab/gtd/flows"
|
|
1011
|
+
const ENVV = `import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
1008
1012
|
|
|
1009
|
-
|
|
1013
|
+
const defaults = { floor: "0.7" }
|
|
1014
|
+
const envDefaults = { testCommand: "npm test" }
|
|
1015
|
+
|
|
1016
|
+
export default workflow(async () => {
|
|
1010
1017
|
await human("idle")
|
|
1011
1018
|
await agent("working", "w")
|
|
1012
|
-
}
|
|
1019
|
+
}, { defaults, envDefaults })
|
|
1013
1020
|
|
|
1014
|
-
export const defaults = { floor: "0.7" }
|
|
1015
|
-
export const envDefaults = { testCommand: "npm test" }
|
|
1016
1021
|
`
|
|
1017
1022
|
const seeded = () => repoWith(ENVV)
|
|
1018
1023
|
|
|
@@ -1119,18 +1124,21 @@ describe("noProcessUnderway / restIsIdle", () => {
|
|
|
1119
1124
|
})
|
|
1120
1125
|
|
|
1121
1126
|
describe("judge rests", () => {
|
|
1122
|
-
const JUDGED = (
|
|
1127
|
+
const JUDGED = (
|
|
1128
|
+
budget: string,
|
|
1129
|
+
) => `import { human, judge, read, workflow } from "@pmelab/gtd/flows"
|
|
1123
1130
|
|
|
1124
|
-
|
|
1131
|
+
const defaults = { judgeBudgetBytes: ${JSON.stringify(budget)} }
|
|
1132
|
+
|
|
1133
|
+
export default workflow(async () => {
|
|
1125
1134
|
await human("idle")
|
|
1126
1135
|
await judge("verdict", {
|
|
1127
1136
|
questions: [{ id: "q", primitive: "noul", instructions: "i", criteria: "c" }],
|
|
1128
1137
|
evidence: { log: read("LOG.md") ?? "" },
|
|
1129
1138
|
message: "judge-message",
|
|
1130
1139
|
})
|
|
1131
|
-
}
|
|
1140
|
+
}, { defaults })
|
|
1132
1141
|
|
|
1133
|
-
export const defaults = { judgeBudgetBytes: ${JSON.stringify(budget)} }
|
|
1134
1142
|
`
|
|
1135
1143
|
const LOG = Array.from({ length: 10 }, (_, i) => `line ${i} of the log`).join("\n") + "\n"
|
|
1136
1144
|
|
|
@@ -1315,17 +1323,17 @@ export default async () => {
|
|
|
1315
1323
|
expect(landing.spec.branch).toBeUndefined()
|
|
1316
1324
|
})
|
|
1317
1325
|
|
|
1318
|
-
it("a
|
|
1319
|
-
const repo = repoWith(`import { agent, human } from "@pmelab/gtd/flows"
|
|
1326
|
+
it("a gtd start process's entry is its opening commit", async () => {
|
|
1327
|
+
const repo = repoWith(`import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
1320
1328
|
|
|
1321
1329
|
export default async () => {
|
|
1322
1330
|
await human("idle")
|
|
1323
1331
|
await agent("building", "build-prompt")
|
|
1324
1332
|
}
|
|
1325
1333
|
|
|
1326
|
-
export const side = async () => {
|
|
1334
|
+
export const side = workflow(async () => {
|
|
1327
1335
|
await agent("fixing", "fix-prompt")
|
|
1328
|
-
}
|
|
1336
|
+
})
|
|
1329
1337
|
`)
|
|
1330
1338
|
const opening = await enter(repo, "side")
|
|
1331
1339
|
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
|
*/
|
|
@@ -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"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// gtd's own imports of the flows facade: everything public, plus what only the
|
|
2
|
+
// engine uses.
|
|
3
|
+
export * from "./index.js"
|
|
4
|
+
export { installContext, protocolMismatch, type FlowContext, type StepRequest } from "./runtime.js"
|
|
5
|
+
export { markedOptions } from "./runtime.js"
|
|
6
|
+
export { configure, isFactoryWorkflow, type OptionIssue } from "./factory.js"
|
|
7
|
+
export { configProblems } from "./config.js"
|