@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "20.2.0",
3
+ "version": "20.3.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
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
 
@@ -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
- //#endregion
32270
- //#region src/flows/runtime.ts
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
- /** Engine-only: install the replay context a flow run reads. */
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
- globalThis[CONTEXT_KEY] = context;
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 context = globalThis[CONTEXT_KEY];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "20.2.0",
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": {
@@ -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
+ })
@@ -159,20 +159,66 @@ export interface FlowContext {
159
159
  readonly accessFor: (localName: string, file?: string) => AccessDef
160
160
  }
161
161
 
162
- const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context")
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
- type ContextHolder = { [CONTEXT_KEY]?: FlowContext | undefined }
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: install the replay context a flow run reads. */
197
+ /** Engine-only. */
167
198
  export const installContext = (context: FlowContext | undefined): void => {
168
- ;(globalThis as ContextHolder)[CONTEXT_KEY] = context
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 context = (globalThis as ContextHolder)[CONTEXT_KEY]
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