@pmelab/gtd 24.0.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 (196) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -5
  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 +43339 -55575
  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 +8 -6
  19. package/package.json +69 -8
  20. package/src/cli/Cli.test.ts +1329 -0
  21. package/src/cli/Cli.ts +1348 -0
  22. package/src/cli/Program.test.ts +2262 -0
  23. package/src/cli/Program.ts +1331 -0
  24. package/src/cli/ToolCommands.ts +43 -0
  25. package/src/cli/index.ts +14 -0
  26. package/src/edge/Edge.test.ts +1016 -0
  27. package/src/edge/Edge.ts +1225 -0
  28. package/src/edge/index.ts +16 -0
  29. package/src/emit/Emit.test.ts +425 -0
  30. package/src/emit/Emit.ts +139 -0
  31. package/src/emit/GitScript.test.ts +260 -0
  32. package/src/emit/GitScript.ts +131 -0
  33. package/src/emit/ModeContradiction.test.ts +120 -0
  34. package/src/emit/ModeContradiction.ts +66 -0
  35. package/src/emit/OutcomeScript.test.ts +238 -0
  36. package/src/emit/OutcomeScript.ts +73 -0
  37. package/src/emit/SteeringMode.test.ts +274 -0
  38. package/src/emit/SteeringMode.ts +256 -0
  39. package/src/emit/index.ts +40 -0
  40. package/src/install/ConfigScaffold.ts +19 -0
  41. package/src/install/Install.test.ts +426 -0
  42. package/src/install/Install.ts +634 -0
  43. package/src/install/Version.ts +27 -0
  44. package/src/install/index.ts +3 -0
  45. package/src/platform/CommandRunner.ts +74 -0
  46. package/src/platform/Commentary.test.ts +126 -0
  47. package/src/platform/Commentary.ts +92 -0
  48. package/src/platform/Git.test.ts +50 -0
  49. package/src/platform/Git.ts +552 -0
  50. package/src/platform/Host.ts +42 -0
  51. package/src/platform/RetainedHistory.test.ts +65 -0
  52. package/src/platform/RetainedHistory.ts +34 -0
  53. package/src/platform/Workspace.test.ts +669 -0
  54. package/src/platform/Workspace.ts +631 -0
  55. package/src/platform/WorktreeState.test.ts +162 -0
  56. package/src/platform/WorktreeState.ts +50 -0
  57. package/src/platform/index.ts +22 -0
  58. package/src/replay/Access.test.ts +53 -0
  59. package/src/replay/Access.ts +46 -0
  60. package/src/replay/Format.test.ts +20 -0
  61. package/src/replay/Format.ts +14 -0
  62. package/src/replay/Replay.test.ts +887 -0
  63. package/src/replay/Replay.ts +669 -0
  64. package/src/replay/Trailers.test.ts +117 -0
  65. package/src/replay/Trailers.ts +213 -0
  66. package/src/replay/Tree.test.ts +25 -0
  67. package/src/replay/Tree.ts +45 -0
  68. package/src/replay/index.ts +11 -0
  69. package/src/steering/Blocks.fixture.ts +6 -0
  70. package/src/steering/Blocks.test.ts +167 -0
  71. package/src/steering/Blocks.ts +321 -0
  72. package/src/steering/CodeThreads.test.ts +127 -0
  73. package/src/steering/CodeThreads.ts +197 -0
  74. package/src/steering/Edits.test.ts +19 -0
  75. package/src/steering/Edits.ts +20 -0
  76. package/src/steering/Eol.test.ts +16 -0
  77. package/src/steering/Eol.ts +2 -0
  78. package/src/steering/FORMATS.md +58 -0
  79. package/src/steering/Footnotes.test.ts +977 -0
  80. package/src/steering/Footnotes.ts +661 -0
  81. package/src/steering/Inline.fixture.ts +7 -0
  82. package/src/steering/Inline.test.ts +227 -0
  83. package/src/steering/Inline.ts +195 -0
  84. package/src/steering/MarkdownTree.test.ts +247 -0
  85. package/src/steering/MarkdownTree.ts +260 -0
  86. package/src/steering/Outline.ts +19 -0
  87. package/src/steering/SteeringFormat.test.ts +91 -0
  88. package/src/steering/SteeringFormat.ts +267 -0
  89. package/src/steering/SteeringFormats.test.ts +163 -0
  90. package/src/steering/Threads.ts +163 -0
  91. package/src/steering/contract.test.ts +264 -0
  92. package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
  93. package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
  94. package/src/steering/fixtures/free-form/notes.md +12 -0
  95. package/src/steering/fixtures/free-form/notes.read.json +42 -0
  96. package/src/steering/fixtures/qa/answered.md +23 -0
  97. package/src/steering/fixtures/qa/answered.read.json +119 -0
  98. package/src/steering/fixtures/qa/requirements.md +160 -0
  99. package/src/steering/fixtures/qa/requirements.read.json +475 -0
  100. package/src/steering/fixtures/review/review.md +76 -0
  101. package/src/steering/fixtures/review/review.read.json +237 -0
  102. package/src/steering/fixtures/review/ticked.md +23 -0
  103. package/src/steering/fixtures/review/ticked.read.json +99 -0
  104. package/src/steering/fixtures/review-notes/baseline.md +14 -0
  105. package/src/steering/fixtures/review-notes/noted.md +19 -0
  106. package/src/steering/freeform.test.ts +311 -0
  107. package/src/steering/freeform.ts +148 -0
  108. package/src/steering/index.test.ts +62 -0
  109. package/src/steering/index.ts +140 -0
  110. package/src/steering/qa.test.ts +2160 -0
  111. package/src/steering/qa.ts +1104 -0
  112. package/src/steering/review.test.ts +1829 -0
  113. package/src/steering/review.ts +1180 -0
  114. package/src/step/LandStep.ts +31 -0
  115. package/src/step/RepoSnapshot.ts +29 -0
  116. package/src/step/accessRefusal.test.ts +58 -0
  117. package/src/step/accessRefusal.ts +26 -0
  118. package/src/step/index.ts +5 -0
  119. package/src/step/planStart.test.ts +202 -0
  120. package/src/step/planStart.ts +167 -0
  121. package/src/step/planStep.test.ts +104 -0
  122. package/src/step/planStep.ts +89 -0
  123. package/src/step/snapshot.fixture.ts +28 -0
  124. package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
  125. package/src/testkit/EmittedScriptRecognizer.ts +642 -0
  126. package/src/testkit/FakeGitOperations.ts +95 -0
  127. package/src/testkit/GitTiers.test.ts +11 -0
  128. package/src/testkit/GitTiers.ts +1052 -0
  129. package/src/testkit/InMemRepo.ts +467 -0
  130. package/src/testkit/Layers.test.ts +58 -0
  131. package/src/testkit/Layers.ts +241 -0
  132. package/src/testkit/StrictGitOperations.ts +31 -0
  133. package/src/testkit/TestRepo.test.ts +119 -0
  134. package/src/testkit/TestRepo.ts +274 -0
  135. package/src/testkit/cliIo.ts +36 -0
  136. package/src/testkit/index.ts +9 -0
  137. package/src/testkit/steps.ts +177 -0
  138. package/src/testkit/testkit.feature +65 -0
  139. package/src/wire/BeatDocument.test.ts +419 -0
  140. package/src/wire/BeatDocument.ts +114 -0
  141. package/src/wire/BeatPlain.test.ts +113 -0
  142. package/src/wire/BeatPlain.ts +127 -0
  143. package/src/wire/BeatStatus.test.ts +59 -0
  144. package/src/wire/BeatStatus.ts +81 -0
  145. package/src/wire/Demand.test.ts +142 -0
  146. package/src/wire/Demand.ts +108 -0
  147. package/src/wire/ExitCodes.test.ts +21 -0
  148. package/src/wire/ExitCodes.ts +16 -0
  149. package/src/wire/Glob.ts +29 -0
  150. package/src/wire/Judge.ts +22 -0
  151. package/src/wire/Land.test.ts +120 -0
  152. package/src/wire/Land.ts +75 -0
  153. package/src/wire/Select.test.ts +148 -0
  154. package/src/wire/Select.ts +78 -0
  155. package/src/wire/Sessions.test.ts +59 -0
  156. package/src/wire/Sessions.ts +39 -0
  157. package/src/wire/WireSchema.test.ts +162 -0
  158. package/src/wire/constants.ts +22 -0
  159. package/src/wire/golden/1/beat-capture.json +34 -0
  160. package/src/wire/golden/1/beat-message.json +34 -0
  161. package/src/wire/golden/1/beat-prompt.json +44 -0
  162. package/src/wire/golden/1/beat-script.json +34 -0
  163. package/src/wire/golden/1/beat-stalled.json +34 -0
  164. package/src/wire/golden/1/judge.json +14 -0
  165. package/src/wire/golden/1/land-noop.json +10 -0
  166. package/src/wire/golden/1/land.json +10 -0
  167. package/src/wire/index.ts +12 -0
  168. package/src/wire/types.ts +46 -0
  169. package/src/workflow/Diagnostic.test.ts +93 -0
  170. package/src/workflow/Diagnostic.ts +54 -0
  171. package/src/workflow/SteeringFormats.test.ts +27 -0
  172. package/src/workflow/SteeringFormats.ts +6 -0
  173. package/src/workflow/Workflow.ts +77 -0
  174. package/src/workflow/compile.test.ts +53 -0
  175. package/src/workflow/compile.ts +56 -0
  176. package/src/workflow/discovery.ts +122 -0
  177. package/src/workflow/fingerprint.test.ts +52 -0
  178. package/src/workflow/fingerprint.ts +53 -0
  179. package/src/workflow/index.ts +33 -0
  180. package/src/workflow/load.test.ts +1017 -0
  181. package/src/workflow/load.ts +836 -0
  182. package/src/workflow/skills.test.ts +109 -0
  183. package/src/workflow/skills.ts +90 -0
  184. package/src/workflow/vars.ts +33 -0
  185. package/src/workflows/feature/feature.test.ts +27 -33
  186. package/src/workflows/feature/index.ts +10 -0
  187. package/src/workflows/fix/fix.feature +80 -0
  188. package/src/workflows/fix/fix.test.ts +58 -78
  189. package/src/workflows/fix/index.ts +6 -0
  190. package/src/workflows/index.ts +2 -10
  191. package/src/workflows/review/index.ts +6 -0
  192. package/src/workflows/review/review.test.ts +85 -29
  193. package/src/workflows/review/review.ts +3 -1
  194. package/src/workflows/shared/config.fixture.ts +12 -0
  195. package/src/workflows/shared/reserved.ts +11 -0
  196. package/src/workflows/shared/workflows.steps.ts +7 -0
package/dist/flows.mjs ADDED
@@ -0,0 +1,525 @@
1
+ import { t as __exportAll } from "./chunks/rolldown-runtime-8H4AJuhK.mjs";
2
+ //#region src/flows/runtime.ts
3
+ /**
4
+ * Bump only when the installed `FlowContext` object changes shape or semantics;
5
+ * type-only authoring-API changes don't count. A bump past 1 must keep v1
6
+ * members working or move the context to a new symbol key — pre-handshake
7
+ * facades never read the protocol.
8
+ */
9
+ const FLOWS_PROTOCOL = 1;
10
+ var FlowsProtocolError = class extends Error {
11
+ constructor(engine, flows) {
12
+ 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"}`);
13
+ this.name = "FlowsProtocolError";
14
+ }
15
+ };
16
+ const CONTEXT_KEY = Symbol.for("@pmelab/gtd/flow-context");
17
+ const PROTOCOL_KEY = Symbol.for("@pmelab/gtd/flow-protocol");
18
+ const MISMATCH_KEY = Symbol.for("@pmelab/gtd/flow-protocol-mismatch");
19
+ /** Engine-only. */
20
+ const installContext = (context) => {
21
+ const holder = globalThis;
22
+ holder[CONTEXT_KEY] = context;
23
+ holder[PROTOCOL_KEY] = 1;
24
+ holder[MISMATCH_KEY] = void 0;
25
+ };
26
+ /** Engine-only: the mismatch a facade copy hit during this replay, if any. */
27
+ const protocolMismatch = () => globalThis[MISMATCH_KEY];
28
+ const ctx = () => {
29
+ const holder = globalThis;
30
+ const context = holder[CONTEXT_KEY];
31
+ if (context === void 0) throw new Error("gtd: a workflow step or helper was called outside a gtd replay");
32
+ const protocol = holder[PROTOCOL_KEY] ?? 1;
33
+ if (protocol !== 1) {
34
+ const error = new FlowsProtocolError(protocol, 1);
35
+ holder[MISMATCH_KEY] ??= error;
36
+ throw error;
37
+ }
38
+ return context;
39
+ };
40
+ /** An agent turn: `prompt` is emitted, the driver lands the diff. */
41
+ const agent = (name, prompt, options = {}) => ctx().step({
42
+ kind: "agent",
43
+ name,
44
+ prompt,
45
+ options
46
+ });
47
+ /** A human gate: the process rests until a person lands a turn. */
48
+ const human = (name, options = {}) => ctx().step({
49
+ kind: "human",
50
+ name,
51
+ options
52
+ });
53
+ /** A check: `body` runs at the edge and its effect on the tree is committed. */
54
+ const run = (name, body, options = {}) => ctx().step({
55
+ kind: "run",
56
+ name,
57
+ body,
58
+ options
59
+ });
60
+ /** A judge gate: resolves to what the judge answered, for the flow to decide on. */
61
+ const judge = (name, spec) => {
62
+ const { questions, evidence, ...options } = spec;
63
+ return ctx().step({
64
+ kind: "judge",
65
+ name,
66
+ questions,
67
+ evidence,
68
+ options
69
+ });
70
+ };
71
+ /** End the episode from any depth. The next episode starts the flow afresh. */
72
+ const restart = () => ctx().step({ kind: "restart" });
73
+ /** Refuse the pending turn: nothing lands, the process stays where it rests. */
74
+ const refuse = (message) => ctx().refuse(message);
75
+ /**
76
+ * Run `fn` inside a scope: `scope("build", fn)` prefixes every step name with
77
+ * `build.` (also their memory scope); an object may add the model and system
78
+ * prompt its agent steps run with.
79
+ */
80
+ const scope = async (scope, fn) => {
81
+ const context = ctx();
82
+ context.pushScope(typeof scope === "string" ? { name: scope } : scope);
83
+ try {
84
+ return await fn();
85
+ } finally {
86
+ context.popScope();
87
+ }
88
+ };
89
+ const read = (path) => ctx().read(path);
90
+ /** Every path in the tree matching `pattern` (`*` stays within a segment, `**` crosses them). */
91
+ const glob = (pattern) => ctx().glob(pattern);
92
+ const toChanges = (list) => Object.freeze(Object.assign([...list], {
93
+ paths: list.map((c) => c.path),
94
+ get: (path) => list.find((c) => c.path === path)
95
+ }));
96
+ /** What the last step changed, optionally only the paths matching a glob. */
97
+ const changes = (pattern) => {
98
+ const context = ctx();
99
+ const all = context.changes();
100
+ return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
101
+ };
102
+ /**
103
+ * Every change from the tree at `hash` to the tree replay stands on — this
104
+ * package's own range, when `hash` is captured at its first step. `hash` must
105
+ * be the episode's base or one of its commits; anything else fails the step.
106
+ */
107
+ const changesSince = (hash, pattern) => {
108
+ const context = ctx();
109
+ const all = context.changesSince(hash);
110
+ return toChanges(pattern === void 0 ? all : all.filter((c) => context.matches(c.path, pattern)));
111
+ };
112
+ /** The commit the process stands on at this point of the flow. */
113
+ const head = () => ctx().head();
114
+ /** The process's diff base: the commit before it began, or the base `gtd --workflow` fixed. */
115
+ const start = () => ctx().start();
116
+ /** The skill list `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
117
+ const skillsFor = (localName) => ctx().skillsFor(localName);
118
+ /** The folded access `localName` (scoped from here, same as `agent()`) resolves to — for a prompt preamble. */
119
+ const accessFor = (localName, file) => ctx().accessFor(localName, file);
120
+ const settingsProxy = (read) => new Proxy({}, {
121
+ get: (_target, key) => typeof key === "string" ? read()[key] : void 0,
122
+ has: (_target, key) => typeof key === "string" && key in read(),
123
+ ownKeys: () => Object.keys(read()),
124
+ getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && key in read() ? {
125
+ enumerable: true,
126
+ configurable: true,
127
+ value: read()[key]
128
+ } : void 0
129
+ });
130
+ /** The process settings: pinned at process start, so flow code may branch on them. */
131
+ const vars = settingsProxy(() => ctx().vars);
132
+ /**
133
+ * The environment settings: resolved live on every invocation. Flow code must
134
+ * read them only where they cannot change the next step (step options, script
135
+ * bodies) — replay cannot enforce this.
136
+ */
137
+ const env = settingsProxy(() => ctx().env);
138
+ /** The top-level `## ` heading texts of markdown `text`. */
139
+ const sections = (text) => ctx().sections(text);
140
+ /** The top-level `## ` sections of markdown `text`, each with its own markdown. */
141
+ const sectionBodies = (text) => ctx().sectionBodies(text);
142
+ /** Every `H:`/`A:` thread (a footnote) in `text`. */
143
+ const threads = (text) => ctx().threads(text);
144
+ /** Every `H:`/`A:` thread in line comments of the files changed since the process's diff base. */
145
+ const codeThreads = () => ctx().codeThreads();
146
+ /** The qa-format questions in `text` that are still unanswered. */
147
+ const openQuestions = (text) => ctx().openQuestions(text);
148
+ //#endregion
149
+ //#region src/flows/scripts.ts
150
+ /** POSIX single-quoting: `value` survives the shell as one word, verbatim. */
151
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
152
+ const removal = (paths) => paths.length === 0 ? [] : [`rm -rf -- ${paths.map(quote).join(" ")}`];
153
+ /**
154
+ * Run `command` and keep its outcome in the tree: on failure its output (or
155
+ * a note that there was none) lands in `report`, on success `report` goes.
156
+ */
157
+ const checkScript = (command, options) => {
158
+ const report = quote(options.report);
159
+ const output = quote(`${options.report}.output`);
160
+ return [
161
+ "#!/usr/bin/env sh",
162
+ "set +e",
163
+ ...options.preamble ?? [],
164
+ ...removal(options.sweep ?? []),
165
+ `mkdir -p "$(dirname ${report})"`,
166
+ "(",
167
+ command,
168
+ `) > ${output} 2>&1`,
169
+ "code=$?",
170
+ "if [ \"$code\" -ne 0 ]; then",
171
+ ` if [ -s ${output} ]; then`,
172
+ ` mv ${output} ${report}`,
173
+ " else",
174
+ ` rm -f ${output}`,
175
+ ` printf 'the test command failed with exit code %s and produced no output.' "$code" > ${report}`,
176
+ " fi",
177
+ ` printf '\\n<!-- gtd check %s -->\\n' ${quote(options.stamp)} >> ${report}`,
178
+ "else",
179
+ ` rm -f ${output} ${report}`,
180
+ ...removal(options.sweepOnGreen ?? []).map((line) => ` ${line}`),
181
+ "fi",
182
+ ""
183
+ ].join("\n");
184
+ };
185
+ /**
186
+ * Reverse-apply `commit`'s changes to the working tree. A failure — the
187
+ * patch no longer applies — writes `failure` and git's error to `report`,
188
+ * since a failed revert and one with nothing to undo both leave the tree clean.
189
+ */
190
+ const revertScript = (commit, report, failure) => {
191
+ const range = `${quote(`${commit}^`)} ${quote(commit)}`;
192
+ const error = quote(`${report}.output`);
193
+ return [
194
+ "#!/usr/bin/env sh",
195
+ "set +e",
196
+ `if ! git diff --quiet ${range} --; then`,
197
+ ` mkdir -p "$(dirname ${quote(report)})"`,
198
+ ` git diff --binary ${range} -- | git apply -R 2> ${error}`,
199
+ " code=$?",
200
+ " if [ \"$code\" -ne 0 ]; then",
201
+ " {",
202
+ ` printf '%s\\n\\n' ${quote(failure)}`,
203
+ ` if [ -s ${error} ]; then cat ${error}; else printf 'Reverting it exited %s and produced no output.\\n' "$code"; fi`,
204
+ ` } > ${quote(report)}`,
205
+ " fi",
206
+ ` rm -f ${error}`,
207
+ "fi",
208
+ ""
209
+ ].join("\n");
210
+ };
211
+ /**
212
+ * Undo the edits one commit made to `paths`: each goes back to its content
213
+ * before `commit` — or away, when `commit` added it — but only while it is
214
+ * still exactly as `commit` left it. A path changed since is left alone.
215
+ */
216
+ const restoreScript = (commit, paths, source) => {
217
+ const unchanged = (path) => `git diff --quiet ${quote(commit)} -- ${quote(path)}`;
218
+ return [
219
+ "#!/usr/bin/env sh",
220
+ "set +e",
221
+ ...paths.restore.map((path) => `${unchanged(path)} && git restore --source=${quote(source ?? `${commit}~1`)} --worktree -- ${quote(path)}`),
222
+ ...paths.remove.map((path) => `${unchanged(path)} && rm -f -- ${quote(path)}`),
223
+ ""
224
+ ].join("\n");
225
+ };
226
+ /** Remove `paths` from the working tree; absent ones are fine. */
227
+ const removeScript = (paths) => [
228
+ "#!/usr/bin/env sh",
229
+ ...removal(paths),
230
+ ""
231
+ ].join("\n");
232
+ /** Move `from` to `to`, creating `to`'s directory. */
233
+ const moveScript = (from, to) => [
234
+ "#!/usr/bin/env sh",
235
+ "set -e",
236
+ `mkdir -p "$(dirname ${quote(to)})"`,
237
+ `mv -- ${quote(from)} ${quote(to)}`,
238
+ ""
239
+ ].join("\n");
240
+ //#endregion
241
+ //#region src/flows/helpers.ts
242
+ /** Whether a judge answered `expected` at a probability of at least `minP`. */
243
+ const answered = (answer, expected, minP) => answer !== void 0 && answer.answer === expected && answer.p >= minP;
244
+ /** A numeric var, or `fallback` when it is unset, blank or not a finite number. */
245
+ const numeric = (value, fallback) => {
246
+ const n = Number(value);
247
+ return value === void 0 || value.trim() === "" || !Number.isFinite(n) ? fallback : n;
248
+ };
249
+ /** Whether the last step added or rewrote `path`. */
250
+ const wrote = (path) => changes(path).some((c) => c.status !== "deleted");
251
+ /**
252
+ * A step that runs `command` through the driver and keeps its outcome in the
253
+ * tree: a failure writes `report`, a pass removes it. Resolves `true` when the
254
+ * command passed.
255
+ */
256
+ const check = async (name, command, options) => {
257
+ const { report, label, sweep, sweepOnGreen, preamble } = options;
258
+ await run(name, checkScript(command, {
259
+ report,
260
+ stamp: head().slice(0, 7),
261
+ sweep,
262
+ sweepOnGreen,
263
+ preamble
264
+ }), { label });
265
+ return !wrote(report);
266
+ };
267
+ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
268
+ /** The last step's changes outside `.gtd/`. */
269
+ const codeChanges = () => changes().filter((c) => isCode(c.path));
270
+ /** Refuse a turn whose only change deletes `file` — unless it says it found nothing to do. */
271
+ const requireProgress = (file) => {
272
+ const change = changes().get(file);
273
+ if (change?.status !== "deleted") return;
274
+ if (codeChanges().length > 0) return;
275
+ if ((change.before ?? "").trim().startsWith("NOTHING ACTIONABLE")) return;
276
+ refuse(`gtd land: feedback-progress: ${file} was deleted without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file.`);
277
+ };
278
+ /** Refuse a turn that leaves a question in `file` unanswered. An untouched tree is silence, and allowed. */
279
+ const requireAnswers = (file) => {
280
+ if (changes().length === 0) return;
281
+ const open = openQuestions(read(file) ?? "");
282
+ if (open.length === 0) return;
283
+ const list = open.map((q) => ` - ${file}:${q.line}: ${q.question}`).join("\n");
284
+ refuse(`gtd land: answer-completeness: ${open.length} open question(s) in ${file} not answered — tick exactly one option per question, or delete a question you don't want to answer. To accept the plan as-is instead, revert everything and re-run:\n${list}`);
285
+ };
286
+ /** Footnote threads in `file` waiting on `waitingOn`, plus any with a syntax fault — a fault refuses both ways. */
287
+ const threadLines = (file, waitingOn) => threads(read(file) ?? "").filter((t) => t.waitingOn === waitingOn || t.faults.length > 0).flatMap((t) => [` - ${file}:${t.line}: [^${t.name}]`, ...t.faults.map((fault) => ` ${fault}`)]);
288
+ /** Code threads waiting on `waitingOn`, plus any with a syntax fault — a fault refuses both ways. */
289
+ const codeThreadLines = (waitingOn) => codeThreads().filter((t) => t.waitingOn === waitingOn || t.faults.length > 0).flatMap((t) => [` - ${t.path}:${t.line}: ${t.first}`, ...t.faults.map((fault) => ` ${fault}`)]);
290
+ const countOf = (lines) => lines.filter((line) => line.startsWith(" - ")).length;
291
+ /** Whether a thread waits on `waitingOn`: one in footnote file `file`, or any code thread in the process's changed files. */
292
+ const hasThreadFor = (waitingOn, file) => file !== void 0 && threads(read(file) ?? "").some((t) => t.waitingOn === waitingOn) || codeThreads().some((t) => t.waitingOn === waitingOn);
293
+ /** Refuse any landing while a thread in `file` or a code thread in a changed file waits on the human (its last entry is the agent's), or has a syntax fault. */
294
+ const requireThreadsClosed = (file) => {
295
+ const open = [...threadLines(file, "human"), ...codeThreadLines("human")];
296
+ if (open.length === 0) return;
297
+ refuse(`gtd land: open-threads: ${countOf(open)} thread(s) in ${file} or code comments wait on you — reply with a conclusion, or delete the thread:\n${open.join("\n")}`);
298
+ };
299
+ /** Refuse an agent turn that leaves a thread in `file`, or a code thread in a changed file, waiting on the agent (its last entry is `H:`) or with a syntax fault. */
300
+ const requireReplies = (file) => {
301
+ const open = [...threadLines(file, "agent"), ...codeThreadLines("agent")];
302
+ if (open.length === 0) return;
303
+ refuse(`gtd land: unanswered-threads: ${countOf(open)} thread(s) in ${file} or code comments still wait on the agent — append one "- A:" reply to each (one "A:" comment line, same token and indentation, directly below a code thread's last line), or fold a concluded thread and delete it:\n${open.join("\n")}`);
304
+ };
305
+ /** Refuse a turn that did not undo the code edits `edited` recorded, back to their content before `base`. */
306
+ const requireRevert = (edited, base) => {
307
+ const residue = edited.filter((c) => read(c.path) !== c.before).map((c) => c.path);
308
+ if (residue.length === 0) return;
309
+ const quoted = residue.map((path) => `'${path.replace(/'/g, "'\\''")}'`).join(" ");
310
+ refuse(`gtd land: require-revert: ${residue.join(", ")} still differ from ${base}~1 — the revert did not take. Run \`git checkout ${base}~1 -- ${quoted}\`, then \`gtd land\` again.`);
311
+ };
312
+ //#endregion
313
+ //#region src/flows/factory.ts
314
+ const FACTORY = Symbol.for("@pmelab/gtd/workflow-factory");
315
+ /**
316
+ * Define a workflow configured by options. `schema` is any Standard Schema
317
+ * (https://standardschema.dev); it should reject unknown keys, so a removed or
318
+ * misspelled option fails at load. Exported from `gtd.config.ts`, the factory
319
+ * itself and a call's result are both workflows.
320
+ */
321
+ const defineWorkflow = (schema, build) => {
322
+ const internals = {
323
+ schema,
324
+ build
325
+ };
326
+ const factory = (options) => ({
327
+ [FACTORY]: {
328
+ ...internals,
329
+ options
330
+ },
331
+ then: () => {
332
+ throw new Error("a configured workflow is not a flow — compose fixFlow/reviewFlow instead");
333
+ }
334
+ });
335
+ return Object.assign(factory, { [FACTORY]: internals });
336
+ };
337
+ const internalsOf = (value) => (typeof value === "function" || typeof value === "object" && value !== null) && FACTORY in value ? value[FACTORY] : void 0;
338
+ const isFactoryWorkflow = (value) => internalsOf(value) !== void 0;
339
+ const segment = (key) => {
340
+ const raw = typeof key === "object" ? key.key : key;
341
+ return typeof raw === "number" ? raw : String(raw);
342
+ };
343
+ /**
344
+ * Validate a factory workflow's options — those it was called with, `overrides`
345
+ * on top — and build it. `undefined` for an export that is no factory workflow.
346
+ */
347
+ const configure = (value, overrides = {}) => {
348
+ const internals = internalsOf(value);
349
+ if (internals === void 0) return void 0;
350
+ const options = {
351
+ ...internals.options,
352
+ ...overrides
353
+ };
354
+ const result = internals.schema["~standard"].validate(options);
355
+ if (result instanceof Promise) return {
356
+ ok: false,
357
+ issues: [{
358
+ path: [],
359
+ message: "the options schema validates asynchronously — gtd needs a synchronous one"
360
+ }]
361
+ };
362
+ if (result.issues !== void 0) return {
363
+ ok: false,
364
+ issues: result.issues.length === 0 ? [{
365
+ path: [],
366
+ message: "the options schema rejected them without saying why"
367
+ }] : result.issues.map((issue) => ({
368
+ path: (issue.path ?? []).map(segment),
369
+ message: issue.message
370
+ }))
371
+ };
372
+ return {
373
+ ok: true,
374
+ build: internals.build(result.value)
375
+ };
376
+ };
377
+ //#endregion
378
+ //#region src/flows/config.ts
379
+ const CONFIG_KEYS = [
380
+ "modes",
381
+ "ui",
382
+ "skills",
383
+ "access"
384
+ ];
385
+ const MODE_KEYS = ["format", "validate"];
386
+ const UI_KEYS = [
387
+ "port",
388
+ "host",
389
+ "cert",
390
+ "key",
391
+ "format"
392
+ ];
393
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
394
+ const isStringArray = (v) => Array.isArray(v) && v.every((entry) => typeof entry === "string");
395
+ const unknownKeys = (value, known, path) => Object.keys(value).filter((key) => !known.includes(key)).map((key) => ({
396
+ path: [...path, key],
397
+ message: "is not a known key"
398
+ }));
399
+ const recordOf = (value, path, shape, entry) => isRecord(value) ? Object.entries(value).flatMap(([key, v]) => entry(v, [...path, key])) : [{
400
+ path,
401
+ message: `must be an object of ${shape}`
402
+ }];
403
+ const modeProblems = (value, path) => {
404
+ if (path.at(-1) === "") return [{
405
+ path,
406
+ message: "is a mode without a name"
407
+ }];
408
+ if (!isRecord(value)) return [{
409
+ path,
410
+ message: "must be an object with \"format\" and/or \"validate\""
411
+ }];
412
+ return [...unknownKeys(value, MODE_KEYS, path), ...MODE_KEYS.flatMap((key) => {
413
+ const command = value[key];
414
+ return command === void 0 || typeof command === "string" && command.trim() !== "" ? [] : [{
415
+ path: [...path, key],
416
+ message: "must be a non-empty shell command"
417
+ }];
418
+ })];
419
+ };
420
+ const uiProblems = (value, path) => {
421
+ if (!isRecord(value)) return [{
422
+ path,
423
+ message: "must be an object"
424
+ }];
425
+ return [...unknownKeys(value, UI_KEYS, path), ...UI_KEYS.flatMap((key) => {
426
+ const v = value[key];
427
+ if (v === void 0) return [];
428
+ return (key === "port" ? Number.isInteger(v) : typeof v === "string") ? [] : [{
429
+ path: [...path, key],
430
+ message: key === "port" ? "must be an integer" : "must be a string"
431
+ }];
432
+ })];
433
+ };
434
+ const accessProblems = (value, path) => {
435
+ if (!isRecord(value)) return [{
436
+ path,
437
+ message: "must be an object of { read?, write? } glob arrays"
438
+ }];
439
+ return [...unknownKeys(value, ["read", "write"], path), ...["read", "write"].flatMap((side) => value[side] === void 0 || isStringArray(value[side]) ? [] : [{
440
+ path: [...path, side],
441
+ message: "must be an array of glob strings"
442
+ }])];
443
+ };
444
+ /** Every way `value` is not a `GtdConfig`; an unknown key is one, so a removed key fails loudly. */
445
+ const configProblems = (value) => {
446
+ if (!isRecord(value)) return [{
447
+ path: [],
448
+ message: "must be an object"
449
+ }];
450
+ return [
451
+ ...unknownKeys(value, CONFIG_KEYS, []),
452
+ ...value.modes === void 0 ? [] : recordOf(value.modes, ["modes"], "modes", modeProblems),
453
+ ...value.ui === void 0 ? [] : uiProblems(value.ui, ["ui"]),
454
+ ...value.skills === void 0 ? [] : recordOf(value.skills, ["skills"], "skill-name arrays", (v, path) => isStringArray(v) ? [] : [{
455
+ path,
456
+ message: "must be an array of skill names"
457
+ }]),
458
+ ...value.access === void 0 ? [] : recordOf(value.access, ["access"], "scope access", accessProblems)
459
+ ];
460
+ };
461
+ const formatConfigProblem = ({ path, message }) => `${["config", ...path].join(".")}: ${message}`;
462
+ /**
463
+ * Type and check the `config` export. Not generic on purpose: a literal argument
464
+ * then gets TypeScript's excess-property check at every depth, so an unknown key
465
+ * is a type error — and it throws at runtime too.
466
+ */
467
+ const defineConfig = (config) => {
468
+ const problems = configProblems(config);
469
+ if (problems.length > 0) throw new Error(problems.map(formatConfigProblem).join("; "));
470
+ return config;
471
+ };
472
+ //#endregion
473
+ //#region src/flows/index.ts
474
+ var flows_exports = /* @__PURE__ */ __exportAll({
475
+ FLOWS_PROTOCOL: () => 1,
476
+ FlowsProtocolError: () => FlowsProtocolError,
477
+ accessFor: () => accessFor,
478
+ agent: () => agent,
479
+ answered: () => answered,
480
+ changes: () => changes,
481
+ changesSince: () => changesSince,
482
+ check: () => check,
483
+ checkScript: () => checkScript,
484
+ codeChanges: () => codeChanges,
485
+ codeThreads: () => codeThreads,
486
+ configProblems: () => configProblems,
487
+ configure: () => configure,
488
+ defineConfig: () => defineConfig,
489
+ defineWorkflow: () => defineWorkflow,
490
+ env: () => env,
491
+ glob: () => glob,
492
+ hasThreadFor: () => hasThreadFor,
493
+ head: () => head,
494
+ human: () => human,
495
+ installContext: () => installContext,
496
+ isFactoryWorkflow: () => isFactoryWorkflow,
497
+ judge: () => judge,
498
+ moveScript: () => moveScript,
499
+ numeric: () => numeric,
500
+ openQuestions: () => openQuestions,
501
+ protocolMismatch: () => protocolMismatch,
502
+ quote: () => quote,
503
+ read: () => read,
504
+ refuse: () => refuse,
505
+ removeScript: () => removeScript,
506
+ requireAnswers: () => requireAnswers,
507
+ requireProgress: () => requireProgress,
508
+ requireReplies: () => requireReplies,
509
+ requireRevert: () => requireRevert,
510
+ requireThreadsClosed: () => requireThreadsClosed,
511
+ restart: () => restart,
512
+ restoreScript: () => restoreScript,
513
+ revertScript: () => revertScript,
514
+ run: () => run,
515
+ scope: () => scope,
516
+ sectionBodies: () => sectionBodies,
517
+ sections: () => sections,
518
+ skillsFor: () => skillsFor,
519
+ start: () => start,
520
+ threads: () => threads,
521
+ vars: () => vars,
522
+ wrote: () => wrote
523
+ });
524
+ //#endregion
525
+ export { FLOWS_PROTOCOL, FlowsProtocolError, accessFor, agent, answered, changes, changesSince, check, checkScript, codeChanges, codeThreads, configProblems, configure, defineConfig, defineWorkflow, env, glob, hasThreadFor, head, human, installContext, isFactoryWorkflow, judge, moveScript, numeric, openQuestions, protocolMismatch, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restart, restoreScript, revertScript, run, scope, sectionBodies, sections, skillsFor, start, flows_exports as t, threads, vars, wrote };