@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/wire.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { C as EXIT_SIGTERM, D as WIRE_SCHEMA, E as UNATTRIBUTED_MODEL, O as renderFormat, S as EXIT_SIGINT, T as ACCESS_REFUSAL, _ as demandOf, a as landFields, b as EXIT_OK, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, h as BEAT_KINDS, i as judgeJson, l as renderLandPlain, m as statusOf, n as resolveSession, o as landProseText, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING, v as stallDiagnosis, w as EXIT_USAGE_ERROR, x as EXIT_RUNTIME_ERROR, y as EXIT_CODES } from "./chunks/wire-DA7q2ikx.mjs";
2
+ export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, resolveSession, selectPath, stallDiagnosis, statusOf };
@@ -0,0 +1,352 @@
1
+ import { changes, changesSince, hasThreadFor, head, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
+ import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks } from "./chunks/steering-D-PvRC5W.mjs";
3
+ //#region src/workflow-kit/health.ts
4
+ /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
+ const escalation = async (count, steps) => {
6
+ if (count.rounds >= steps.limit) return steps.exhausted();
7
+ count.rounds++;
8
+ await steps.describe();
9
+ await steps.stop();
10
+ };
11
+ /** Run `check` again while `pending()` holds — a run then does not count — and resolve the first run that does. */
12
+ const untilSettled = async (check, pending) => {
13
+ for (;;) {
14
+ const green = await check();
15
+ if (!pending()) return green;
16
+ }
17
+ };
18
+ const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
19
+ const stripStamp = (text) => text.replace(STAMP, "\n");
20
+ /**
21
+ * Loop until the suite is green: check, and on red run `fix` — escalating
22
+ * first once `fixCap` fixes are spent, or `sameFailure` calls the failure
23
+ * identical to the previous round's.
24
+ */
25
+ const healthy = async (fix, health) => {
26
+ let fixes = health.fixesSoFar ?? 0;
27
+ let previous;
28
+ for (;;) {
29
+ if (await health.check()) {
30
+ health.escalations.rounds = 0;
31
+ return;
32
+ }
33
+ const current = stripStamp(read(health.report) ?? "");
34
+ const stuck = previous !== void 0 && await health.sameFailure(previous, current);
35
+ previous = current;
36
+ if (stuck || fixes >= health.fixCap) {
37
+ await escalation(health.escalations, health.escalation);
38
+ fixes = 0;
39
+ }
40
+ fixes++;
41
+ await fix();
42
+ }
43
+ };
44
+ /** Hold the process in scope `name` until `passes` resolves `true`, resting at `blocked` between tries. */
45
+ const gate = (name, passes, blocked) => scope(name, async () => {
46
+ while (!await passes()) await blocked();
47
+ });
48
+ //#endregion
49
+ //#region src/workflow-kit/quality.ts
50
+ /** Split a comma-separated lens list: trimmed, blanks dropped. */
51
+ const lensesOf = (value) => value.split(",").map((lens) => lens.trim()).filter((lens) => lens.length > 0);
52
+ /** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
53
+ const lensScopes = (scope, lenses, entry) => Object.fromEntries(lenses.map((lens) => [`${scope}.${lens}`, entry(lens)]));
54
+ /** One `review` turn per lens, each appending its findings to `findings`; resolves `"findings"` when that file has any. */
55
+ const qualityLap = async (lenses, review, findings) => {
56
+ for (const lens of lenses) await review(lens);
57
+ return (read(findings) ?? "").length > 0 ? "findings" : "clean";
58
+ };
59
+ /** Resolves `true` once `findings` is gone, `false` when `cap` turns escalated instead. */
60
+ const fixFindings = async (f) => {
61
+ for (let turns = 0; turns < f.cap; turns++) {
62
+ await f.fix();
63
+ if (read(f.findings) === void 0) return true;
64
+ }
65
+ await f.escalate();
66
+ return false;
67
+ };
68
+ //#endregion
69
+ //#region src/workflow-kit/scenarios.ts
70
+ const DOC_STRING = /^(\s*)("""|```)/;
71
+ /**
72
+ * Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
73
+ * dropped — except inside a doc string, whose content is test input: kept
74
+ * whole, indented relative to its opening delimiter (as Gherkin reads it).
75
+ */
76
+ const scenarioText = (source) => {
77
+ const out = [];
78
+ let doc;
79
+ for (const line of source.split("\n")) {
80
+ const delimiter = DOC_STRING.exec(line);
81
+ if (doc === void 0 && delimiter !== null) {
82
+ doc = {
83
+ indent: delimiter[1].length,
84
+ fence: delimiter[2]
85
+ };
86
+ out.push(line.trim());
87
+ } else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
88
+ doc = void 0;
89
+ out.push(line.trim());
90
+ } else if (doc !== void 0) {
91
+ const lead = line.length - line.trimStart().length;
92
+ out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
93
+ } else {
94
+ const trimmed = line.trim();
95
+ if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
96
+ }
97
+ }
98
+ return out.join("\n");
99
+ };
100
+ const textOf = (path) => {
101
+ const source = read(path);
102
+ return source === void 0 ? void 0 : scenarioText(source);
103
+ };
104
+ /** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
105
+ const freezeScenarios = (paths, previous) => {
106
+ if (paths.length === 0) return previous;
107
+ const texts = { ...previous?.texts };
108
+ for (const path of paths) {
109
+ const text = textOf(path);
110
+ if (text !== void 0) texts[path] = text;
111
+ }
112
+ return Object.keys(texts).length === 0 ? previous : {
113
+ at: head(),
114
+ texts
115
+ };
116
+ };
117
+ /** Paths whose wording differs from the snapshot (a deleted file counts). */
118
+ const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
119
+ const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
120
+ /** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
121
+ const lcsTable = (a, b) => {
122
+ const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
123
+ for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
124
+ return table;
125
+ };
126
+ /** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
127
+ const lineDiff = (a, b) => {
128
+ const table = lcsTable(a, b);
129
+ const out = [];
130
+ let i = 0;
131
+ let j = 0;
132
+ while (i < a.length && j < b.length) if (a[i] === b[j]) {
133
+ i++;
134
+ j++;
135
+ } else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
136
+ else out.push(`+ ${b[j++]}`);
137
+ return [
138
+ ...out,
139
+ ...a.slice(i).map((l) => `- ${l}`),
140
+ ...b.slice(j).map((l) => `+ ${l}`)
141
+ ];
142
+ };
143
+ const wordingDrift = (frozen, path) => ({
144
+ path,
145
+ lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
146
+ });
147
+ /**
148
+ * After a turn: rest at `stop` while wording drifted from `frozen`. Whatever
149
+ * still differs after it is accepted and moves the snapshot; what was
150
+ * restored stays frozen as it was.
151
+ */
152
+ const holdWording = async (frozen, stop) => {
153
+ if (frozen === void 0) return;
154
+ const drifted = driftedScenarios(frozen);
155
+ if (drifted.length === 0) return;
156
+ await stop(frozen.at, drifted.map((path) => wordingDrift(frozen, path)));
157
+ const accepted = driftedScenarios(frozen);
158
+ if (accepted.length === 0) return;
159
+ for (const path of accepted) {
160
+ const text = textOf(path);
161
+ if (text === void 0) delete frozen.texts[path];
162
+ else frozen.texts[path] = text;
163
+ }
164
+ frozen.at = head();
165
+ };
166
+ //#endregion
167
+ //#region src/workflow-kit/review.ts
168
+ const collect = async (p, capture) => {
169
+ await p.collecting(capture);
170
+ if (read(p.feedback) !== void 0) return "feedback";
171
+ if (changes().length === 0) return "signoff";
172
+ return refuse(p.refusals.unmatched);
173
+ };
174
+ /** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
175
+ const notesOf = (text) => text.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "").replace(/\[\^[^\]]+\]/g, "").replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
176
+ /**
177
+ * The human gate and its reply rounds: a question gets an answer inside
178
+ * the review file and rests at the gate again, without a lap. Resolves the
179
+ * commit of the last collecting turn, `"missing"` when the round left no
180
+ * review, or `undefined` when no such turn ran.
181
+ */
182
+ const converse = async (p, base, collectedBefore) => {
183
+ let collectedAt = collectedBefore;
184
+ for (;;) {
185
+ await p.awaitReview(base);
186
+ if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.refusals.deleted);
187
+ if (read(p.file) === void 0) {
188
+ await p.reviewMissing(head());
189
+ return "missing";
190
+ }
191
+ requireThreadsClosed(p.file);
192
+ if (!hasThreadFor("agent", p.file)) return collectedAt;
193
+ await p.collecting(p.captures.edits(head()));
194
+ collectedAt = head();
195
+ requireReplies(p.file);
196
+ if (!hasThreadFor("human", p.file)) return collectedAt;
197
+ }
198
+ };
199
+ /** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
200
+ const routeNotes = async (p, notes, r) => {
201
+ const verdicts = await p.triage(notes);
202
+ const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
203
+ const [edits, questions, nits] = [
204
+ of("edit"),
205
+ of("question"),
206
+ of("nit")
207
+ ];
208
+ if (edits.length + questions.length + nits.length === 0) return r.unfolded();
209
+ let answeredAt;
210
+ if (questions.length > 0) {
211
+ await p.answerQuestions(questions);
212
+ answeredAt = head();
213
+ }
214
+ if (nits.length > 0) {
215
+ await p.fixNits(nits);
216
+ await p.keepGreen();
217
+ }
218
+ if (edits.length > 0) {
219
+ await r.close();
220
+ return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)));
221
+ }
222
+ if (nits.length > 0) {
223
+ await r.close();
224
+ return {
225
+ next: "rereview",
226
+ carry: answeredAt
227
+ };
228
+ }
229
+ return {
230
+ next: "await",
231
+ reviewed: answeredAt
232
+ };
233
+ };
234
+ /** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
235
+ const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
236
+ /** Whether anything but thread edits and ticks landed in the review file since `commit`. */
237
+ const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
238
+ /**
239
+ * What the in-loop collecting turn left: `folded` when it wrote `feedback`
240
+ * (those folds need their lap whatever is judged later), `settled` when only
241
+ * thread edits and ticks landed after it.
242
+ */
243
+ const afterCollect = (p, collectedAt) => collectedAt === "" ? {
244
+ folded: false,
245
+ settled: false
246
+ } : settledAfter(p, collectedAt);
247
+ const settledAfter = (p, collectedAt) => ({
248
+ folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
249
+ settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
250
+ });
251
+ const finish = async (p, reviewed, collectedAt) => {
252
+ const round = head();
253
+ const since = changesSince(reviewed);
254
+ const edited = since.filter((c) => isCodeEdit(p, c));
255
+ const baselineText = since.get(p.file)?.before ?? "";
256
+ const record = read(p.file) ?? "";
257
+ const { folded, settled } = afterCollect(p, collectedAt);
258
+ const noted = since.some((c) => c.path === p.file);
259
+ const close = () => p.close(round);
260
+ const outcome = (verdict) => verdict === "signoff" ? { verdict } : {
261
+ verdict,
262
+ base: round,
263
+ restoreFrom: reviewed,
264
+ edited
265
+ };
266
+ const unfolded = async () => {
267
+ await close();
268
+ return outcome(folded ? "feedback" : "signoff");
269
+ };
270
+ const collectWith = async (capture) => {
271
+ await close();
272
+ return outcome(await collect(p, capture));
273
+ };
274
+ if (settled || edited.length === 0 && !noted) return unfolded();
275
+ if (edited.length > 0) return collectWith(p.captures.edits(round));
276
+ const notes = reviewNotes(baselineText, record);
277
+ if (notes.length === 0) return collectWith(p.captures.notes(round));
278
+ return routeNotes(p, notes, {
279
+ round,
280
+ close,
281
+ outcome,
282
+ unfolded
283
+ });
284
+ };
285
+ /** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
286
+ const reviewOnce = async (p, base, carry) => {
287
+ await p.reviewing(base, carry);
288
+ const risks = reviewRisks(read(p.file) ?? "");
289
+ if (risks.length === 0) return;
290
+ await p.fixRisks(risks);
291
+ await p.keepGreen();
292
+ await p.reviewing(base, carry);
293
+ };
294
+ /**
295
+ * A reviewer writes the review over everything since `base`, a human
296
+ * reviews and signs off or comments, and each note is judged: edits go to
297
+ * another lap, questions are answered in place, nits fixed in one batch,
298
+ * praise dropped.
299
+ */
300
+ const review = async (base, p) => {
301
+ let carry;
302
+ for (;;) {
303
+ await reviewOnce(p, base, carry);
304
+ carry = void 0;
305
+ let reviewed = head();
306
+ let collectedAt;
307
+ for (;;) {
308
+ collectedAt = await converse(p, base, collectedAt);
309
+ if (collectedAt === "missing") break;
310
+ const result = await finish(p, reviewed, collectedAt);
311
+ if (!("next" in result)) return result;
312
+ if (result.next === "rereview") {
313
+ carry = result.carry;
314
+ break;
315
+ }
316
+ reviewed = result.reviewed;
317
+ }
318
+ }
319
+ };
320
+ /**
321
+ * A reviewer writes the review over everything since `base` and a human
322
+ * reviews it. No note signs off and closes; any note ends the round with the
323
+ * review left in place, for another workflow to read its notes — nothing is
324
+ * judged or re-planned here.
325
+ */
326
+ const reviewToHandOff = async (base, p) => {
327
+ for (;;) {
328
+ await reviewOnce(p, base, void 0);
329
+ const reviewed = head();
330
+ await p.awaitReview(base);
331
+ if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.deleted);
332
+ const record = read(p.file);
333
+ if (record === void 0) {
334
+ await p.reviewMissing(head());
335
+ continue;
336
+ }
337
+ requireThreadsClosed(p.file);
338
+ if (hasThreadFor("agent", p.file)) refuse(p.threadsPending);
339
+ const since = changesSince(reviewed);
340
+ const before = since.get(p.file)?.before ?? record;
341
+ if (notesOf(before) === notesOf(record)) {
342
+ if (since.paths.some((f) => !f.startsWith(".gtd/"))) await p.keepGreen();
343
+ await p.close(head());
344
+ return;
345
+ }
346
+ if (reviewNotes(before, record).length === 0) refuse(p.unparsed);
347
+ await p.handOff(reviewed);
348
+ return;
349
+ }
350
+ };
351
+ //#endregion
352
+ export { driftedScenarios, escalation, fixFindings, freezeScenarios, gate, healthy, holdWording, lensScopes, lensesOf, qualityLap, review, reviewToHandOff, scenarioText, untilSettled, wordingDrift };
@@ -0,0 +1,315 @@
1
+ import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
2
+ import { freezeScenarios, review as review$1 } from "../workflow-kit.mjs";
3
+ import { $ as unwindFailure, A as architectureAuthorPrompt, B as designTriagePrompt, C as fixCheck, D as reviewing, E as reviewMissing, F as buildReviewFixNitsPrompt, G as packagesItemFixSuitePrompt, I as buildReviewTriageMessage, K as reviewEditNotesCapture, L as builderSystem, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewCollectingPrompt, Q as unwindFailedMessage, R as designGateAnswerMessage, S as coder, T as planner, U as idleMessage, W as packagesItemBuildingPrompt, X as reviewerSystem, Y as reviewNotesCapture, Z as startGateBlockedMessage, _ as ARCHITECTURE, b as REVIEW, c as refusePendingHandOff, d as shipped, f as defaults, g as healthy, h as gate, i as skills, j as architectureDecomposePrompt, k as architectSystem, l as tail, n as summary, o as closeReview, p as envDefaults, q as reviewEditsCapture, r as access, t as steering, u as guarded, v as FEEDBACK, w as fixRisks, x as awaitReview, y as REQUIREMENTS, z as designSystem } from "../chunks/reserved-ejn-LVYO.mjs";
4
+ //#region src/workflows/feature/steps.ts
5
+ /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
6
+ const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
7
+ label: "Triaging the change",
8
+ file: REQUIREMENTS,
9
+ mode: "qa",
10
+ model: planner(),
11
+ system: designSystem()
12
+ });
13
+ const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
14
+ label: "Refining the technical plan",
15
+ file: ARCHITECTURE,
16
+ mode: "qa",
17
+ model: planner(),
18
+ system: architectSystem()
19
+ });
20
+ const decompose = () => agentWithSkills("decompose.decomposing", architectureDecomposePrompt(), {
21
+ label: "Decomposing into packages",
22
+ model: planner(),
23
+ system: architectSystem()
24
+ });
25
+ const answerProductQuestions = () => human("gate.answer", {
26
+ message: designGateAnswerMessage(),
27
+ label: "Awaiting your product answers",
28
+ file: REQUIREMENTS,
29
+ mode: "qa",
30
+ acceptClean: true
31
+ });
32
+ const answerTechnicalQuestions = () => human("gate.answer", {
33
+ message: architectureGateAnswerMessage(),
34
+ label: "Awaiting your technical answers",
35
+ file: ARCHITECTURE,
36
+ mode: "qa",
37
+ acceptClean: true
38
+ });
39
+ const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
40
+ label: "Building",
41
+ model: coder(),
42
+ system: builderSystem()
43
+ });
44
+ const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuitePrompt(), {
45
+ label: "Fixing the check",
46
+ file: FEEDBACK,
47
+ model: coder(),
48
+ system: builderSystem()
49
+ });
50
+ /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
51
+ const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
52
+ label: "Answering your questions",
53
+ file: REVIEW,
54
+ mode: "review",
55
+ model: planner(),
56
+ system: reviewerSystem()
57
+ });
58
+ /** Fix every `nit` note in one turn. */
59
+ const fixNits = (notes) => agentWithSkills("review.fix.nits.fixing", buildReviewFixNitsPrompt(notes), {
60
+ label: "Fixing your nits",
61
+ file: REVIEW,
62
+ model: planner(),
63
+ system: reviewerSystem()
64
+ });
65
+ /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
66
+ const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
67
+ label: "Collecting your feedback",
68
+ file: REQUIREMENTS,
69
+ mode: "qa",
70
+ model: planner(),
71
+ system: reviewerSystem(),
72
+ allowEmpty: true
73
+ });
74
+ //#endregion
75
+ //#region src/workflows/feature/packages.ts
76
+ /** Package 0: the e2e scenarios, written red. */
77
+ const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
78
+ const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
79
+ /** The `## Tests` entries of a package file; malformed lines are ignored. */
80
+ const declaredTests = (packageText) => {
81
+ const found = [];
82
+ let inTests = false;
83
+ for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
84
+ else if (inTests) {
85
+ const match = TEST_LINE.exec(line);
86
+ if (match) found.push({
87
+ level: match[1],
88
+ path: match[2]
89
+ });
90
+ }
91
+ return found;
92
+ };
93
+ /**
94
+ * Refuse a build or fix turn unless every test `pkg` declares exists and is in the
95
+ * diff since `since`. The package text is read as of `since`, so a builder
96
+ * editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
97
+ * written, existing in the tree suffices.
98
+ */
99
+ const requireDeclaredTests = (pkg, since) => {
100
+ const range = changesSince(since);
101
+ const text = range.get(pkg)?.before ?? read(pkg) ?? "";
102
+ const satisfied = read(".gtd/SATISFIED.md") !== void 0;
103
+ const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
104
+ if (missing.length === 0) return;
105
+ refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
106
+ };
107
+ /** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
108
+ const packageItem = async (pkg, frozen) => {
109
+ const from = head();
110
+ const declared = (turn) => async () => {
111
+ await turn();
112
+ requireDeclaredTests(pkg, from);
113
+ };
114
+ await guarded(frozen, declared(() => build(pkg)))();
115
+ await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
116
+ await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
117
+ return {
118
+ pkg,
119
+ from,
120
+ to: head()
121
+ };
122
+ };
123
+ /** The first queued package — the one a package step works on. */
124
+ const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
125
+ const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
126
+ /** Build every package file under `.gtd/packages/`, in name order. */
127
+ const packages = (carried) => scope("packages", async () => {
128
+ let frozen = carried;
129
+ const ranges = [];
130
+ let touched = [];
131
+ for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
132
+ const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
133
+ ranges.push(range);
134
+ if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
135
+ touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
136
+ frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
137
+ }
138
+ }
139
+ const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
140
+ return {
141
+ ranges,
142
+ scenarios: {
143
+ added: paths("added"),
144
+ changed: paths("modified")
145
+ },
146
+ frozen
147
+ };
148
+ });
149
+ //#endregion
150
+ //#region src/workflows/feature/planning.ts
151
+ /**
152
+ * Always stop for the human. Resolves `true` when the round changed anything
153
+ * (the author revises), `false` on a clean re-run. Open threads refuse every
154
+ * landing; unticked questions only refuse a round with no thread for the
155
+ * agent to answer.
156
+ */
157
+ const noteGate = async (file, answer) => {
158
+ await answer();
159
+ requireThreadsClosed(file);
160
+ if (changes().length === 0) return false;
161
+ if (!hasThreadFor("agent", file)) requireAnswers(file);
162
+ return true;
163
+ };
164
+ /** Triage the sketch since `base` into requirements, until no product question is open. */
165
+ const design = (base) => scope("design", async () => {
166
+ do {
167
+ await triage(base);
168
+ requireProgress(REQUIREMENTS);
169
+ requireReplies(REQUIREMENTS);
170
+ } while (await noteGate(REQUIREMENTS, answerProductQuestions));
171
+ });
172
+ /** Work out the technical plan, then decompose it into package files. */
173
+ const architecture = () => scope("architecture", async () => {
174
+ do {
175
+ await author();
176
+ requireReplies(ARCHITECTURE);
177
+ } while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
178
+ await decompose();
179
+ if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
180
+ if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the build tail's full run needs it; it is removed only once that run is green");
181
+ });
182
+ //#endregion
183
+ //#region src/workflows/feature/review.ts
184
+ const VERDICTS = [
185
+ "edit",
186
+ "question",
187
+ "nit",
188
+ "praise"
189
+ ];
190
+ /** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
191
+ const verdictQuestion = (note) => ({
192
+ id: note.id,
193
+ primitive: "choice",
194
+ instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
195
+ criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
196
+ });
197
+ /**
198
+ * One judge rest, one verdict per note. Dismissing a note is the risky
199
+ * direction, so only a confident non-`edit` verdict on uncut evidence counts;
200
+ * a cut, unanswered or below-floor note is an `edit`, and a blank floor makes
201
+ * every note one.
202
+ */
203
+ const triage$1 = async (notes) => {
204
+ const { answers, truncated } = await judge("review.triage", {
205
+ questions: notes.map(verdictQuestion),
206
+ evidence: Object.fromEntries(notes.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
207
+ message: buildReviewTriageMessage(),
208
+ label: "Judging each note"
209
+ });
210
+ const minP = numeric(vars.reviewNoteActionable, Infinity);
211
+ return new Map(notes.map((n) => {
212
+ const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
213
+ return [n.id, truncated.includes(n.id) || verdict === void 0 ? "edit" : verdict];
214
+ }));
215
+ };
216
+ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
217
+ /**
218
+ * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
219
+ * reviews and signs off or comments, and each note is judged: edits go to
220
+ * another planning lap, questions are answered in place, nits fixed in one
221
+ * batch, praise dropped.
222
+ */
223
+ const review = (base, escalations = { rounds: 0 }, frozen) => review$1(base, {
224
+ file: REVIEW,
225
+ feedback: REQUIREMENTS,
226
+ isCode,
227
+ reviewing,
228
+ fixRisks: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
229
+ keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
230
+ awaitReview,
231
+ reviewMissing,
232
+ collecting,
233
+ triage: triage$1,
234
+ answerQuestions: answerReviewQuestions,
235
+ fixNits: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
236
+ close: closeReview,
237
+ captures: {
238
+ edits: reviewEditsCapture,
239
+ notes: reviewNotesCapture,
240
+ editNotes: reviewEditNotesCapture
241
+ },
242
+ refusals: {
243
+ deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
244
+ unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
245
+ }
246
+ });
247
+ /** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
248
+ const buildTail = (fixFirst, base, built) => tail(fixFirst, (escalations, frozen) => review(base, escalations, frozen), built);
249
+ //#endregion
250
+ //#region src/workflows/feature/feature.ts
251
+ /** Revert the sketch that started the process out of the working tree; its intent survives in history. */
252
+ const unwind = () => {
253
+ const commit = head();
254
+ return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
255
+ };
256
+ /**
257
+ * Undo the human's review-round code edits. A path changed since the human
258
+ * left it is not overwritten; requireRevert names it instead.
259
+ */
260
+ const reUnwind = async (feedback) => {
261
+ const { base, restoreFrom, edited } = feedback;
262
+ await run("re-unwind", restoreScript(base, {
263
+ restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
264
+ remove: edited.filter((c) => c.status === "added").map((c) => c.path)
265
+ }, restoreFrom), {
266
+ label: "Re-unwinding your review edit",
267
+ file: REVIEW,
268
+ base
269
+ });
270
+ requireRevert(edited, base);
271
+ };
272
+ /** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
273
+ const planAndBuild = async (firstBase) => {
274
+ let base = firstBase;
275
+ let carried;
276
+ for (;;) {
277
+ await design(base);
278
+ await architecture();
279
+ const built = await packages(carried);
280
+ carried = built.frozen;
281
+ const outcome = await buildTail(false, base, built);
282
+ if (outcome.verdict === "signoff") return;
283
+ await reUnwind(outcome);
284
+ base = outcome.base;
285
+ }
286
+ };
287
+ /** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
288
+ const afterTail = async (outcome) => {
289
+ if (outcome.verdict === "signoff") return;
290
+ await reUnwind(outcome);
291
+ await planAndBuild(outcome.base);
292
+ };
293
+ /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
294
+ const ordinaryStart = async () => {
295
+ await human("idle", {
296
+ message: idleMessage(),
297
+ label: "Idle",
298
+ file: ".gtd/TODO.md"
299
+ });
300
+ refusePendingHandOff();
301
+ await unwind();
302
+ if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
303
+ message: unwindFailedMessage(),
304
+ label: "Could not unwind your input",
305
+ file: FEEDBACK
306
+ });
307
+ await gate("start-gate", startGateBlockedMessage());
308
+ await planAndBuild(start());
309
+ };
310
+ const feature = shipped(ordinaryStart);
311
+ //#endregion
312
+ //#region src/workflows/feature/index.ts
313
+ var feature_default = feature;
314
+ //#endregion
315
+ export { SCENARIO_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };