@pmelab/gtd 23.2.0 → 24.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.
Files changed (236) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +44 -11
  3. package/claude/hooks/access.ts +1 -1
  4. package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
  5. package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
  6. package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
  7. package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
  8. package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
  9. package/dist/flows.mjs +525 -0
  10. package/dist/gtd.bundle.mjs +43830 -56044
  11. package/dist/steering.mjs +2 -0
  12. package/dist/testkit.mjs +13006 -0
  13. package/dist/wire.mjs +2 -0
  14. package/dist/workflow-kit.mjs +352 -0
  15. package/dist/workflows/feature.mjs +315 -0
  16. package/dist/workflows/fix.mjs +40 -0
  17. package/dist/workflows/review.mjs +25 -0
  18. package/dist/workflows.mjs +1953 -1724
  19. package/package.json +70 -8
  20. package/skills/authoring/SKILL.md +5 -4
  21. package/src/cli/Cli.test.ts +1329 -0
  22. package/src/cli/Cli.ts +1348 -0
  23. package/src/cli/Program.test.ts +2262 -0
  24. package/src/cli/Program.ts +1331 -0
  25. package/src/cli/ToolCommands.ts +43 -0
  26. package/src/cli/index.ts +14 -0
  27. package/src/edge/Edge.test.ts +1016 -0
  28. package/src/edge/Edge.ts +1225 -0
  29. package/src/edge/index.ts +16 -0
  30. package/src/emit/Emit.test.ts +425 -0
  31. package/src/emit/Emit.ts +139 -0
  32. package/src/emit/GitScript.test.ts +260 -0
  33. package/src/emit/GitScript.ts +131 -0
  34. package/src/emit/ModeContradiction.test.ts +120 -0
  35. package/src/emit/ModeContradiction.ts +66 -0
  36. package/src/emit/OutcomeScript.test.ts +238 -0
  37. package/src/emit/OutcomeScript.ts +73 -0
  38. package/src/emit/SteeringMode.test.ts +274 -0
  39. package/src/emit/SteeringMode.ts +256 -0
  40. package/src/emit/index.ts +40 -0
  41. package/src/flows/factory.ts +5 -1
  42. package/src/flows/runtime.ts +3 -0
  43. package/src/install/ConfigScaffold.ts +19 -0
  44. package/src/install/Install.test.ts +426 -0
  45. package/src/install/Install.ts +634 -0
  46. package/src/install/Version.ts +27 -0
  47. package/src/install/index.ts +3 -0
  48. package/src/platform/CommandRunner.ts +74 -0
  49. package/src/platform/Commentary.test.ts +126 -0
  50. package/src/platform/Commentary.ts +92 -0
  51. package/src/platform/Git.test.ts +50 -0
  52. package/src/platform/Git.ts +552 -0
  53. package/src/platform/Host.ts +42 -0
  54. package/src/platform/RetainedHistory.test.ts +65 -0
  55. package/src/platform/RetainedHistory.ts +34 -0
  56. package/src/platform/Workspace.test.ts +669 -0
  57. package/src/platform/Workspace.ts +631 -0
  58. package/src/platform/WorktreeState.test.ts +162 -0
  59. package/src/platform/WorktreeState.ts +50 -0
  60. package/src/platform/index.ts +22 -0
  61. package/src/replay/Access.test.ts +53 -0
  62. package/src/replay/Access.ts +46 -0
  63. package/src/replay/Format.test.ts +20 -0
  64. package/src/replay/Format.ts +14 -0
  65. package/src/replay/Replay.test.ts +887 -0
  66. package/src/replay/Replay.ts +669 -0
  67. package/src/replay/Trailers.test.ts +117 -0
  68. package/src/replay/Trailers.ts +213 -0
  69. package/src/replay/Tree.test.ts +25 -0
  70. package/src/replay/Tree.ts +45 -0
  71. package/src/replay/index.ts +11 -0
  72. package/src/steering/Blocks.fixture.ts +6 -0
  73. package/src/steering/Blocks.test.ts +167 -0
  74. package/src/steering/Blocks.ts +321 -0
  75. package/src/steering/CodeThreads.test.ts +127 -0
  76. package/src/steering/CodeThreads.ts +197 -0
  77. package/src/steering/Edits.test.ts +19 -0
  78. package/src/steering/Edits.ts +20 -0
  79. package/src/steering/Eol.test.ts +16 -0
  80. package/src/steering/Eol.ts +2 -0
  81. package/src/steering/FORMATS.md +58 -0
  82. package/src/steering/Footnotes.test.ts +977 -0
  83. package/src/steering/Footnotes.ts +661 -0
  84. package/src/steering/Inline.fixture.ts +7 -0
  85. package/src/steering/Inline.test.ts +227 -0
  86. package/src/steering/Inline.ts +195 -0
  87. package/src/steering/MarkdownTree.test.ts +247 -0
  88. package/src/steering/MarkdownTree.ts +260 -0
  89. package/src/steering/Outline.ts +19 -0
  90. package/src/steering/SteeringFormat.test.ts +91 -0
  91. package/src/steering/SteeringFormat.ts +267 -0
  92. package/src/steering/SteeringFormats.test.ts +163 -0
  93. package/src/steering/Threads.ts +163 -0
  94. package/src/steering/contract.test.ts +264 -0
  95. package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
  96. package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
  97. package/src/steering/fixtures/free-form/notes.md +12 -0
  98. package/src/steering/fixtures/free-form/notes.read.json +42 -0
  99. package/src/steering/fixtures/qa/answered.md +23 -0
  100. package/src/steering/fixtures/qa/answered.read.json +119 -0
  101. package/src/steering/fixtures/qa/requirements.md +160 -0
  102. package/src/steering/fixtures/qa/requirements.read.json +475 -0
  103. package/src/steering/fixtures/review/review.md +76 -0
  104. package/src/steering/fixtures/review/review.read.json +237 -0
  105. package/src/steering/fixtures/review/ticked.md +23 -0
  106. package/src/steering/fixtures/review/ticked.read.json +99 -0
  107. package/src/steering/fixtures/review-notes/baseline.md +14 -0
  108. package/src/steering/fixtures/review-notes/noted.md +19 -0
  109. package/src/steering/freeform.test.ts +311 -0
  110. package/src/steering/freeform.ts +148 -0
  111. package/src/steering/index.test.ts +62 -0
  112. package/src/steering/index.ts +140 -0
  113. package/src/steering/qa.test.ts +2160 -0
  114. package/src/steering/qa.ts +1104 -0
  115. package/src/steering/review.test.ts +1829 -0
  116. package/src/steering/review.ts +1180 -0
  117. package/src/step/LandStep.ts +31 -0
  118. package/src/step/RepoSnapshot.ts +29 -0
  119. package/src/step/accessRefusal.test.ts +58 -0
  120. package/src/step/accessRefusal.ts +26 -0
  121. package/src/step/index.ts +5 -0
  122. package/src/step/planStart.test.ts +202 -0
  123. package/src/step/planStart.ts +167 -0
  124. package/src/step/planStep.test.ts +104 -0
  125. package/src/step/planStep.ts +89 -0
  126. package/src/step/snapshot.fixture.ts +28 -0
  127. package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
  128. package/src/testkit/EmittedScriptRecognizer.ts +642 -0
  129. package/src/testkit/FakeGitOperations.ts +95 -0
  130. package/src/testkit/GitTiers.test.ts +11 -0
  131. package/src/testkit/GitTiers.ts +1052 -0
  132. package/src/testkit/InMemRepo.ts +467 -0
  133. package/src/testkit/Layers.test.ts +58 -0
  134. package/src/testkit/Layers.ts +241 -0
  135. package/src/testkit/StrictGitOperations.ts +31 -0
  136. package/src/testkit/TestRepo.test.ts +119 -0
  137. package/src/testkit/TestRepo.ts +274 -0
  138. package/src/testkit/cliIo.ts +36 -0
  139. package/src/testkit/index.ts +9 -0
  140. package/src/testkit/steps.ts +177 -0
  141. package/src/testkit/testkit.feature +65 -0
  142. package/src/wire/BeatDocument.test.ts +419 -0
  143. package/src/wire/BeatDocument.ts +114 -0
  144. package/src/wire/BeatPlain.test.ts +113 -0
  145. package/src/wire/BeatPlain.ts +127 -0
  146. package/src/wire/BeatStatus.test.ts +59 -0
  147. package/src/wire/BeatStatus.ts +81 -0
  148. package/src/wire/Demand.test.ts +142 -0
  149. package/src/wire/Demand.ts +108 -0
  150. package/src/wire/ExitCodes.test.ts +21 -0
  151. package/src/wire/ExitCodes.ts +16 -0
  152. package/src/wire/Glob.ts +29 -0
  153. package/src/wire/Judge.ts +22 -0
  154. package/src/wire/Land.test.ts +120 -0
  155. package/src/wire/Land.ts +75 -0
  156. package/src/wire/Select.test.ts +148 -0
  157. package/src/wire/Select.ts +78 -0
  158. package/src/wire/Sessions.test.ts +59 -0
  159. package/src/wire/Sessions.ts +39 -0
  160. package/src/wire/WireSchema.test.ts +162 -0
  161. package/src/wire/constants.ts +22 -0
  162. package/src/wire/golden/1/beat-capture.json +34 -0
  163. package/src/wire/golden/1/beat-message.json +34 -0
  164. package/src/wire/golden/1/beat-prompt.json +44 -0
  165. package/src/wire/golden/1/beat-script.json +34 -0
  166. package/src/wire/golden/1/beat-stalled.json +34 -0
  167. package/src/wire/golden/1/judge.json +14 -0
  168. package/src/wire/golden/1/land-noop.json +10 -0
  169. package/src/wire/golden/1/land.json +10 -0
  170. package/src/wire/index.ts +12 -0
  171. package/src/wire/types.ts +46 -0
  172. package/src/workflow/Diagnostic.test.ts +93 -0
  173. package/src/workflow/Diagnostic.ts +54 -0
  174. package/src/workflow/SteeringFormats.test.ts +27 -0
  175. package/src/workflow/SteeringFormats.ts +6 -0
  176. package/src/workflow/Workflow.ts +77 -0
  177. package/src/workflow/compile.test.ts +53 -0
  178. package/src/workflow/compile.ts +56 -0
  179. package/src/workflow/discovery.ts +122 -0
  180. package/src/workflow/fingerprint.test.ts +52 -0
  181. package/src/workflow/fingerprint.ts +53 -0
  182. package/src/workflow/index.ts +33 -0
  183. package/src/workflow/load.test.ts +1017 -0
  184. package/src/workflow/load.ts +836 -0
  185. package/src/workflow/skills.test.ts +109 -0
  186. package/src/workflow/skills.ts +90 -0
  187. package/src/workflow/vars.ts +33 -0
  188. package/src/workflow-kit/health.test.ts +30 -0
  189. package/src/workflow-kit/health.ts +86 -0
  190. package/src/workflow-kit/index.ts +7 -0
  191. package/src/workflow-kit/quality.test.ts +17 -0
  192. package/src/workflow-kit/quality.ts +45 -0
  193. package/src/workflow-kit/review.ts +324 -0
  194. package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
  195. package/src/workflows/feature/feature.test.ts +32 -0
  196. package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
  197. package/src/workflows/feature/index.ts +10 -0
  198. package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
  199. package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
  200. package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
  201. package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
  202. package/src/workflows/feature/review.ts +106 -0
  203. package/src/workflows/feature/steps.test.ts +82 -0
  204. package/src/workflows/feature/steps.ts +92 -0
  205. package/src/workflows/fix/fix.feature +80 -0
  206. package/src/workflows/fix/fix.test.ts +74 -0
  207. package/src/workflows/fix/fix.ts +52 -0
  208. package/src/workflows/fix/index.ts +6 -0
  209. package/src/workflows/index.ts +36 -2
  210. package/src/workflows/review/index.ts +6 -0
  211. package/src/workflows/review/review.test.ts +99 -0
  212. package/src/workflows/review/review.ts +33 -0
  213. package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
  214. package/src/workflows/{access.ts → shared/access.ts} +3 -5
  215. package/src/workflows/shared/config.fixture.ts +12 -0
  216. package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
  217. package/src/workflows/{health.ts → shared/health.ts} +38 -55
  218. package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
  219. package/src/workflows/{options.ts → shared/options.ts} +14 -2
  220. package/src/workflows/shared/reserved.ts +11 -0
  221. package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
  222. package/src/workflows/shared/scenarios.ts +35 -0
  223. package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
  224. package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
  225. package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
  226. package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
  227. package/src/workflows/shared/tail.test.ts +205 -0
  228. package/src/workflows/shared/tail.ts +135 -0
  229. package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
  230. package/src/workflows/{text.ts → shared/text.ts} +48 -7
  231. package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
  232. package/src/workflows/shared/workflows.steps.ts +7 -0
  233. package/src/workflows/review.ts +0 -362
  234. /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
  235. /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
  236. /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
@@ -0,0 +1,473 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ //#region src/wire/constants.ts
3
+ /**
4
+ * The top-level `schema` of every JSON document a driver reads. Bumped only
5
+ * when a field is removed or renamed — never for an added one — and never tied
6
+ * to the package version, since a driver can be installed apart from gtd.
7
+ */
8
+ const WIRE_SCHEMA = 1;
9
+ /** The bucket a cost with no `--model` tag is grouped under, kept distinct so a mixed history still totals correctly. */
10
+ const UNATTRIBUTED_MODEL = "unspecified";
11
+ /** The plain-text twin of `OutcomeScript.ts`'s `printfLine`. Lives here so `src/wire/`'s encoders reach it without importing out of the leaf. */
12
+ const renderFormat = (fmt, ...args) => {
13
+ let i = 0;
14
+ return fmt.replace(/%s/g, () => args[i++] ?? "");
15
+ };
16
+ /** The first line of the landing refusal for a turn that wrote outside its `write` access — a stable marker drivers and tests grep for. */
17
+ const ACCESS_REFUSAL = "gtd land: this turn wrote outside its write access — revert these paths, then run `gtd land` again:";
18
+ //#endregion
19
+ //#region src/wire/ExitCodes.ts
20
+ const EXIT_OK = 0;
21
+ /** Something failed that was never a matter of how gtd was invoked — a refusal or an unexpected error. */
22
+ const EXIT_RUNTIME_ERROR = 1;
23
+ /** gtd was invoked wrong — an unknown option/command, bad arity, a scope violation, a decode failure. */
24
+ const EXIT_USAGE_ERROR = 2;
25
+ const EXIT_SIGINT = 130;
26
+ const EXIT_SIGTERM = 143;
27
+ /** Frozen: adding a code is a driver-protocol break, the same as removing a wire field. */
28
+ const EXIT_CODES = /* @__PURE__ */ new Set([
29
+ 0,
30
+ 1,
31
+ 2,
32
+ 130,
33
+ 143
34
+ ]);
35
+ //#endregion
36
+ //#region src/wire/Demand.ts
37
+ /**
38
+ * `BeatKind`'s own member list, as a value — the ONE place the five kinds are
39
+ * enumerated. Every test that needs to loop "every kind" (the JSON document,
40
+ * `DEMAND_BRIEFING`, the plain header) imports this instead of re-typing its
41
+ * own `as const satisfies readonly BeatKind[]` literal, so the type and the
42
+ * loop can never drift apart.
43
+ */
44
+ const BEAT_KINDS = [
45
+ "capture",
46
+ "message",
47
+ "script",
48
+ "prompt",
49
+ "stalled"
50
+ ];
51
+ /**
52
+ * The beat kind for a rest, in precedence order:
53
+ *
54
+ * 1. `stalled` — `Edge.ts`'s `stalledAt` verdict, passed in as a plain
55
+ * boolean (this module never resolves it itself). `stalledAt` already
56
+ * requires a clean tree, so this can never collide with `capture` below.
57
+ * 2. `capture` — a `message` rest with a DIRTY tree: the human already
58
+ * acted; a driver lands it immediately.
59
+ * 3. Otherwise, the content kind verbatim (`script`/`prompt`, or `message`
60
+ * on a clean tree).
61
+ */
62
+ const beatKindOf = (input) => {
63
+ if (input.stalled) return "stalled";
64
+ if (input.contentKind === "message" && input.dirty) return "capture";
65
+ return input.contentKind;
66
+ };
67
+ /**
68
+ * The `stalled` beat's own content: a diagnosis naming the stuck step and
69
+ * the ways out. The first line is shaped so a stderr grep for
70
+ * `stalled at "<state>"` stays a stable substring (see
71
+ * `tests/integration/features/driver-doc.feature`).
72
+ */
73
+ const stallDiagnosis = (state, actor) => `stalled at "${state}": the last gtd(${actor}): ${state} turn landed an empty attempt, the tree is clean, and another dispatch would repeat it.\n\nTwo ways out:\n - sharpen the step's prompt so the turn has something concrete to author\n - pass allowEmpty: true to the step, so an empty turn completes it and the\n flow decides what follows — finishing, or an escalation step after N\n empty turns\n`;
74
+ /**
75
+ * Assemble one `Demand` — the ONLY place a rest's resolved content becomes
76
+ * `Demand.content` (`stallDiagnosis` at `kind === "stalled"`, the rendered
77
+ * content otherwise), and the only place `session`/`validate`/`skills`/`access` are
78
+ * attached, which the `prompt` variant's own type makes impossible to do at
79
+ * any other kind.
80
+ */
81
+ const demandOf = (input) => {
82
+ const { rendered, kind, session, validate } = input;
83
+ const content = kind === "stalled" ? stallDiagnosis(rendered.state, rendered.actor) : rendered.content;
84
+ const skills = rendered.skills !== void 0 && rendered.skills.length > 0 ? rendered.skills : void 0;
85
+ const access = rendered.access ?? {
86
+ read: null,
87
+ write: null
88
+ };
89
+ return kind === "prompt" ? {
90
+ kind,
91
+ content,
92
+ session,
93
+ validate,
94
+ skills,
95
+ access
96
+ } : {
97
+ kind,
98
+ content
99
+ };
100
+ };
101
+ //#endregion
102
+ //#region src/wire/BeatStatus.ts
103
+ /**
104
+ * Assemble one `BeatStatus`. `system` is omitted (not just falsy) when its
105
+ * rendered value is the empty string, unlike `model`: an empty
106
+ * `--system-prompt ""` would silently delete the harness's own default
107
+ * instead of failing loudly.
108
+ */
109
+ const statusOf = (input) => {
110
+ const { rendered, idle, initial, workflow, log, changes, next, cost, costByModel } = input;
111
+ return {
112
+ idle,
113
+ initial,
114
+ model: rendered.model,
115
+ system: rendered.system !== void 0 && rendered.system !== "" ? rendered.system : void 0,
116
+ log,
117
+ state: rendered.state,
118
+ actor: rendered.actor,
119
+ workflow,
120
+ label: rendered.label,
121
+ memory: rendered.memory,
122
+ file: rendered.file,
123
+ mode: rendered.mode,
124
+ changes,
125
+ next,
126
+ cost,
127
+ costByModel,
128
+ judge: rendered.judge
129
+ };
130
+ };
131
+ //#endregion
132
+ //#region src/wire/BeatDocument.ts
133
+ /** `gtd next --json`'s `next` key — `null` when landing would commit nothing. */
134
+ const nextField = (next) => next === null ? null : { target: next.target };
135
+ /** Unreachable in a well-typed call — used as the `default` arm of an exhaustive switch over `Demand["kind"]`, so a future `BeatKind` member that isn't handled fails `tsc`, not just review. */
136
+ const assertNeverDemand = (demand) => {
137
+ throw new Error(`unreachable demand: ${JSON.stringify(demand)}`);
138
+ };
139
+ /**
140
+ * The ONE exhaustive switch translating a `Demand` into its wire
141
+ * `session`/`validate`/`skills`/`access` set — only the `prompt` variant carries
142
+ * any of them, and every other member is listed explicitly (not a
143
+ * `default:` fallthrough) so adding a sixth `BeatKind` without adding its
144
+ * case here fails `tsc` at the `assertNeverDemand` call, not silently
145
+ * falling through to "no session."
146
+ */
147
+ const dispatchFieldsOf = (demand) => {
148
+ switch (demand.kind) {
149
+ case "prompt": return {
150
+ session: demand.session,
151
+ validate: demand.validate,
152
+ skills: demand.skills,
153
+ access: demand.access
154
+ };
155
+ case "capture":
156
+ case "message":
157
+ case "script":
158
+ case "stalled": return {
159
+ session: void 0,
160
+ validate: void 0,
161
+ skills: void 0,
162
+ access: void 0
163
+ };
164
+ default: return assertNeverDemand(demand);
165
+ }
166
+ };
167
+ /**
168
+ * Flatten a `Demand` plus its `BeatStatus` into the single 24-key document
169
+ * `gtd next --json` emits — the ONLY place the two are joined. `cost`/
170
+ * `costByModel` are omitted together, exactly when no cost was recorded
171
+ * (`cost <= 0`).
172
+ */
173
+ const beatDocument = (demand, status) => {
174
+ const { session, validate, skills, access } = dispatchFieldsOf(demand);
175
+ const hasCost = status.cost > 0;
176
+ return {
177
+ schema: 1,
178
+ kind: demand.kind,
179
+ content: demand.content,
180
+ idle: status.idle,
181
+ initial: status.initial,
182
+ session,
183
+ model: status.model,
184
+ system: status.system,
185
+ validate,
186
+ log: status.log,
187
+ state: status.state,
188
+ actor: status.actor,
189
+ workflow: status.workflow,
190
+ label: status.label,
191
+ memory: status.memory,
192
+ file: status.file,
193
+ mode: status.mode,
194
+ changes: status.changes,
195
+ next: nextField(status.next),
196
+ cost: hasCost ? status.cost : void 0,
197
+ costByModel: hasCost ? status.costByModel : void 0,
198
+ judge: status.judge,
199
+ skills,
200
+ access
201
+ };
202
+ };
203
+ const renderBeatJson = (document) => JSON.stringify(document) + "\n";
204
+ //#endregion
205
+ //#region src/wire/BeatPlain.ts
206
+ /**
207
+ * True when the per-model breakdown adds information beyond the `Cost:`
208
+ * total: more than one model, or a single model that carries an actual
209
+ * `--model` tag (a lone `unspecified` bucket just restates the total, so
210
+ * it's suppressed).
211
+ */
212
+ const breakdownIsInformative = (byModel) => byModel.length > 1 || byModel.length === 1 && byModel[0].model !== "unspecified";
213
+ const costStatusLines = (cost, byModel) => {
214
+ if (cost <= 0) return [];
215
+ const lines = [`Cost: ${cost}`];
216
+ if (breakdownIsInformative(byModel)) for (const m of byModel) lines.push(` ${m.model}: ${m.cost}`);
217
+ return lines;
218
+ };
219
+ const pendingStatusLines = (statusChanges) => statusChanges.length === 0 ? ["Pending: (clean)"] : ["Pending:", ...statusChanges.map((c) => ` ${c.status} ${c.path}`)];
220
+ const nextStatusLine = (next) => next === null ? "Next: (nothing would land)" : `Next: → ${next.target}`;
221
+ /** Builds `{[key]: value}` for each entry whose value isn't `undefined` — the shared "omit absent optional fields" shape the header uses. */
222
+ const definedFields = (entries) => {
223
+ const result = {};
224
+ for (const [key, value] of entries) if (value !== void 0) result[key] = value;
225
+ return result;
226
+ };
227
+ /**
228
+ * The header block's own lines — `State:`/`Awaits:`/`Workflow:`/optional `Label:`/
229
+ * `Model:`/`Memory:`/`File:`/`Mode:`/`Cost:`(+breakdown)/`Pending:`/`Next:`,
230
+ * in that order. Used by `renderBeatPlain`'s header-showing kinds below.
231
+ */
232
+ const beatHeaderLines = (document) => {
233
+ const optional = definedFields([
234
+ ["Label", document.label],
235
+ ["Model", document.model],
236
+ ["Memory", document.memory],
237
+ ["File", document.file],
238
+ ["Mode", document.mode]
239
+ ]);
240
+ return [
241
+ `State: ${document.state}`,
242
+ `Awaits: ${document.actor}`,
243
+ `Workflow: ${document.workflow}`,
244
+ ...Object.entries(optional).map(([key, value]) => `${key}: ${value}`),
245
+ ...costStatusLines(document.cost ?? 0, document.costByModel ?? []),
246
+ ...pendingStatusLines(document.changes),
247
+ nextStatusLine(document.next)
248
+ ];
249
+ };
250
+ /**
251
+ * One entry per `BeatKind`, typed as `Record<BeatKind, DemandBriefing>` so
252
+ * the object literal itself is exhaustive: adding or removing a `BeatKind`
253
+ * member without updating this map fails `tsc`, the same guarantee
254
+ * `dispatchFieldsOf`'s switch (`BeatDocument.ts`) gives the JSON side. Exported
255
+ * so the one cross-cutting exhaustiveness test (`BeatDocument.test.ts`) can
256
+ * assert against it directly, rather than re-deriving it from
257
+ * `renderBeatPlain`'s output. Replaces the old `renderBeatPlain`'s hand-written
258
+ * `if (kind === "script") … if (kind === "capture") …` pair.
259
+ */
260
+ const DEMAND_BRIEFING = {
261
+ capture: {
262
+ instructionLine: "The edit is already made — run `gtd land` to land it.",
263
+ suppressHeader: false
264
+ },
265
+ message: {
266
+ instructionLine: void 0,
267
+ suppressHeader: false
268
+ },
269
+ script: {
270
+ instructionLine: "Run this script:",
271
+ suppressHeader: false
272
+ },
273
+ stalled: {
274
+ instructionLine: void 0,
275
+ suppressHeader: false
276
+ },
277
+ prompt: {
278
+ instructionLine: void 0,
279
+ suppressHeader: true
280
+ }
281
+ };
282
+ /**
283
+ * The self-validation instruction appended to a `prompt` beat's plain output
284
+ * only — a structured (`--json`) driver runs the `validate` field
285
+ * itself instead. Advisory either way: `gtd land` refuses a turn whose
286
+ * steering file is invalid regardless of whether this was followed.
287
+ */
288
+ const selfValidateInstruction = (command, file) => `\nBefore finishing your turn, run \`${command}\` — it checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
289
+ /**
290
+ * `gtd next`'s plain-text encoding, driven by `DEMAND_BRIEFING`. At
291
+ * `kind === "prompt"` the status header is suppressed and only `content`
292
+ * plus the self-validation instruction is emitted — those bytes ARE the
293
+ * agent's input, so gtd's own bookkeeping (state name, edges, pending
294
+ * changes) must not be prefixed onto it. Every other kind gets the header
295
+ * plus its instruction line, since its plain output is read by a human or
296
+ * driver, never an agent. `selfValidateCommand` is already resolved; this
297
+ * function never resolves it itself.
298
+ */
299
+ const renderBeatPlain = (document, selfValidateCommand) => {
300
+ const content = document.content.endsWith("\n") ? document.content : document.content + "\n";
301
+ const briefing = DEMAND_BRIEFING[document.kind];
302
+ if (briefing.suppressHeader) return selfValidateCommand !== void 0 && document.file !== void 0 ? content + selfValidateInstruction(selfValidateCommand, document.file) : content;
303
+ const body = beatHeaderLines(document).join("\n") + "\n\n" + content;
304
+ return briefing.instructionLine !== void 0 ? `${briefing.instructionLine}\n` + body : body;
305
+ };
306
+ //#endregion
307
+ //#region src/wire/Land.ts
308
+ /** Assembles one `gtd land` result's fields in `LandFields`' declared order — the ONLY place `gtd land --json`'s wire shape is built. */
309
+ const landFields = (input) => ({
310
+ schema: 1,
311
+ script: input.script,
312
+ settled: input.settled,
313
+ idle: input.idle,
314
+ state: input.state,
315
+ subject: input.subject,
316
+ cost: input.cost,
317
+ model: input.model
318
+ });
319
+ const renderLandJson = (fields) => JSON.stringify(fields) + "\n";
320
+ const FMT_NOOP = "nothing to do at \"%s\"\n";
321
+ const FMT_LAND_PROSE = "commit everything with this message: %s\n";
322
+ /** `nothing to do at "<state>"` — a no-op step's plain-text line, and the text a print-only script's own `printf` carries. */
323
+ const noopText = (state) => renderFormat(FMT_NOOP, state);
324
+ /** `commit everything with this message: <subject>` — plain `gtd land`'s own stdout at a pending diff (no script); `--json` keeps emitting the script itself, unaffected. */
325
+ const landProseText = (subject) => renderFormat(FMT_LAND_PROSE, subject);
326
+ /**
327
+ * `gtd land`'s plain-text encoding — names the commit subject at a real
328
+ * landing and points at `--json=script`, since the script itself is
329
+ * unreachable from plain output; prints the existing no-op note otherwise.
330
+ * `command` names the invocation to point at — `"land"` by default, but
331
+ * `gtd judge answer` (the only other caller of `planLanding`/`landFields`)
332
+ * passes its own two-word name so the hint stays runnable verbatim.
333
+ */
334
+ const renderLandPlain = (fields, command = "land") => fields.subject !== null ? `${landProseText(fields.subject).trimEnd()}\n(run \`gtd ${command} --json=script | sh\` to get the landing script)\n` : noopText(fields.state);
335
+ //#endregion
336
+ //#region src/wire/Judge.ts
337
+ const judgeJson = (state, questions) => JSON.stringify({
338
+ schema: 1,
339
+ state,
340
+ questions
341
+ });
342
+ //#endregion
343
+ //#region src/wire/Select.ts
344
+ /**
345
+ * Renders a fully-walked leaf value: scalars/booleans stringify directly,
346
+ * arrays become one `JSON.stringify` per entry, newline-joined.
347
+ * `selectPath`'s loop already returns `absent` for `undefined`/`null` after
348
+ * every segment (including the last), so neither ever reaches here.
349
+ */
350
+ const toSelection = (value) => {
351
+ if (Array.isArray(value)) return {
352
+ kind: "value",
353
+ text: value.map((entry) => JSON.stringify(entry)).join("\n")
354
+ };
355
+ if (typeof value === "object") return {
356
+ kind: "value",
357
+ text: JSON.stringify(value)
358
+ };
359
+ return {
360
+ kind: "value",
361
+ text: String(value)
362
+ };
363
+ };
364
+ /** A segment that resolves to no key at all (missing from an object, out-of-range or non-numeric against an array) — distinct from a present key holding `undefined`. */
365
+ const NOT_FOUND = Symbol("not-found");
366
+ /** An all-digits segment (`"0"`, `"12"`) — the only shape an array segment resolves against. */
367
+ const ARRAY_INDEX = /^\d+$/;
368
+ /**
369
+ * One step of the walk: looks `segment` up on `current`, or reports
370
+ * `NOT_FOUND` for a key that was never there (a primitive has no keys).
371
+ * Against an array, only an all-digits segment resolves — an in-range index
372
+ * (`skills.0` reads the first declared skill), never a non-index own key
373
+ * (`length`/`map`/every other inherited array member stays `NOT_FOUND`).
374
+ * Against an object, presence is an OWN-property test
375
+ * (`Object.prototype.hasOwnProperty`), never the `in` operator — `in` walks
376
+ * the prototype chain, so it would resolve inherited members
377
+ * (`constructor`, `toString`, `hasOwnProperty`, `valueOf`, ...) as real
378
+ * document fields.
379
+ */
380
+ const resolveSegment = (current, segment) => {
381
+ if (Array.isArray(current)) return ARRAY_INDEX.test(segment) && Number(segment) < current.length ? current[Number(segment)] : NOT_FOUND;
382
+ if (typeof current === "object" && current !== null) {
383
+ const record = current;
384
+ return Object.prototype.hasOwnProperty.call(record, segment) ? record[segment] : NOT_FOUND;
385
+ }
386
+ return NOT_FOUND;
387
+ };
388
+ /**
389
+ * Walks `fields` by dotted key path. Never throws — any unwalkable shape
390
+ * (a primitive mid-path, an out-of-range or non-numeric array segment)
391
+ * degrades to `unknown`, and a present-but-`undefined` value anywhere along
392
+ * the path short-circuits the whole remaining path to `absent` rather than
393
+ * reporting a key that was never actually missing.
394
+ */
395
+ const selectPath = (fields, path) => {
396
+ try {
397
+ let current = fields;
398
+ for (const segment of path.split(".")) {
399
+ current = resolveSegment(current, segment);
400
+ if (current === NOT_FOUND) return {
401
+ kind: "unknown",
402
+ path
403
+ };
404
+ if (current === void 0 || current === null) return { kind: "absent" };
405
+ }
406
+ return toSelection(current);
407
+ } catch {
408
+ return {
409
+ kind: "unknown",
410
+ path
411
+ };
412
+ }
413
+ };
414
+ //#endregion
415
+ //#region src/wire/Sessions.ts
416
+ /** Fixed forever — every derived session id descends from this one constant. */
417
+ const GTD_SESSION_NAMESPACE = "ca4be249-805e-41c1-8b6a-75a11c011e25";
418
+ const toBytes = (uuid) => Buffer.from(uuid.replace(/-/g, ""), "hex");
419
+ const toUuidString = (bytes) => {
420
+ const hex = bytes.toString("hex");
421
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
422
+ };
423
+ /**
424
+ * UUIDv5 (RFC 4122 §4.3), hand-rolled over `node:crypto`'s SHA-1 rather than
425
+ * a dependency — correctness is checked against the RFC's own DNS-namespace
426
+ * test vector in `Sessions.test.ts`.
427
+ */
428
+ const uuidv5 = (namespace, name) => {
429
+ const bytes = createHash("sha1").update(toBytes(namespace)).update(name, "utf8").digest().subarray(0, 16);
430
+ bytes[6] = bytes[6] & 15 | 80;
431
+ bytes[8] = bytes[8] & 63 | 128;
432
+ return toUuidString(bytes);
433
+ };
434
+ /**
435
+ * Resolves a `prompt` rest's session id: with no `memoryKey` there's no
436
+ * history to derive a stable id from, so it mints an ephemeral one and forces
437
+ * `resume: false`; otherwise the id is deterministic (`uuidv5`) and `resume`
438
+ * is passed through as given.
439
+ */
440
+ const resolveSession = (memoryKey, resume, mint = randomUUID) => memoryKey === void 0 ? {
441
+ sessionId: mint(),
442
+ resume: false
443
+ } : {
444
+ sessionId: uuidv5(GTD_SESSION_NAMESPACE, memoryKey),
445
+ resume
446
+ };
447
+ //#endregion
448
+ //#region src/wire/Glob.ts
449
+ const ESCAPE_RE = /[.+^${}()|[\]\\?]/g;
450
+ const compile = (glob) => {
451
+ let pattern = "^";
452
+ let i = 0;
453
+ while (i < glob.length) {
454
+ const char = glob[i];
455
+ if (char !== "*") {
456
+ pattern += char.replace(ESCAPE_RE, "\\$&");
457
+ i += 1;
458
+ } else if (glob[i + 1] !== "*") {
459
+ pattern += "[^/]*";
460
+ i += 1;
461
+ } else if (glob[i + 2] === "/") {
462
+ pattern += "(?:.*/)?";
463
+ i += 3;
464
+ } else {
465
+ pattern += ".*";
466
+ i += 2;
467
+ }
468
+ }
469
+ return new RegExp(`${pattern}$`);
470
+ };
471
+ const globMatches = (path, glob) => compile(glob).test(path);
472
+ //#endregion
473
+ export { EXIT_SIGTERM as C, WIRE_SCHEMA as D, UNATTRIBUTED_MODEL as E, renderFormat as O, EXIT_SIGINT as S, ACCESS_REFUSAL as T, demandOf as _, landFields as a, EXIT_OK as b, renderLandJson as c, renderBeatPlain as d, beatDocument as f, beatKindOf as g, BEAT_KINDS as h, judgeJson as i, renderLandPlain as l, statusOf as m, resolveSession as n, landProseText as o, renderBeatJson as p, selectPath as r, noopText as s, globMatches as t, DEMAND_BRIEFING as u, stallDiagnosis as v, EXIT_USAGE_ERROR as w, EXIT_RUNTIME_ERROR as x, EXIT_CODES as y };