@really-knows-ai/foundry 2.3.2 → 3.0.1

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 (170) hide show
  1. package/README.md +180 -369
  2. package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
  3. package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
  4. package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
  5. package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
  6. package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
  7. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
  8. package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
  9. package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
  10. package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
  11. package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
  12. package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
  13. package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
  14. package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
  15. package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
  16. package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
  17. package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
  18. package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
  19. package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
  20. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
  21. package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
  22. package/dist/.opencode/plugins/foundry.js +105 -0
  23. package/dist/CHANGELOG.md +533 -0
  24. package/dist/LICENSE +21 -0
  25. package/dist/README.md +278 -0
  26. package/dist/docs/README.md +59 -0
  27. package/dist/docs/architecture.md +433 -0
  28. package/dist/docs/concepts.md +395 -0
  29. package/dist/docs/getting-started.md +344 -0
  30. package/dist/docs/memory-maintenance.md +176 -0
  31. package/dist/docs/tools.md +1411 -0
  32. package/dist/docs/work-spec.md +283 -0
  33. package/dist/scripts/lib/artefacts.js +151 -0
  34. package/dist/scripts/lib/assay/loader.js +151 -0
  35. package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
  36. package/dist/scripts/lib/assay/permissions.js +52 -0
  37. package/dist/scripts/lib/assay/run.js +219 -0
  38. package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
  39. package/dist/scripts/lib/attestation/attest.js +111 -0
  40. package/dist/scripts/lib/attestation/canonical-json.js +109 -0
  41. package/dist/scripts/lib/attestation/hash.js +17 -0
  42. package/dist/scripts/lib/attestation/parse.js +14 -0
  43. package/dist/scripts/lib/attestation/payload.js +106 -0
  44. package/dist/scripts/lib/attestation/render.js +16 -0
  45. package/dist/scripts/lib/attestation/verify.js +15 -0
  46. package/dist/scripts/lib/branch-guard.js +72 -0
  47. package/dist/scripts/lib/config-creators/appraiser.js +9 -0
  48. package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
  49. package/dist/scripts/lib/config-creators/cycle.js +11 -0
  50. package/dist/scripts/lib/config-creators/factory.js +49 -0
  51. package/dist/scripts/lib/config-creators/flow.js +11 -0
  52. package/dist/scripts/lib/config-validators/appraiser.js +49 -0
  53. package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
  54. package/dist/scripts/lib/config-validators/cycle.js +131 -0
  55. package/dist/scripts/lib/config-validators/flow.js +57 -0
  56. package/dist/scripts/lib/config-validators/helpers.js +96 -0
  57. package/dist/scripts/lib/config-validators/law.js +96 -0
  58. package/dist/scripts/lib/config.js +328 -0
  59. package/dist/scripts/lib/failed-flow.js +131 -0
  60. package/dist/scripts/lib/feedback-store.js +249 -0
  61. package/dist/scripts/lib/feedback-transitions.js +105 -0
  62. package/dist/scripts/lib/finalize.js +70 -0
  63. package/dist/scripts/lib/foundational-guards.js +13 -0
  64. package/dist/scripts/lib/git-bridge.js +77 -0
  65. package/dist/scripts/lib/git-finish/work-finish.js +233 -0
  66. package/dist/scripts/lib/git-policy.js +101 -0
  67. package/dist/scripts/lib/guards.js +125 -0
  68. package/dist/scripts/lib/history.js +132 -0
  69. package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
  70. package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
  71. package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
  72. package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
  73. package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
  74. package/dist/scripts/lib/memory/admin/dump.js +47 -0
  75. package/dist/scripts/lib/memory/admin/helpers.js +31 -0
  76. package/dist/scripts/lib/memory/admin/init.js +170 -0
  77. package/dist/scripts/lib/memory/admin/live-store.js +76 -0
  78. package/dist/scripts/lib/memory/admin/reembed.js +285 -0
  79. package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
  80. package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
  81. package/dist/scripts/lib/memory/admin/reset.js +24 -0
  82. package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
  83. package/dist/scripts/lib/memory/admin/validate.js +19 -0
  84. package/dist/scripts/lib/memory/config.js +149 -0
  85. package/dist/scripts/lib/memory/cozo.js +136 -0
  86. package/dist/scripts/lib/memory/drift.js +71 -0
  87. package/dist/scripts/lib/memory/embeddings.js +128 -0
  88. package/dist/scripts/lib/memory/frontmatter.js +75 -0
  89. package/dist/scripts/lib/memory/ndjson.js +84 -0
  90. package/dist/scripts/lib/memory/paths.js +25 -0
  91. package/dist/scripts/lib/memory/permissions.js +41 -0
  92. package/dist/scripts/lib/memory/prompt.js +109 -0
  93. package/dist/scripts/lib/memory/query.js +56 -0
  94. package/dist/scripts/lib/memory/reads.js +109 -0
  95. package/dist/scripts/lib/memory/schema.js +64 -0
  96. package/dist/scripts/lib/memory/search.js +73 -0
  97. package/dist/scripts/lib/memory/singleton.js +49 -0
  98. package/dist/scripts/lib/memory/store.js +162 -0
  99. package/dist/scripts/lib/memory/types.js +93 -0
  100. package/dist/scripts/lib/memory/validate.js +58 -0
  101. package/dist/scripts/lib/memory/writes.js +40 -0
  102. package/{scripts → dist/scripts}/lib/pending.js +7 -2
  103. package/dist/scripts/lib/secret.js +59 -0
  104. package/{scripts → dist/scripts}/lib/slug.js +3 -2
  105. package/dist/scripts/lib/snapshot/finish.js +103 -0
  106. package/dist/scripts/lib/snapshot/inspect.js +253 -0
  107. package/dist/scripts/lib/snapshot/render.js +55 -0
  108. package/dist/scripts/lib/sort-fs-check.js +121 -0
  109. package/dist/scripts/lib/sort-routing.js +101 -0
  110. package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
  111. package/{scripts → dist/scripts}/lib/state.js +4 -0
  112. package/dist/scripts/lib/token.js +57 -0
  113. package/dist/scripts/lib/tracing.js +59 -0
  114. package/dist/scripts/lib/ulid.js +100 -0
  115. package/dist/scripts/lib/validator-jsonl.js +162 -0
  116. package/{scripts → dist/scripts}/lib/workfile.js +38 -20
  117. package/dist/scripts/orchestrate-cycle.js +215 -0
  118. package/dist/scripts/orchestrate-phases.js +314 -0
  119. package/dist/scripts/orchestrate.js +163 -0
  120. package/dist/scripts/sort.js +278 -0
  121. package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
  122. package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
  123. package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
  124. package/dist/skills/add-extractor/SKILL.md +133 -0
  125. package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
  126. package/dist/skills/add-law/SKILL.md +191 -0
  127. package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
  128. package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
  129. package/{skills → dist/skills}/appraise/SKILL.md +62 -13
  130. package/dist/skills/assay/SKILL.md +72 -0
  131. package/dist/skills/change-embedding-model/SKILL.md +58 -0
  132. package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
  133. package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
  134. package/dist/skills/dry-run/SKILL.md +116 -0
  135. package/{skills → dist/skills}/flow/SKILL.md +15 -2
  136. package/dist/skills/forge/SKILL.md +121 -0
  137. package/dist/skills/human-appraise/SKILL.md +153 -0
  138. package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
  139. package/dist/skills/init-memory/SKILL.md +92 -0
  140. package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
  141. package/dist/skills/quench/SKILL.md +99 -0
  142. package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
  143. package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
  144. package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
  145. package/dist/skills/reset-memory/SKILL.md +54 -0
  146. package/dist/skills/upgrade-foundry/SKILL.md +191 -0
  147. package/package.json +34 -17
  148. package/.opencode/plugins/foundry.js +0 -761
  149. package/CHANGELOG.md +0 -100
  150. package/docs/concepts.md +0 -122
  151. package/docs/getting-started.md +0 -187
  152. package/docs/work-spec.md +0 -207
  153. package/scripts/lib/artefacts.js +0 -124
  154. package/scripts/lib/config.js +0 -175
  155. package/scripts/lib/feedback-transitions.js +0 -25
  156. package/scripts/lib/feedback.js +0 -440
  157. package/scripts/lib/finalize.js +0 -41
  158. package/scripts/lib/history.js +0 -59
  159. package/scripts/lib/secret.js +0 -23
  160. package/scripts/lib/tags.js +0 -108
  161. package/scripts/lib/token.js +0 -26
  162. package/scripts/orchestrate.js +0 -418
  163. package/scripts/sort.js +0 -370
  164. package/scripts/validate-tags.js +0 -54
  165. package/skills/add-law/SKILL.md +0 -111
  166. package/skills/forge/SKILL.md +0 -88
  167. package/skills/human-appraise/SKILL.md +0 -82
  168. package/skills/quench/SKILL.md +0 -62
  169. package/skills/upgrade-foundry/SKILL.md +0 -216
  170. /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
@@ -1,440 +0,0 @@
1
- /**
2
- * Feedback parsing and manipulation utilities for WORK.md.
3
- */
4
-
5
- import { extractAllTags } from './tags.js';
6
- import { validateTransition, hashText } from './feedback-transitions.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Parsing
10
- // ---------------------------------------------------------------------------
11
-
12
- export function parseFeedbackItem(line) {
13
- const item = { raw: line, state: 'unknown', tags: [], resolved: false };
14
-
15
- if (line.startsWith('- [ ]')) {
16
- item.state = 'open';
17
- } else if (line.startsWith('- [x]')) {
18
- item.state = 'actioned';
19
- } else if (line.startsWith('- [~]')) {
20
- item.state = 'wont-fix';
21
- }
22
-
23
- if (line.includes('| approved')) {
24
- item.resolved = true;
25
- } else if (line.includes('| rejected')) {
26
- item.state = 'rejected';
27
- item.resolved = false;
28
- }
29
-
30
- item.tags = extractAllTags(line);
31
-
32
- return item;
33
- }
34
-
35
- export function parseFeedback(text, cycle, artefacts) {
36
- const cycleFiles = new Set();
37
- for (const art of artefacts) {
38
- if (art.cycle === cycle) {
39
- cycleFiles.add(art.file || '');
40
- }
41
- }
42
- const filterByFile = cycleFiles.size > 0;
43
-
44
- const items = [];
45
- let currentFile = null;
46
- let inFeedback = false;
47
- let feedbackLevel = 0;
48
-
49
- for (const line of text.split('\n')) {
50
- const stripped = line.trim();
51
-
52
- if (stripped === '# Feedback' || stripped === '## Feedback') {
53
- inFeedback = true;
54
- feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
55
- continue;
56
- }
57
-
58
- // Exit feedback on a heading at the same or higher level
59
- if (inFeedback && /^#{1,2} /.test(stripped)) {
60
- const level = stripped.startsWith('## ') ? 2 : 1;
61
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
62
- inFeedback = false;
63
- continue;
64
- }
65
- }
66
-
67
- if (!inFeedback) continue;
68
-
69
- // File sub-headings are one level below the Feedback heading
70
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
71
- if (stripped.startsWith(fileHeadingPrefix)) {
72
- currentFile = stripped.slice(fileHeadingPrefix.length).trim();
73
- continue;
74
- }
75
-
76
- if ((!filterByFile || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
77
- items.push(parseFeedbackItem(stripped));
78
- }
79
- }
80
-
81
- return items;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Manipulation
86
- // ---------------------------------------------------------------------------
87
-
88
- export function addFeedbackItem(text, file, itemText, tag) {
89
- // Dedup by (file, tag, text hash): if any existing item under this file
90
- // heading has the same tag and the same itemText, return without mutating.
91
- const existing = collectItemsForFile(text, file);
92
- const h = hashText(itemText);
93
- for (const ex of existing) {
94
- if (ex.tags.includes(`#${tag}`) && hashText(ex.coreText) === h) {
95
- return { text, deduped: true };
96
- }
97
- }
98
-
99
- const newItem = `- [ ] ${itemText} #${tag}`;
100
- const lines = text.split('\n');
101
-
102
- // Find ## Feedback section
103
- let feedbackIdx = -1;
104
- let feedbackLevel = 0;
105
- for (let i = 0; i < lines.length; i++) {
106
- const stripped = lines[i].trim();
107
- if (stripped === '## Feedback') {
108
- feedbackIdx = i;
109
- feedbackLevel = 2;
110
- break;
111
- }
112
- if (stripped === '# Feedback') {
113
- feedbackIdx = i;
114
- feedbackLevel = 1;
115
- break;
116
- }
117
- }
118
-
119
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
120
- const fileHeading = `${fileHeadingPrefix}${file}`;
121
-
122
- if (feedbackIdx === -1) {
123
- // No Feedback section — append one
124
- lines.push('', '## Feedback', '', `### ${file}`, newItem);
125
- return { text: lines.join('\n'), deduped: false };
126
- }
127
-
128
- // Find the file heading within the feedback section
129
- let fileIdx = -1;
130
- let sectionEnd = lines.length; // end of feedback section
131
- for (let i = feedbackIdx + 1; i < lines.length; i++) {
132
- const stripped = lines[i].trim();
133
- // Check if we've left the feedback section
134
- if (/^#{1,2} /.test(stripped)) {
135
- const level = stripped.startsWith('## ') ? 2 : 1;
136
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
137
- sectionEnd = i;
138
- break;
139
- }
140
- }
141
- if (stripped === fileHeading.trim()) {
142
- fileIdx = i;
143
- }
144
- }
145
-
146
- if (fileIdx === -1) {
147
- // File heading doesn't exist — add it before section end
148
- lines.splice(sectionEnd, 0, '', fileHeading, newItem);
149
- return { text: lines.join('\n'), deduped: false };
150
- }
151
-
152
- // Find last item under this file heading
153
- let insertIdx = fileIdx + 1;
154
- for (let i = fileIdx + 1; i < sectionEnd; i++) {
155
- const stripped = lines[i].trim();
156
- if (stripped.startsWith(fileHeadingPrefix)) break; // next file heading
157
- if (/^- \[/.test(stripped)) {
158
- insertIdx = i + 1;
159
- }
160
- }
161
-
162
- lines.splice(insertIdx, 0, newItem);
163
- return { text: lines.join('\n'), deduped: false };
164
- }
165
-
166
- export function actionFeedbackItem(text, file, index, stageBase) {
167
- return transformFeedbackItemWithValidation(text, file, index, 'actioned', stageBase, (line) =>
168
- line.replace('- [ ]', '- [x]')
169
- );
170
- }
171
-
172
- export function wontfixFeedbackItem(text, file, index, reason, stageBase) {
173
- return transformFeedbackItemWithValidation(text, file, index, 'wont-fix', stageBase, (line) =>
174
- line.replace('- [ ]', '- [~]') + ` | wont-fix: ${reason}`
175
- );
176
- }
177
-
178
- export function resolveFeedbackItem(text, file, index, resolution, reason, stageBase) {
179
- return transformFeedbackItemWithValidation(text, file, index, resolution, stageBase, (line) => {
180
- if (resolution === 'approved') {
181
- return line + ' | approved';
182
- }
183
- return line + ` | rejected: ${reason}`;
184
- });
185
- }
186
-
187
- // ---------------------------------------------------------------------------
188
- // Listing
189
- // ---------------------------------------------------------------------------
190
-
191
- export function listFeedback(text, cycle, artefacts, filterFile) {
192
- const cycleFiles = new Set();
193
- for (const art of artefacts) {
194
- if (art.cycle === cycle) {
195
- cycleFiles.add(art.file || '');
196
- }
197
- }
198
- const filterByCycle = cycleFiles.size > 0;
199
-
200
- const results = [];
201
- let currentFile = null;
202
- let fileIndex = 0;
203
- let inFeedback = false;
204
- let feedbackLevel = 0;
205
-
206
- for (const line of text.split('\n')) {
207
- const stripped = line.trim();
208
-
209
- if (stripped === '# Feedback' || stripped === '## Feedback') {
210
- inFeedback = true;
211
- feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
212
- continue;
213
- }
214
-
215
- if (inFeedback && /^#{1,2} /.test(stripped)) {
216
- const level = stripped.startsWith('## ') ? 2 : 1;
217
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
218
- inFeedback = false;
219
- continue;
220
- }
221
- }
222
-
223
- if (!inFeedback) continue;
224
-
225
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
226
- if (stripped.startsWith(fileHeadingPrefix)) {
227
- currentFile = stripped.slice(fileHeadingPrefix.length).trim();
228
- fileIndex = 0;
229
- continue;
230
- }
231
-
232
- if ((!filterByCycle || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
233
- if (!filterFile || filterFile === currentFile) {
234
- const item = parseFeedbackItem(stripped);
235
- results.push({
236
- file: currentFile,
237
- index: fileIndex,
238
- text: item.raw,
239
- state: item.state,
240
- tags: item.tags,
241
- resolved: item.resolved,
242
- });
243
- }
244
- fileIndex++;
245
- }
246
- }
247
-
248
- return results;
249
- }
250
-
251
- /**
252
- * Detect feedback items stuck in a deadlock — rejected N or more times.
253
- * A deadlock occurs when forge-appraise cycles keep rejecting the same item.
254
- */
255
- export function detectDeadlocks(feedback, history, threshold = 3) {
256
- // Count forge→appraise cycles (each pair = one iteration)
257
- const forgeAppraiseCount = history.filter(
258
- e => (e.stage || '').split(':')[0] === 'appraise'
259
- ).length;
260
-
261
- if (forgeAppraiseCount < threshold) return [];
262
-
263
- // Items that are still rejected after threshold iterations are deadlocked
264
- return feedback.filter(f => f.state === 'rejected' || f.state === 'open');
265
- }
266
-
267
- // ---------------------------------------------------------------------------
268
- // Internal helpers
269
- // ---------------------------------------------------------------------------
270
-
271
- /**
272
- * Collect feedback items under a specific file heading, returning the parsed
273
- * representation plus the "core text" (item body with tag and trailing resolution
274
- * stripped) for dedup hashing.
275
- */
276
- function collectItemsForFile(text, file) {
277
- const items = [];
278
- const lines = text.split('\n');
279
- let inFeedback = false;
280
- let feedbackLevel = 0;
281
- let currentFile = null;
282
-
283
- for (const line of lines) {
284
- const stripped = line.trim();
285
-
286
- if (stripped === '# Feedback' || stripped === '## Feedback') {
287
- inFeedback = true;
288
- feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
289
- continue;
290
- }
291
-
292
- if (inFeedback && /^#{1,2} /.test(stripped)) {
293
- const level = stripped.startsWith('## ') ? 2 : 1;
294
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
295
- inFeedback = false;
296
- continue;
297
- }
298
- }
299
-
300
- if (!inFeedback) continue;
301
-
302
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
303
- if (stripped.startsWith(fileHeadingPrefix)) {
304
- currentFile = stripped.slice(fileHeadingPrefix.length).trim();
305
- continue;
306
- }
307
-
308
- if (currentFile === file && /^- \[/.test(stripped)) {
309
- const parsed = parseFeedbackItem(stripped);
310
- // Strip checkbox, tags, and trailing `| approved` / `| rejected: ...` /
311
- // `| wont-fix: ...` to get the core author-supplied text for dedup.
312
- let core = stripped.replace(/^- \[[ x~]\]\s*/, '');
313
- core = core.replace(/\s*\|\s*(approved|rejected[^|]*|wont-fix[^|]*)\s*$/, '');
314
- for (const t of parsed.tags) {
315
- core = core.replace(t, '');
316
- }
317
- core = core.trim();
318
- items.push({ line: stripped, state: parsed.state, tags: parsed.tags, coreText: core });
319
- }
320
- }
321
-
322
- return items;
323
- }
324
-
325
- /**
326
- * Read the line at (file, index) and return its current feedback state
327
- * (or null if not found).
328
- */
329
- function readItemState(text, file, index) {
330
- const lines = text.split('\n');
331
- let inFeedback = false;
332
- let feedbackLevel = 0;
333
- let currentFile = null;
334
- let fileIndex = 0;
335
-
336
- for (let i = 0; i < lines.length; i++) {
337
- const stripped = lines[i].trim();
338
-
339
- if (stripped === '# Feedback' || stripped === '## Feedback') {
340
- inFeedback = true;
341
- feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
342
- continue;
343
- }
344
-
345
- if (inFeedback && /^#{1,2} /.test(stripped)) {
346
- const level = stripped.startsWith('## ') ? 2 : 1;
347
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
348
- inFeedback = false;
349
- continue;
350
- }
351
- }
352
-
353
- if (!inFeedback) continue;
354
-
355
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
356
- if (stripped.startsWith(fileHeadingPrefix)) {
357
- currentFile = stripped.slice(fileHeadingPrefix.length).trim();
358
- fileIndex = 0;
359
- continue;
360
- }
361
-
362
- if (currentFile === file && /^- \[/.test(stripped)) {
363
- if (fileIndex === index) {
364
- const parsed = parseFeedbackItem(stripped);
365
- // Map parseFeedbackItem's (state, resolved) pair onto state-machine states:
366
- // - `| approved` → terminal "approved"
367
- // - `| rejected` → "rejected" (parseFeedbackItem already sets this)
368
- // - bare `[x]` → "actioned"
369
- // - bare `[~]` → "wont-fix"
370
- // - bare `[ ]` → "open"
371
- if (parsed.resolved) return 'approved';
372
- return parsed.state;
373
- }
374
- fileIndex++;
375
- }
376
- }
377
- return null;
378
- }
379
-
380
- function transformFeedbackItemWithValidation(text, file, index, target, stageBase, transform) {
381
- if (stageBase !== undefined) {
382
- const current = readItemState(text, file, index);
383
- if (!current) {
384
- return { ok: false, error: `feedback item not found: file=${file} index=${index}` };
385
- }
386
- const v = validateTransition(current, target, stageBase);
387
- if (!v.ok) {
388
- return { ok: false, error: v.reason };
389
- }
390
- const updated = transformFeedbackItem(text, file, index, transform);
391
- return { ok: true, text: updated };
392
- }
393
- // Backward-compatible path: return plain string.
394
- return transformFeedbackItem(text, file, index, transform);
395
- }
396
-
397
- function transformFeedbackItem(text, file, index, transform) {
398
- const lines = text.split('\n');
399
- let inFeedback = false;
400
- let feedbackLevel = 0;
401
- let currentFile = null;
402
- let fileIndex = 0;
403
-
404
- for (let i = 0; i < lines.length; i++) {
405
- const stripped = lines[i].trim();
406
-
407
- if (stripped === '# Feedback' || stripped === '## Feedback') {
408
- inFeedback = true;
409
- feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
410
- continue;
411
- }
412
-
413
- if (inFeedback && /^#{1,2} /.test(stripped)) {
414
- const level = stripped.startsWith('## ') ? 2 : 1;
415
- if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
416
- inFeedback = false;
417
- continue;
418
- }
419
- }
420
-
421
- if (!inFeedback) continue;
422
-
423
- const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
424
- if (stripped.startsWith(fileHeadingPrefix)) {
425
- currentFile = stripped.slice(fileHeadingPrefix.length).trim();
426
- fileIndex = 0;
427
- continue;
428
- }
429
-
430
- if (currentFile === file && /^- \[/.test(stripped)) {
431
- if (fileIndex === index) {
432
- lines[i] = transform(lines[i]);
433
- return lines.join('\n');
434
- }
435
- fileIndex++;
436
- }
437
- }
438
-
439
- return text;
440
- }
@@ -1,41 +0,0 @@
1
- // scripts/lib/finalize.js
2
- import { execSync } from 'node:child_process';
3
- import { minimatch } from 'minimatch';
4
-
5
- const TOOL_MANAGED = [
6
- 'WORK.md',
7
- 'WORK.history.yaml',
8
- ];
9
- const TOOL_MANAGED_PREFIX = ['.foundry/'];
10
-
11
- function changedFiles(cwd, baseSha) {
12
- const tracked = execSync(`git diff --name-only ${baseSha} HEAD`, { cwd }).toString().split('\n').filter(Boolean);
13
- const diffUnstaged = execSync('git diff --name-only', { cwd }).toString().split('\n').filter(Boolean);
14
- const untracked = execSync('git ls-files --others --exclude-standard', { cwd }).toString().split('\n').filter(Boolean);
15
- return [...new Set([...tracked, ...diffUnstaged, ...untracked])];
16
- }
17
-
18
- function isToolManaged(f) {
19
- if (TOOL_MANAGED.includes(f)) return true;
20
- return TOOL_MANAGED_PREFIX.some(p => f.startsWith(p));
21
- }
22
-
23
- export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, registerArtefact }) {
24
- const files = changedFiles(cwd, baseSha).filter(f => !isToolManaged(f));
25
- const allowedPatterns = stageBase === 'forge'
26
- ? (artefactTypes[cycleDef.outputArtefactType]?.filePatterns ?? [])
27
- : [];
28
- const unexpected = [];
29
- const matched = [];
30
- for (const f of files) {
31
- const hit = allowedPatterns.find(p => minimatch(f, p));
32
- if (hit) matched.push(f);
33
- else unexpected.push(f);
34
- }
35
- if (unexpected.length) return { ok: false, error: 'unexpected_files', files: unexpected };
36
- const artefacts = matched.map(file => {
37
- registerArtefact({ file, type: cycleDef.outputArtefactType, status: 'draft' });
38
- return { file, type: cycleDef.outputArtefactType, status: 'draft' };
39
- });
40
- return { ok: true, artefacts };
41
- }
@@ -1,59 +0,0 @@
1
- import yaml from 'js-yaml';
2
-
3
- /**
4
- * Load history entries for a cycle, sorted by timestamp ascending.
5
- */
6
- export function loadHistory(historyPath, cycle, io) {
7
- if (!io.exists(historyPath)) return [];
8
- const data = yaml.load(io.readFile(historyPath)) || [];
9
- const filtered = data.filter(e => e.cycle === cycle);
10
- filtered.sort((a, b) => {
11
- const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
12
- const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
13
- return ta - tb;
14
- });
15
- return filtered;
16
- }
17
-
18
- /**
19
- * Append a history entry with auto-generated ISO timestamp.
20
- */
21
- export function appendEntry(historyPath, { cycle, stage, iteration, comment, route }, io) {
22
- if (iteration == null) throw new Error('iteration is required');
23
- if (!comment) throw new Error('comment is required');
24
-
25
- let existing = [];
26
- if (io.exists(historyPath)) {
27
- existing = yaml.load(io.readFile(historyPath)) || [];
28
- }
29
-
30
- const entry = {
31
- cycle,
32
- stage,
33
- iteration,
34
- comment,
35
- timestamp: new Date().toISOString(),
36
- };
37
- if (route !== undefined) entry.route = route;
38
- existing.push(entry);
39
-
40
- io.writeFile(historyPath, yaml.dump(existing));
41
- }
42
-
43
- /**
44
- * Count forge entries for a cycle.
45
- */
46
- export function getIteration(historyPath, cycle, io) {
47
- const history = loadHistory(historyPath, cycle, io);
48
- return history.filter(e => (e.stage || '').split(':')[0] === 'forge').length;
49
- }
50
-
51
- /**
52
- * Return the `route` field from the most recent `sort` history entry for a
53
- * given cycle, or null if none exists.
54
- */
55
- export function readLastSortRoute(historyPath, cycle, io) {
56
- const entries = loadHistory(historyPath, cycle, io).filter(e => e.stage === 'sort');
57
- if (!entries.length) return null;
58
- return entries[entries.length - 1].route ?? null;
59
- }
@@ -1,23 +0,0 @@
1
- import { existsSync, readFileSync, mkdirSync, openSync, writeSync, closeSync } from 'node:fs';
2
- import { randomBytes } from 'node:crypto';
3
- import { join } from 'node:path';
4
-
5
- export function readOrCreateSecret(directory) {
6
- const dir = join(directory, '.foundry');
7
- const file = join(dir, '.secret');
8
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9
- const bytes = randomBytes(32);
10
- let fd;
11
- try {
12
- fd = openSync(file, 'wx', 0o600);
13
- } catch (err) {
14
- if (err.code === 'EEXIST') return readFileSync(file);
15
- throw err;
16
- }
17
- try {
18
- writeSync(fd, bytes);
19
- } finally {
20
- closeSync(fd);
21
- }
22
- return bytes;
23
- }
@@ -1,108 +0,0 @@
1
- /**
2
- * Shared tag validation utilities used by sort.js and validate-tags.js.
3
- */
4
-
5
- import { readFileSync, existsSync, readdirSync } from 'fs';
6
- import { join } from 'path';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Constants
10
- // ---------------------------------------------------------------------------
11
-
12
- export const VALID_TAG_RE = /^(#validation|#hitl|#law:[\w-]+)$/;
13
-
14
- // ---------------------------------------------------------------------------
15
- // Law collection
16
- // ---------------------------------------------------------------------------
17
-
18
- export function collectLawIds(foundryDir) {
19
- const ids = new Set();
20
-
21
- const lawsDir = join(foundryDir, 'laws');
22
- if (existsSync(lawsDir)) {
23
- for (const file of readdirSync(lawsDir)) {
24
- if (!file.endsWith('.md')) continue;
25
- const text = readFileSync(join(lawsDir, file), 'utf-8');
26
- for (const id of extractLawHeadings(text)) ids.add(id);
27
- }
28
- }
29
-
30
- const artefactsDir = join(foundryDir, 'artefacts');
31
- if (existsSync(artefactsDir)) {
32
- for (const typeDir of readdirSync(artefactsDir)) {
33
- const lawsPath = join(artefactsDir, typeDir, 'laws.md');
34
- if (!existsSync(lawsPath)) continue;
35
- const text = readFileSync(lawsPath, 'utf-8');
36
- for (const id of extractLawHeadings(text)) ids.add(id);
37
- }
38
- }
39
-
40
- return ids;
41
- }
42
-
43
- export function extractLawHeadings(text) {
44
- const ids = [];
45
- for (const line of text.split('\n')) {
46
- const match = line.match(/^## (.+)$/);
47
- if (match) ids.push(match[1].trim());
48
- }
49
- return ids;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Tag extraction
54
- // ---------------------------------------------------------------------------
55
-
56
- /**
57
- * Extract all hash-tags from a feedback line.
58
- * Returns an array of strings like ['#validation', '#law:brevity'].
59
- */
60
- export function extractAllTags(line) {
61
- return (line.match(/#[\w][\w:-]*/g) || []);
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Validation
66
- // ---------------------------------------------------------------------------
67
-
68
- /**
69
- * Validate all feedback tags in the Feedback section of WORK.md text.
70
- *
71
- * Returns an array of error strings. Empty array = all valid.
72
- */
73
- export function validateTags(workText, foundryDir) {
74
- const lawIds = collectLawIds(foundryDir);
75
- const errors = [];
76
- let inFeedback = false;
77
- let lineNum = 0;
78
-
79
- for (const line of workText.split('\n')) {
80
- lineNum++;
81
- const stripped = line.trim();
82
-
83
- if (stripped === '# Feedback') { inFeedback = true; continue; }
84
- if (inFeedback && stripped.startsWith('# ') && stripped !== '# Feedback') {
85
- inFeedback = false; continue;
86
- }
87
- if (!inFeedback || !(/^- \[/.test(stripped))) continue;
88
-
89
- const tags = extractAllTags(stripped);
90
- if (tags.length === 0) {
91
- errors.push({ line: lineNum, message: 'Feedback item has no tag', raw: stripped });
92
- continue;
93
- }
94
-
95
- for (const tag of tags) {
96
- if (!VALID_TAG_RE.test(tag)) {
97
- errors.push({ line: lineNum, message: `Unknown tag: ${tag}`, raw: stripped });
98
- } else if (tag.startsWith('#law:')) {
99
- const lawId = tag.slice(5);
100
- if (!lawIds.has(lawId)) {
101
- errors.push({ line: lineNum, message: `Law not found: ${lawId}`, raw: stripped });
102
- }
103
- }
104
- }
105
- }
106
-
107
- return errors;
108
- }
@@ -1,26 +0,0 @@
1
- import { createHmac, timingSafeEqual } from 'node:crypto';
2
-
3
- export function signToken(payload, secret) {
4
- const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
5
- const mac = createHmac('sha256', secret).update(body).digest('base64url');
6
- return `${body}.${mac}`;
7
- }
8
-
9
- export function verifyToken(token, secret) {
10
- if (typeof token !== 'string' || !token.includes('.')) return { ok: false, reason: 'malformed' };
11
- const [body, mac] = token.split('.');
12
- if (!body || !mac) return { ok: false, reason: 'malformed' };
13
- const expected = createHmac('sha256', secret).update(body).digest();
14
- let given;
15
- try { given = Buffer.from(mac, 'base64url'); } catch { return { ok: false, reason: 'malformed' }; }
16
- if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
17
- return { ok: false, reason: 'bad_signature' };
18
- }
19
- let payload;
20
- try { payload = JSON.parse(Buffer.from(body, 'base64url').toString()); }
21
- catch { return { ok: false, reason: 'malformed' }; }
22
- if (typeof payload.exp !== 'number' || payload.exp < Date.now()) {
23
- return { ok: false, reason: 'expired' };
24
- }
25
- return { ok: true, payload };
26
- }