@pmelab/gtd 20.2.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/dist/gtd.bundle.mjs +32 -6
- package/package.json +1 -1
- package/src/flows/runtime.test.ts +54 -0
- package/src/flows/runtime.ts +51 -5
package/README.md
CHANGED
|
@@ -501,7 +501,9 @@ The workflow itself is a plain async TypeScript function: a `gtd.config.ts` at
|
|
|
501
501
|
the repository root replaces it, and the pieces the bundled one is built from
|
|
502
502
|
are exported for yours to reuse. See
|
|
503
503
|
[Configuration](https://github.com/pmelab/gtd/blob/main/docs/configuration.md) —
|
|
504
|
-
and remember gtd evaluates that file on every command.
|
|
504
|
+
and remember gtd evaluates that file on every command. A shared workflow package
|
|
505
|
+
bundling its own `@pmelab/gtd` is checked against the running gtd and refused,
|
|
506
|
+
naming which side to upgrade, on a flows protocol mismatch.
|
|
505
507
|
|
|
506
508
|
## License
|
|
507
509
|
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -32266,16 +32266,34 @@ Any runtime works: the contract is \`--json\`/\`--json=<path>\` in, subprocesses
|
|
|
32266
32266
|
out.
|
|
32267
32267
|
`;
|
|
32268
32268
|
const renderBriefing = () => HEADER() + BEAT_PROTOCOL + JSON_FIELD_REFERENCE + DRIVER_OBLIGATIONS + RECOVERY + commandSuite() + EDITOR_INTEGRATION + PREREQUISITES;
|
|
32269
|
-
|
|
32270
|
-
|
|
32269
|
+
var FlowsProtocolError = class extends Error {
|
|
32270
|
+
constructor(engine, flows) {
|
|
32271
|
+
super(`gtd: the workflow speaks flows protocol ${flows}, but the engine installed protocol ${engine} — ${engine < flows ? "upgrade the gtd that runs it" : "upgrade the @pmelab/gtd the workflow imports"}`);
|
|
32272
|
+
this.name = "FlowsProtocolError";
|
|
32273
|
+
}
|
|
32274
|
+
};
|
|
32271
32275
|
const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context");
|
|
32272
|
-
|
|
32276
|
+
const PROTOCOL_KEY = Symbol.for("@pmelab/gtd/flow-protocol");
|
|
32277
|
+
const MISMATCH_KEY = Symbol.for("@pmelab/gtd/flow-protocol-mismatch");
|
|
32278
|
+
/** Engine-only. */
|
|
32273
32279
|
const installContext = (context) => {
|
|
32274
|
-
|
|
32280
|
+
const holder = globalThis;
|
|
32281
|
+
holder[CONTEXT_KEY] = context;
|
|
32282
|
+
holder[PROTOCOL_KEY] = 1;
|
|
32283
|
+
holder[MISMATCH_KEY] = void 0;
|
|
32275
32284
|
};
|
|
32285
|
+
/** Engine-only: the mismatch a facade copy hit during this replay, if any. */
|
|
32286
|
+
const protocolMismatch = () => globalThis[MISMATCH_KEY];
|
|
32276
32287
|
const ctx = () => {
|
|
32277
|
-
const
|
|
32288
|
+
const holder = globalThis;
|
|
32289
|
+
const context = holder[CONTEXT_KEY];
|
|
32278
32290
|
if (context === void 0) throw new Error("gtd: a workflow step or helper was called outside a gtd replay");
|
|
32291
|
+
const protocol = holder[PROTOCOL_KEY] ?? 1;
|
|
32292
|
+
if (protocol !== 1) {
|
|
32293
|
+
const error = new FlowsProtocolError(protocol, 1);
|
|
32294
|
+
holder[MISMATCH_KEY] ??= error;
|
|
32295
|
+
throw error;
|
|
32296
|
+
}
|
|
32279
32297
|
return context;
|
|
32280
32298
|
};
|
|
32281
32299
|
/** An agent turn: `prompt` is emitted, the driver lands the diff. */
|
|
@@ -32553,6 +32571,8 @@ const requireRevert = (edited, base) => {
|
|
|
32553
32571
|
//#endregion
|
|
32554
32572
|
//#region src/flows/index.ts
|
|
32555
32573
|
var flows_exports = /* @__PURE__ */ __exportAll({
|
|
32574
|
+
FLOWS_PROTOCOL: () => 1,
|
|
32575
|
+
FlowsProtocolError: () => FlowsProtocolError,
|
|
32556
32576
|
accessFor: () => accessFor,
|
|
32557
32577
|
agent: () => agent,
|
|
32558
32578
|
answered: () => answered,
|
|
@@ -32572,6 +32592,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
32572
32592
|
moveScript: () => moveScript,
|
|
32573
32593
|
numeric: () => numeric,
|
|
32574
32594
|
openQuestions: () => openQuestions,
|
|
32595
|
+
protocolMismatch: () => protocolMismatch,
|
|
32575
32596
|
quote: () => quote,
|
|
32576
32597
|
read: () => read,
|
|
32577
32598
|
refuse: () => refuse,
|
|
@@ -49496,7 +49517,7 @@ const replay = async (input) => {
|
|
|
49496
49517
|
if (error instanceof Stop) return;
|
|
49497
49518
|
outcome ??= {
|
|
49498
49519
|
kind: "failed",
|
|
49499
|
-
message: `gtd: the workflow threw: ${error instanceof Error ? error.message : String(error)}`
|
|
49520
|
+
message: error instanceof Error && error.name === "FlowsProtocolError" ? error.message : `gtd: the workflow threw: ${error instanceof Error ? error.message : String(error)}`
|
|
49500
49521
|
};
|
|
49501
49522
|
});
|
|
49502
49523
|
const stuck = new Promise((resolve) => setImmediate(resolve));
|
|
@@ -49505,6 +49526,11 @@ const replay = async (input) => {
|
|
|
49505
49526
|
settled,
|
|
49506
49527
|
stuck
|
|
49507
49528
|
]);
|
|
49529
|
+
const mismatch = protocolMismatch();
|
|
49530
|
+
if (mismatch !== void 0) return {
|
|
49531
|
+
kind: "failed",
|
|
49532
|
+
message: mismatch.message
|
|
49533
|
+
};
|
|
49508
49534
|
} finally {
|
|
49509
49535
|
installContext(void 0);
|
|
49510
49536
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|