@pmelab/gtd 20.1.0 → 20.3.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 +3 -1
- package/claude/hooks/register.tsx +27 -30
- package/claude/hooks/schema.ts +52 -0
- package/dist/gtd.bundle.mjs +45988 -45953
- package/package.json +3 -1
- package/src/flows/runtime.test.ts +54 -0
- package/src/flows/runtime.ts +51 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
"*": "oxfmt --no-error-on-unmatched-pattern --write"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
+
"@commitlint/cli": "^21.2.3",
|
|
76
|
+
"@commitlint/config-conventional": "^21.2.3",
|
|
75
77
|
"@effect/language-service": "^0.72.0",
|
|
76
78
|
"@radix-ui/colors": "^3.0.0",
|
|
77
79
|
"@semantic-release/exec": "^7.1.0",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
FLOWS_PROTOCOL,
|
|
4
|
+
FlowsProtocolError,
|
|
5
|
+
head,
|
|
6
|
+
installContext,
|
|
7
|
+
protocolMismatch,
|
|
8
|
+
type FlowContext,
|
|
9
|
+
} from "./runtime.js"
|
|
10
|
+
|
|
11
|
+
afterEach(() => installContext(undefined))
|
|
12
|
+
|
|
13
|
+
const context = { head: () => "h1" } as unknown as FlowContext
|
|
14
|
+
const globals = globalThis as Record<symbol, unknown>
|
|
15
|
+
const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context")
|
|
16
|
+
const PROTOCOL_KEY = Symbol.for("@pmelab/gtd/flow-protocol")
|
|
17
|
+
|
|
18
|
+
describe("the flows protocol handshake", () => {
|
|
19
|
+
it("reads the context when the engine installed the facade's protocol", () => {
|
|
20
|
+
installContext(context)
|
|
21
|
+
expect(head()).toBe("h1")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("tells a newer engine to have the workflow's package upgraded", () => {
|
|
25
|
+
installContext(context)
|
|
26
|
+
globals[PROTOCOL_KEY] = FLOWS_PROTOCOL + 1
|
|
27
|
+
expect(() => head()).toThrow(FlowsProtocolError)
|
|
28
|
+
expect(() => head()).toThrow(
|
|
29
|
+
`gtd: the workflow speaks flows protocol ${FLOWS_PROTOCOL}, but the engine installed protocol ${FLOWS_PROTOCOL + 1} — upgrade the @pmelab/gtd the workflow imports`,
|
|
30
|
+
)
|
|
31
|
+
expect(protocolMismatch()).toBeInstanceOf(FlowsProtocolError)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("tells an older engine to be upgraded", () => {
|
|
35
|
+
installContext(context)
|
|
36
|
+
globals[PROTOCOL_KEY] = FLOWS_PROTOCOL - 1
|
|
37
|
+
expect(() => head()).toThrow(
|
|
38
|
+
`engine installed protocol ${FLOWS_PROTOCOL - 1} — upgrade the gtd that runs it`,
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Must go red once FLOWS_PROTOCOL passes 1: a pre-handshake engine is v1.
|
|
43
|
+
it("accepts an engine that installs no protocol number", () => {
|
|
44
|
+
delete globals[PROTOCOL_KEY]
|
|
45
|
+
globals[CONTEXT_KEY] = context
|
|
46
|
+
expect(head()).toBe("h1")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("keeps the outside-a-replay error when nothing is installed", () => {
|
|
50
|
+
expect(() => head()).toThrow(
|
|
51
|
+
new Error("gtd: a workflow step or helper was called outside a gtd replay"),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
})
|
package/src/flows/runtime.ts
CHANGED
|
@@ -159,20 +159,66 @@ export interface FlowContext {
|
|
|
159
159
|
readonly accessFor: (localName: string, file?: string) => AccessDef
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Bump only when the installed `FlowContext` object changes shape or semantics;
|
|
164
|
+
* type-only authoring-API changes don't count. A bump past 1 must keep v1
|
|
165
|
+
* members working or move the context to a new symbol key — pre-handshake
|
|
166
|
+
* facades never read the protocol.
|
|
167
|
+
*/
|
|
168
|
+
export const FLOWS_PROTOCOL = 1
|
|
169
|
+
|
|
170
|
+
export class FlowsProtocolError extends Error {
|
|
171
|
+
constructor(engine: number, flows: number) {
|
|
172
|
+
const upgrade =
|
|
173
|
+
engine < flows
|
|
174
|
+
? "upgrade the gtd that runs it"
|
|
175
|
+
: "upgrade the @pmelab/gtd the workflow imports"
|
|
176
|
+
super(
|
|
177
|
+
`gtd: the workflow speaks flows protocol ${flows}, but the engine installed protocol ${engine} — ${upgrade}`,
|
|
178
|
+
)
|
|
179
|
+
this.name = "FlowsProtocolError"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
163
182
|
|
|
164
|
-
|
|
183
|
+
const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context")
|
|
184
|
+
// Separate from the context so a facade older than the protocol still reads
|
|
185
|
+
// the context it expects.
|
|
186
|
+
const PROTOCOL_KEY = Symbol.for("@pmelab/gtd/flow-protocol")
|
|
187
|
+
// The first mismatch, kept for the engine: workflow code may catch what ctx()
|
|
188
|
+
// throws, and the refusal must still win.
|
|
189
|
+
const MISMATCH_KEY = Symbol.for("@pmelab/gtd/flow-protocol-mismatch")
|
|
190
|
+
|
|
191
|
+
type ContextHolder = {
|
|
192
|
+
[CONTEXT_KEY]?: FlowContext | undefined
|
|
193
|
+
[PROTOCOL_KEY]?: number | undefined
|
|
194
|
+
[MISMATCH_KEY]?: Error | undefined
|
|
195
|
+
}
|
|
165
196
|
|
|
166
|
-
/** Engine-only
|
|
197
|
+
/** Engine-only. */
|
|
167
198
|
export const installContext = (context: FlowContext | undefined): void => {
|
|
168
|
-
|
|
199
|
+
const holder = globalThis as ContextHolder
|
|
200
|
+
holder[CONTEXT_KEY] = context
|
|
201
|
+
holder[PROTOCOL_KEY] = FLOWS_PROTOCOL
|
|
202
|
+
holder[MISMATCH_KEY] = undefined
|
|
169
203
|
}
|
|
170
204
|
|
|
205
|
+
/** Engine-only: the mismatch a facade copy hit during this replay, if any. */
|
|
206
|
+
export const protocolMismatch = (): Error | undefined => (globalThis as ContextHolder)[MISMATCH_KEY]
|
|
207
|
+
|
|
171
208
|
const ctx = (): FlowContext => {
|
|
172
|
-
const
|
|
209
|
+
const holder = globalThis as ContextHolder
|
|
210
|
+
const context = holder[CONTEXT_KEY]
|
|
173
211
|
if (context === undefined) {
|
|
174
212
|
throw new Error("gtd: a workflow step or helper was called outside a gtd replay")
|
|
175
213
|
}
|
|
214
|
+
// An engine older than the handshake installs no number but the context
|
|
215
|
+
// protocol 1 describes.
|
|
216
|
+
const protocol = holder[PROTOCOL_KEY] ?? 1
|
|
217
|
+
if (protocol !== FLOWS_PROTOCOL) {
|
|
218
|
+
const error = new FlowsProtocolError(protocol, FLOWS_PROTOCOL)
|
|
219
|
+
holder[MISMATCH_KEY] ??= error
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
176
222
|
return context
|
|
177
223
|
}
|
|
178
224
|
|