@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { isBuiltin } from "node:module"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { Effect, Exit } from "effect"
|
|
6
|
+
import { parseAst } from "rolldown/parseAst"
|
|
7
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
8
|
+
import { PIN_KEY } from "../install/index.js"
|
|
9
|
+
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
10
|
+
import { ConfigDiscovery, ConfigService } from "./index.js"
|
|
11
|
+
import { EngineCore, pin, readPin } from "./pin.js"
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
delete (globalThis as { [PIN_KEY]?: unknown })[PIN_KEY]
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// A pin must run with nothing installed: the engine imports Node and nothing else.
|
|
18
|
+
describe.each([
|
|
19
|
+
["live", EngineCore.Live],
|
|
20
|
+
["in memory", testLayers(new InMemRepo())],
|
|
21
|
+
])("EngineCore (%s)", (_, layer) => {
|
|
22
|
+
const source = () =>
|
|
23
|
+
Effect.runPromise(
|
|
24
|
+
Effect.provide(
|
|
25
|
+
Effect.flatMap(EngineCore, (e) => e.source),
|
|
26
|
+
layer,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
it("serves an ES module importing only Node's builtins", async () => {
|
|
31
|
+
const imported = parseAst(await source()).body.flatMap((node) =>
|
|
32
|
+
"source" in node && node.source !== null && node.source !== undefined
|
|
33
|
+
? [String(node.source.value)]
|
|
34
|
+
: [],
|
|
35
|
+
)
|
|
36
|
+
expect(imported.filter((id) => !isBuiltin(id))).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("EngineCore.layer", () => {
|
|
41
|
+
it("names the engine it cannot find", async () => {
|
|
42
|
+
const exit = await Effect.runPromiseExit(
|
|
43
|
+
Effect.provide(
|
|
44
|
+
Effect.flatMap(EngineCore, (e) => e.source),
|
|
45
|
+
EngineCore.layer(() => "/nowhere/gtd.core.mjs"),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
expect(Exit.isFailure(exit) && String(exit.cause)).toContain(
|
|
49
|
+
"no engine at /nowhere/gtd.core.mjs",
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const CONFIG = `import { human } from "@pmelab/gtd/flows"
|
|
55
|
+
import { message } from "./lib/message.js"
|
|
56
|
+
|
|
57
|
+
export default async () => {
|
|
58
|
+
await human("idle", { message })
|
|
59
|
+
}
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
const pinFrom = (repo: InMemRepo) =>
|
|
63
|
+
Effect.runPromise(
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const module = yield* (yield* ConfigDiscovery).workflowModule("/repo", "/repo")
|
|
66
|
+
return yield* pin(module!, "/repo", "1.2.3")
|
|
67
|
+
}).pipe(Effect.provide(testLayers(repo))),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
describe("pin", () => {
|
|
71
|
+
it("leads with a plain header, then the workflow and the engine gzipped", async () => {
|
|
72
|
+
const repo = new InMemRepo()
|
|
73
|
+
repo.writeFile("gtd.config.ts", CONFIG)
|
|
74
|
+
repo.writeFile("lib/message.ts", `export const message = "pinned message"\n`)
|
|
75
|
+
const bytes = await pinFrom(repo)
|
|
76
|
+
|
|
77
|
+
expect(Buffer.from(bytes).toString("latin1").split("\n")[0]).toBe("gtdpin 1 gtd 1.2.3")
|
|
78
|
+
const { format, version, source } = readPin(bytes)
|
|
79
|
+
expect({ format, version }).toEqual({ format: 1, version: "1.2.3" })
|
|
80
|
+
expect(source).toContain("pinned message")
|
|
81
|
+
expect(source.endsWith('import "node:process"\n')).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("registers a workflow load runs without any config", async () => {
|
|
85
|
+
const repo = new InMemRepo()
|
|
86
|
+
repo.writeFile("gtd.config.ts", CONFIG)
|
|
87
|
+
repo.writeFile("lib/message.ts", `export const message = "pinned message"\n`)
|
|
88
|
+
const { source } = readPin(await pinFrom(repo))
|
|
89
|
+
repo.writeFile("gtd.config.ts", "")
|
|
90
|
+
repo.seedCommit("chore: init")
|
|
91
|
+
|
|
92
|
+
await import(`data:text/javascript,${encodeURIComponent(source)}`)
|
|
93
|
+
const config = await Effect.runPromise(
|
|
94
|
+
Effect.flatMap(ConfigService, (c) => c.load).pipe(Effect.provide(testLayers(repo))),
|
|
95
|
+
)
|
|
96
|
+
expect(config.defaultWorkflow.def.initial).toBe("idle")
|
|
97
|
+
expect(Object.keys(config.fingerprint).sort()).toEqual(["gtd.config.ts", "lib/message.ts"])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe("from disk", () => {
|
|
101
|
+
let dir: string
|
|
102
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }))
|
|
103
|
+
|
|
104
|
+
const pinDir = (config: string) => {
|
|
105
|
+
writeFileSync(join(dir, "gtd.config.ts"), config)
|
|
106
|
+
return Effect.runPromiseExit(
|
|
107
|
+
pin(
|
|
108
|
+
{
|
|
109
|
+
filepath: join(dir, "gtd.config.ts"),
|
|
110
|
+
source: readFileSync(join(dir, "gtd.config.ts"), "utf8"),
|
|
111
|
+
},
|
|
112
|
+
dir,
|
|
113
|
+
"1.2.3",
|
|
114
|
+
).pipe(Effect.provide(testLayers(new InMemRepo()))),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const pkg = (name: string, files: Record<string, string>) => {
|
|
119
|
+
const root = join(dir, "node_modules", name)
|
|
120
|
+
for (const [file, content] of Object.entries({
|
|
121
|
+
"package.json": JSON.stringify({ name, main: "index.js" }),
|
|
122
|
+
...files,
|
|
123
|
+
})) {
|
|
124
|
+
mkdirSync(join(root, file, ".."), { recursive: true })
|
|
125
|
+
writeFileSync(join(root, file), content)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it("carries a CommonJS package", async () => {
|
|
130
|
+
dir = mkdtempSync(join(tmpdir(), "gtd-pin-"))
|
|
131
|
+
pkg("shout", { "index.js": "module.exports = (s) => s.toUpperCase() + '!pinned-cjs'" })
|
|
132
|
+
const exit = await pinDir(
|
|
133
|
+
`import shout from "shout"\nexport default async () => shout("x")\n`,
|
|
134
|
+
)
|
|
135
|
+
expect(Exit.isSuccess(exit) && readPin(exit.value).source).toContain("!pinned-cjs")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("refuses a package node-gyp builds, naming it", async () => {
|
|
139
|
+
dir = mkdtempSync(join(tmpdir(), "gtd-pin-"))
|
|
140
|
+
pkg("sqlish", { "index.js": "module.exports = {}", "binding.gyp": "{}" })
|
|
141
|
+
const exit = await pinDir(`import "sqlish"\nexport default async () => {}\n`)
|
|
142
|
+
expect(Exit.isFailure(exit) && String(exit.cause)).toContain(
|
|
143
|
+
'package "sqlish" loads a native addon',
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("refuses a native addon, naming its package", async () => {
|
|
148
|
+
dir = mkdtempSync(join(tmpdir(), "gtd-pin-"))
|
|
149
|
+
pkg("@scope/native", {
|
|
150
|
+
"index.js": 'module.exports = require("./build/addon.node")',
|
|
151
|
+
"build/addon.node": "",
|
|
152
|
+
})
|
|
153
|
+
const exit = await pinDir(`import "@scope/native"\nexport default async () => {}\n`)
|
|
154
|
+
expect(Exit.isFailure(exit) && String(exit.cause)).toContain(
|
|
155
|
+
'package "@scope/native" loads a native addon',
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { gunzipSync, gzipSync } from "node:zlib"
|
|
4
|
+
import { Context, Effect, Layer } from "effect"
|
|
5
|
+
import { GtdError } from "../platform/index.js"
|
|
6
|
+
import { ownPackage, pinnedWorkflowSource } from "./load.js"
|
|
7
|
+
import type { WorkflowModule } from "./discovery.js"
|
|
8
|
+
|
|
9
|
+
/** Bumped when a launcher could no longer run an older pin the same way. */
|
|
10
|
+
const PIN_FORMAT = 1
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The engine a pin carries, as ES module source. Live, the `gtd.core.mjs` the
|
|
14
|
+
* running gtd ships beside its own bundle.
|
|
15
|
+
*/
|
|
16
|
+
export class EngineCore extends Context.Service<
|
|
17
|
+
EngineCore,
|
|
18
|
+
{ readonly source: Effect.Effect<string, Error> }
|
|
19
|
+
>()("EngineCore") {
|
|
20
|
+
static layer = (path: () => string): Layer.Layer<EngineCore> =>
|
|
21
|
+
Layer.succeed(EngineCore, {
|
|
22
|
+
source: Effect.suspend(() => {
|
|
23
|
+
const file = path()
|
|
24
|
+
return Effect.try({
|
|
25
|
+
try: () => readFileSync(file, "utf8"),
|
|
26
|
+
catch: () => new GtdError(`gtd bundle: no engine at ${file} — build gtd first`),
|
|
27
|
+
})
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
static Live = EngineCore.layer(() => join(ownPackage().root, "dist", "gtd.core.mjs"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A pin: one plain-text header line a launcher reads without unpacking, then
|
|
35
|
+
* the gzipped ES module — the workflow registering itself, then the engine.
|
|
36
|
+
* Gzipped, git sees binary and diffs no source.
|
|
37
|
+
*/
|
|
38
|
+
export const pin = (
|
|
39
|
+
module: WorkflowModule,
|
|
40
|
+
root: string,
|
|
41
|
+
version: string,
|
|
42
|
+
): Effect.Effect<Uint8Array, Error, EngineCore> =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const core = yield* (yield* EngineCore).source
|
|
45
|
+
const workflow = yield* Effect.tryPromise({
|
|
46
|
+
try: () => pinnedWorkflowSource(module, root, version),
|
|
47
|
+
catch: (e) =>
|
|
48
|
+
new GtdError(
|
|
49
|
+
`gtd bundle: ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`,
|
|
50
|
+
),
|
|
51
|
+
})
|
|
52
|
+
// The registration runs first; a shebang is only valid on a file's first line.
|
|
53
|
+
const source = workflow + core.replace(/^#!.*\n/, "")
|
|
54
|
+
return Buffer.concat([
|
|
55
|
+
Buffer.from(`gtdpin ${PIN_FORMAT} gtd ${version}\n`),
|
|
56
|
+
gzipSync(source, { level: 9 }),
|
|
57
|
+
])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/** A pin's header and module source. */
|
|
61
|
+
export const readPin = (
|
|
62
|
+
bytes: Uint8Array,
|
|
63
|
+
): { readonly format: number; readonly version: string; readonly source: string } => {
|
|
64
|
+
const end = bytes.indexOf(0x0a)
|
|
65
|
+
const header = /^gtdpin (\d+) gtd (\S+)$/.exec(Buffer.from(bytes.subarray(0, end)).toString())
|
|
66
|
+
if (end < 0 || header === null) throw new Error("not a gtd pin")
|
|
67
|
+
return {
|
|
68
|
+
format: Number(header[1]),
|
|
69
|
+
version: header[2]!,
|
|
70
|
+
source: gunzipSync(bytes.subarray(end + 1)).toString("utf8"),
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/workflow/vars.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { Context } from "effect"
|
|
|
3
3
|
const PREFIX = "GTD_"
|
|
4
4
|
|
|
5
5
|
/** The live release flags as `flag_<name>` settings, provided by the CLI; absent means no flags. */
|
|
6
|
-
export
|
|
7
|
-
defaultValue: ()
|
|
8
|
-
})
|
|
6
|
+
export const StartFlags = Context.Reference<Readonly<Record<string, string>>>("StartFlags", {
|
|
7
|
+
defaultValue: () => ({}),
|
|
8
|
+
})
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* One kind of setting, later wins: the workflow's own defaults, `--var`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import * as kit from "./index.js"
|
|
3
|
+
|
|
4
|
+
describe("@pmelab/gtd/workflow-kit", () => {
|
|
5
|
+
it("publishes its helpers by name", () => {
|
|
6
|
+
expect(Object.keys(kit).sort()).toEqual(
|
|
7
|
+
[
|
|
8
|
+
"assertionLines",
|
|
9
|
+
"escalation",
|
|
10
|
+
"fixFindings",
|
|
11
|
+
"gate",
|
|
12
|
+
"healthy",
|
|
13
|
+
"lensEntriesOf",
|
|
14
|
+
"lensOf",
|
|
15
|
+
"lensScopes",
|
|
16
|
+
"lensesOf",
|
|
17
|
+
"qualityLap",
|
|
18
|
+
"removedAssertions",
|
|
19
|
+
"review",
|
|
20
|
+
"reviewToHandOff",
|
|
21
|
+
"untilSettled",
|
|
22
|
+
].sort(),
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
// Building blocks workflows share. Policy-free: every skill list,
|
|
2
2
|
// lens, prompt, step, path and threshold arrives as a parameter, so a new
|
|
3
3
|
// workflow composes these without inheriting the shipped one's choices.
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export {
|
|
5
|
+
escalation,
|
|
6
|
+
gate,
|
|
7
|
+
healthy,
|
|
8
|
+
untilSettled,
|
|
9
|
+
type EscalationCount,
|
|
10
|
+
type EscalationSteps,
|
|
11
|
+
type Health,
|
|
12
|
+
type RetryVerdict,
|
|
13
|
+
} from "./health.js"
|
|
14
|
+
export {
|
|
15
|
+
fixFindings,
|
|
16
|
+
lensEntriesOf,
|
|
17
|
+
lensOf,
|
|
18
|
+
lensesOf,
|
|
19
|
+
lensScopes,
|
|
20
|
+
qualityLap,
|
|
21
|
+
type FindingsFix,
|
|
22
|
+
type LensEntry,
|
|
23
|
+
} from "./quality.js"
|
|
24
|
+
export {
|
|
25
|
+
assertionLines,
|
|
26
|
+
removedAssertions,
|
|
27
|
+
type Assertion,
|
|
28
|
+
type RemovedAssertions,
|
|
29
|
+
} from "./scenarios.js"
|
|
30
|
+
export {
|
|
31
|
+
review,
|
|
32
|
+
reviewToHandOff,
|
|
33
|
+
type CollectRound,
|
|
34
|
+
type HandOffPolicy,
|
|
35
|
+
type Requested,
|
|
36
|
+
type ReviewOutcome,
|
|
37
|
+
type ReviewPolicy,
|
|
38
|
+
type ReviewRisk,
|
|
39
|
+
type Verdict,
|
|
40
|
+
} from "./review.js"
|
|
@@ -195,13 +195,13 @@ const converse = async (
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/** What `finish` hands back to `review`: an outcome, or another lap at the gate. */
|
|
198
|
-
type Finish =
|
|
198
|
+
export type Finish =
|
|
199
199
|
| ReviewOutcome
|
|
200
200
|
| { readonly next: "await"; readonly reviewed: string }
|
|
201
201
|
| { readonly next: "rereview"; readonly carry: string | undefined }
|
|
202
202
|
|
|
203
203
|
/** What a round's routing needs from `finish`: its closing step and the ways it ends. */
|
|
204
|
-
interface Round {
|
|
204
|
+
export interface Round {
|
|
205
205
|
readonly round: string
|
|
206
206
|
readonly close: () => Promise<void>
|
|
207
207
|
readonly outcome: (verdict: "signoff" | "feedback") => ReviewOutcome
|