@pmelab/gtd 60.1.0 → 61.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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +7 -0
  3. package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-C9sq0x07.mjs} +1695 -1454
  4. package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
  5. package/dist/gtd.bundle.mjs +1666 -1449
  6. package/dist/gtd.core.mjs +86 -86
  7. package/dist/testkit/gherkin.mjs +1 -1
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/wire.mjs +2 -2
  10. package/package.json +1 -1
  11. package/src/cli/History.test.ts +10 -1
  12. package/src/cli/Metrics.test.ts +10 -1
  13. package/src/cli/Program.test.ts +30 -19
  14. package/src/cli/Program.ts +4 -1
  15. package/src/cli/Strays.ts +3 -2
  16. package/src/edge/Edge.test.ts +78 -25
  17. package/src/edge/Edge.ts +161 -28
  18. package/src/emit/GitScript.ts +21 -3
  19. package/src/emit/index.ts +3 -0
  20. package/src/gherkin/messages.test.ts +1 -0
  21. package/src/gherkin/module-config.feature +1 -0
  22. package/src/gherkin/more-steps.feature +1 -0
  23. package/src/gherkin/testkit.feature +2 -1
  24. package/src/install/Install.ts +9 -2
  25. package/src/replay/Format.test.ts +35 -8
  26. package/src/replay/Format.ts +7 -4
  27. package/src/replay/Replay.test.ts +54 -0
  28. package/src/replay/Replay.ts +61 -17
  29. package/src/replay/Trailers.test.ts +12 -12
  30. package/src/replay/Trailers.ts +35 -20
  31. package/src/replay/Tree.ts +11 -0
  32. package/src/replay/index.ts +3 -0
  33. package/src/step/LandStep.ts +1 -0
  34. package/src/step/RepoSnapshot.ts +2 -0
  35. package/src/step/planStart.test.ts +2 -2
  36. package/src/step/planStep.test.ts +4 -4
  37. package/src/step/planStep.ts +10 -8
  38. package/src/testkit/TestRepo.test.ts +4 -1
  39. package/src/testkit/TestRepo.ts +3 -1
  40. package/src/twins/EmittedScriptRecognizer.ts +32 -1
  41. package/src/twins/GitTiers.ts +23 -0
  42. package/src/twins/InMemRepo.ts +14 -0
  43. package/src/twins/PinBeats.ts +33 -0
  44. package/src/twins/index.ts +1 -0
  45. package/src/wire/BeatDocument.ts +26 -5
  46. package/src/wire/Demand.ts +16 -6
  47. package/src/wire/constants.ts +3 -0
  48. package/src/wire/types.ts +4 -0
@@ -0,0 +1,33 @@
1
+ import { Effect } from "effect"
2
+ import { runCli } from "../cli/index.js"
3
+ import type { PinMark } from "../wire/index.js"
4
+ import { makeCapturingCliIo } from "./cliIo.js"
5
+ import { applyEmittedScript } from "./EmittedScriptRecognizer.js"
6
+ import type { InMemRepo } from "./InMemRepo.js"
7
+
8
+ const gtd = async (repo: InMemRepo, ...args: string[]) => {
9
+ const { io, result } = makeCapturingCliIo(repo)
10
+ await Effect.runPromise(runCli(["node", "gtd.js", ...args], io))
11
+ return result()
12
+ }
13
+
14
+ const apply = (repo: InMemRepo, script: string): void => {
15
+ const outcome = applyEmittedScript(repo, new Map(), script)
16
+ if (!outcome.ok) throw new Error(outcome.error)
17
+ }
18
+
19
+ /**
20
+ * Takes the pending pin or unpin beat — `mark` alone when given — the way a
21
+ * driver does: its script, then its landing. Whether it took one.
22
+ */
23
+ export const takePinBeat = async (repo: InMemRepo, mark?: PinMark): Promise<boolean> => {
24
+ const next = await gtd(repo, "next", "--json")
25
+ if (next.exitCode !== 0) return false
26
+ const beat = JSON.parse(next.stdout) as { readonly pin?: PinMark; readonly content: string }
27
+ if (beat.pin === undefined || (mark !== undefined && beat.pin !== mark)) return false
28
+ apply(repo, beat.content)
29
+ const land = await gtd(repo, "land", "--json")
30
+ if (land.exitCode !== 0) throw new Error(`gtd land exited ${land.exitCode}: ${land.stderr}`)
31
+ apply(repo, (JSON.parse(land.stdout) as { readonly script: string }).script)
32
+ return true
33
+ }
@@ -11,3 +11,4 @@ export { applyFlowScript } from "./FlowScripts.js"
11
11
  export { gitTiers, runGitServiceContract, runGitScriptContract, type GitTier } from "./GitTiers.js"
12
12
  export { strictGitOperations } from "./StrictGitOperations.js"
13
13
  export { fakeGitOperations } from "./FakeGitOperations.js"
14
+ export { takePinBeat } from "./PinBeats.js"
@@ -1,6 +1,14 @@
1
1
  import type { BeatKind, Demand, DemandSession } from "./Demand.js"
2
2
  import type { BeatStatus, NextMatch, StatusChange } from "./BeatStatus.js"
3
- import type { Actor, ApprovalEntry, ModelCost, StateMode, StateName, StepAccess } from "./types.js"
3
+ import type {
4
+ Actor,
5
+ ApprovalEntry,
6
+ ModelCost,
7
+ PinMark,
8
+ StateMode,
9
+ StateName,
10
+ StepAccess,
11
+ } from "./types.js"
4
12
  import { WIRE_SCHEMA } from "./constants.js"
5
13
 
6
14
  /** `gtd next --json`'s `next` key — `null` when landing would commit nothing. */
@@ -27,20 +35,30 @@ const dispatchFieldsOf = (
27
35
  readonly validate: string | undefined
28
36
  readonly skills: readonly string[] | undefined
29
37
  readonly access: StepAccess | undefined
38
+ readonly pin: PinMark | undefined
30
39
  } => {
40
+ const none = {
41
+ session: undefined,
42
+ validate: undefined,
43
+ skills: undefined,
44
+ access: undefined,
45
+ pin: undefined,
46
+ }
31
47
  switch (demand.kind) {
32
48
  case "prompt":
33
49
  return {
50
+ ...none,
34
51
  session: demand.session,
35
52
  validate: demand.validate,
36
53
  skills: demand.skills,
37
54
  access: demand.access,
38
55
  }
56
+ case "script":
57
+ return { ...none, pin: demand.pin }
39
58
  case "capture":
40
59
  case "message":
41
- case "script":
42
60
  case "stalled":
43
- return { session: undefined, validate: undefined, skills: undefined, access: undefined }
61
+ return none
44
62
  default:
45
63
  return assertNeverDemand(demand)
46
64
  }
@@ -57,6 +75,8 @@ export interface BeatDocument {
57
75
  readonly model: string | undefined
58
76
  readonly system: string | undefined
59
77
  readonly validate: string | undefined
78
+ /** On a pin or unpin beat only: what its script does to `.gtd/process.gtdpin`. */
79
+ readonly pin: PinMark | undefined
60
80
  readonly log: string
61
81
  readonly state: StateName
62
82
  readonly actor: Actor
@@ -76,13 +96,13 @@ export interface BeatDocument {
76
96
  }
77
97
 
78
98
  /**
79
- * Flatten a `Demand` plus its `BeatStatus` into the single 25-key document
99
+ * Flatten a `Demand` plus its `BeatStatus` into the single 26-key document
80
100
  * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
81
101
  * `costByModel` are omitted together, exactly when no cost was recorded
82
102
  * (`cost <= 0`).
83
103
  */
84
104
  export const beatDocument = (demand: Demand, status: BeatStatus): BeatDocument => {
85
- const { session, validate, skills, access } = dispatchFieldsOf(demand)
105
+ const { session, validate, skills, access, pin } = dispatchFieldsOf(demand)
86
106
  const hasCost = status.cost > 0
87
107
  return {
88
108
  schema: WIRE_SCHEMA,
@@ -94,6 +114,7 @@ export const beatDocument = (demand: Demand, status: BeatStatus): BeatDocument =
94
114
  model: status.model,
95
115
  system: status.system,
96
116
  validate,
117
+ pin,
97
118
  log: status.log,
98
119
  state: status.state,
99
120
  actor: status.actor,
@@ -1,4 +1,11 @@
1
- import type { Actor, ContentKind, RenderedDemandSource, StateName, StepAccess } from "./types.js"
1
+ import type {
2
+ Actor,
3
+ ContentKind,
4
+ PinMark,
5
+ RenderedDemandSource,
6
+ StateName,
7
+ StepAccess,
8
+ } from "./types.js"
2
9
 
3
10
  /** The whole beat vocabulary a driver acts on — what to DO with a rest, not merely whether dispatching it is safe. */
4
11
  export type BeatKind = "capture" | "message" | "script" | "prompt" | "stalled"
@@ -72,7 +79,7 @@ export interface DemandSession {
72
79
  export type Demand =
73
80
  | { readonly kind: "capture"; readonly content: string }
74
81
  | { readonly kind: "message"; readonly content: string }
75
- | { readonly kind: "script"; readonly content: string }
82
+ | { readonly kind: "script"; readonly content: string; readonly pin?: PinMark }
76
83
  | { readonly kind: "stalled"; readonly content: string }
77
84
  | {
78
85
  readonly kind: "prompt"
@@ -91,7 +98,10 @@ export type Demand =
91
98
  * any other kind.
92
99
  */
93
100
  export const demandOf = (input: {
94
- readonly rendered: Pick<RenderedDemandSource, "actor" | "content" | "state" | "skills" | "access">
101
+ readonly rendered: Pick<
102
+ RenderedDemandSource,
103
+ "actor" | "content" | "state" | "skills" | "access" | "pin"
104
+ >
95
105
  readonly kind: BeatKind
96
106
  readonly session?: DemandSession
97
107
  readonly validate?: string
@@ -102,7 +112,7 @@ export const demandOf = (input: {
102
112
  const skills =
103
113
  rendered.skills !== undefined && rendered.skills.length > 0 ? rendered.skills : undefined
104
114
  const access = rendered.access ?? { read: null, write: null }
105
- return kind === "prompt"
106
- ? { kind, content, session, validate, skills, access }
107
- : { kind, content }
115
+ if (kind === "prompt") return { kind, content, session, validate, skills, access }
116
+ if (kind === "script" && rendered.pin !== undefined) return { kind, content, pin: rendered.pin }
117
+ return { kind, content }
108
118
  }
@@ -20,3 +20,6 @@ export const renderFormat = (fmt: string, ...args: readonly string[]): string =>
20
20
  /** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
21
21
  export const ACCESS_REFUSAL =
22
22
  "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:"
23
+
24
+ /** Where a process keeps its pin: the engine and workflow it runs on, as `gtd bundle` prints them. */
25
+ export const PIN_PATH = ".gtd/process.gtdpin"
package/src/wire/types.ts CHANGED
@@ -10,6 +10,9 @@ export type StateName = string
10
10
  /** The three content kinds a state can carry — exactly one per state. */
11
11
  export type ContentKind = "script" | "prompt" | "message"
12
12
 
13
+ /** What a pin or unpin beat does to the pin; a driver may do it its own way instead of running the script. */
14
+ export type PinMark = "add" | "remove"
15
+
13
16
  /** The name of a steering-file mode. Not a closed vocabulary: the valid set derives from the active definition (`BUILT_IN_MODES` plus whatever `modes:` declares). */
14
17
  export type StateMode = string
15
18
 
@@ -51,4 +54,5 @@ export interface RenderedDemandSource {
51
54
  readonly judge?: string
52
55
  readonly skills?: readonly string[]
53
56
  readonly access?: StepAccess
57
+ readonly pin?: PinMark
54
58
  }