@really-knows-ai/foundry 2.3.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +490 -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 +434 -0
  28. package/dist/docs/concepts.md +396 -0
  29. package/dist/docs/getting-started.md +345 -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 +393 -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 +46 -24
  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 +192 -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
package/scripts/sort.js DELETED
@@ -1,370 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Sort — deterministic routing for a Foundry Cycle.
5
- *
6
- * Reads WORK.md (frontmatter + feedback) and WORK.history.yaml to determine
7
- * the next stage to execute, or signal completion/blocked.
8
- *
9
- * Usage:
10
- * node scripts/sort.js [--work WORK.md] [--history WORK.history.yaml]
11
- *
12
- * Output (stdout): a full stage alias (e.g., forge:write-haiku), 'done', or 'blocked'
13
- * Exit code: 0 on success, 1 on error
14
- */
15
-
16
- import { readFileSync, existsSync } from 'fs';
17
- import { execSync } from 'child_process';
18
- import yaml from 'js-yaml';
19
- import { minimatch } from 'minimatch';
20
- import { validateTags } from './lib/tags.js';
21
- import { parseFrontmatter } from './lib/workfile.js';
22
- import { parseArtefactsTable } from './lib/artefacts.js';
23
- import { loadHistory } from './lib/history.js';
24
- import { parseFeedback, parseFeedbackItem, detectDeadlocks } from './lib/feedback.js';
25
-
26
- // ---------------------------------------------------------------------------
27
- // Stage helpers
28
- // ---------------------------------------------------------------------------
29
-
30
- function baseStage(stage) {
31
- return stage.split(':')[0];
32
- }
33
-
34
- function findFirst(stages, base) {
35
- for (const s of stages) {
36
- if (baseStage(s) === base) return s;
37
- }
38
- return null;
39
- }
40
-
41
- function nextInRoute(stages, current) {
42
- const idx = stages.indexOf(current);
43
- if (idx !== -1 && idx + 1 < stages.length) {
44
- return stages[idx + 1];
45
- }
46
- return null;
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // I/O boundary — injectable for testing
51
- // ---------------------------------------------------------------------------
52
-
53
- const defaultIO = {
54
- readFile: (p) => readFileSync(p, 'utf-8'),
55
- exists: (p) => existsSync(p),
56
- exec: (cmd) => execSync(cmd, { encoding: 'utf8' }),
57
- };
58
-
59
- // ---------------------------------------------------------------------------
60
- // Parsing
61
- // ---------------------------------------------------------------------------
62
-
63
- // ---------------------------------------------------------------------------
64
- // Routing logic
65
- // ---------------------------------------------------------------------------
66
-
67
- function determineRoute(stages, history, feedback, maxIterations, opts = {}) {
68
- const forgeCount = history.filter(e => baseStage(e.stage || '') === 'forge').length;
69
-
70
- const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
71
- const lastEntry = nonSortHistory.length > 0 ? nonSortHistory[nonSortHistory.length - 1].stage : null;
72
- const lastBase = lastEntry ? baseStage(lastEntry) : null;
73
-
74
- if (lastBase === null) return stages[0];
75
-
76
- if (lastBase === 'forge') {
77
- const next = nextInRoute(stages, lastEntry);
78
- return next ?? 'done';
79
- }
80
-
81
- if (lastBase === 'quench') {
82
- return nextAfterQuench(stages, lastEntry, feedback, forgeCount, maxIterations);
83
- }
84
-
85
- if (lastBase === 'appraise') {
86
- return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory, opts);
87
- }
88
-
89
- if (lastBase === 'human-appraise') {
90
- return nextAfterAppraise(stages, lastEntry, feedback, forgeCount, maxIterations, nonSortHistory, opts);
91
- }
92
-
93
- return 'blocked';
94
- }
95
-
96
- function nextAfterQuench(stages, current, feedback, forgeCount, maxIterations) {
97
- const needsForge = feedback.some(f => f.state === 'open' || f.state === 'rejected');
98
- if (needsForge) {
99
- if (forgeCount >= maxIterations) return 'blocked';
100
- return findFirst(stages, 'forge') ?? 'blocked';
101
- }
102
-
103
- return nextInRoute(stages, current) ?? 'done';
104
- }
105
-
106
- function nextAfterAppraise(stages, current, feedback, forgeCount, maxIterations, history = [], opts = {}) {
107
- const {
108
- humanAppraise: humanAppraiseEnabled = false,
109
- deadlockAppraise = true,
110
- deadlockIterations = 5,
111
- cycle = null,
112
- } = opts;
113
-
114
- // Check for deadlock escalation using configured threshold
115
- const deadlocked = detectDeadlocks(feedback, history, deadlockIterations);
116
- if (deadlocked.length > 0) {
117
- const alreadyInHumanAppraise = baseStage(current) === 'human-appraise';
118
- if (alreadyInHumanAppraise) {
119
- // Human-appraise ran and deadlock still present — give up.
120
- return 'blocked';
121
- }
122
- if (deadlockAppraise) {
123
- // Route to human-appraise. Prefer one in `stages`; else synthesize via cycle id.
124
- const inStages = findFirst(stages, 'human-appraise');
125
- if (inStages) return inStages;
126
- if (cycle) return `human-appraise:${cycle}`;
127
- return 'blocked';
128
- }
129
- // deadlock-appraise disabled — block the cycle.
130
- return 'blocked';
131
- }
132
-
133
- const needsForge = feedback.some(f => f.state === 'open' || f.state === 'rejected');
134
- if (needsForge) {
135
- if (forgeCount >= maxIterations) return 'blocked';
136
- return findFirst(stages, 'forge') ?? 'blocked';
137
- }
138
-
139
- const pendingApproval = feedback.some(
140
- f => (f.state === 'actioned' || f.state === 'wont-fix') && !f.resolved
141
- );
142
- if (pendingApproval) {
143
- return findFirst(stages, 'appraise') ?? 'blocked';
144
- }
145
-
146
- return nextInRoute(stages, current) ?? 'done';
147
- }
148
-
149
- // ---------------------------------------------------------------------------
150
- // File modification enforcement
151
- // ---------------------------------------------------------------------------
152
-
153
- function getModifiedFiles(cycle, io = defaultIO) {
154
- try {
155
- // Find the last sort commit for this cycle to use as the base.
156
- // This captures ALL files modified since the last sort invocation,
157
- // even if the stage made multiple commits.
158
- const log = io.exec('git log --oneline -20');
159
- const sortPattern = `[${cycle}] sort:`;
160
- let commitCount = 1;
161
- let foundSortCommit = false;
162
- for (const line of log.trim().split('\n')) {
163
- commitCount++;
164
- if (line.includes(sortPattern)) {
165
- foundSortCommit = true;
166
- break;
167
- }
168
- }
169
- // If no sort commit found in recent history, fall back to HEAD~1
170
- if (!foundSortCommit) commitCount = 1;
171
- const output = io.exec(`git diff --name-only HEAD~${commitCount} HEAD`);
172
- return output.trim().split('\n').filter(Boolean);
173
- } catch {
174
- return [];
175
- }
176
- }
177
-
178
- function globMatch(filePath, pattern) {
179
- return minimatch(filePath, pattern);
180
- }
181
-
182
- function getAllowedPatterns(lastBase, foundryDir, cycleDef, io = defaultIO) {
183
- const always = ['WORK.md', 'WORK.history.yaml'];
184
-
185
- if (lastBase !== 'forge') {
186
- return always;
187
- }
188
-
189
- // For forge: also allow the output artefact's file-patterns
190
- try {
191
- const cycleText = io.readFile(cycleDef);
192
- const cycleFm = parseFrontmatter(cycleText);
193
- const outputType = cycleFm.output;
194
- if (!outputType) return always;
195
-
196
- const artDefPath = `${foundryDir}/artefacts/${outputType}/definition.md`;
197
- if (!io.exists(artDefPath)) return always;
198
-
199
- const artText = io.readFile(artDefPath);
200
- const artFm = parseFrontmatter(artText);
201
- const filePatterns = artFm['file-patterns'] || [];
202
- return [...always, ...filePatterns];
203
- } catch {
204
- return always;
205
- }
206
- }
207
-
208
- function checkModifiedFiles(lastBase, foundryDir, cycleDef, cycle, io = defaultIO) {
209
- const allowedPatterns = getAllowedPatterns(lastBase, foundryDir, cycleDef, io);
210
- const modified = getModifiedFiles(cycle, io);
211
-
212
- if (modified.length === 0) {
213
- return { ok: true, violations: [] };
214
- }
215
-
216
- const violations = modified.filter(f =>
217
- !allowedPatterns.some(pattern => globMatch(f, pattern))
218
- );
219
-
220
- return { ok: violations.length === 0, violations };
221
- }
222
-
223
- // ---------------------------------------------------------------------------
224
- // Micro-commit enforcement
225
- // ---------------------------------------------------------------------------
226
-
227
- /**
228
- * Return a list of tool-managed files that have uncommitted changes
229
- * (modified, staged, or untracked) in the working tree.
230
- *
231
- * Tool-managed files are WORK.md, WORK.history.yaml, and anything under
232
- * .foundry/. The sort skill is the sole writer of these between stages,
233
- * and every stage must end with `foundry_git_commit`. If this function
234
- * returns a non-empty list at the start of a sort invocation, a prior
235
- * stage skipped the commit step.
236
- */
237
- function getDirtyToolManagedFiles(io = defaultIO) {
238
- try {
239
- const output = io.exec('git status --porcelain -- WORK.md WORK.history.yaml .foundry');
240
- return output
241
- .split('\n')
242
- .map(line => line.trim())
243
- .filter(Boolean)
244
- .map(line => line.replace(/^[\sMADRCU?!]+/, '').trim())
245
- .filter(Boolean);
246
- } catch {
247
- return [];
248
- }
249
- }
250
-
251
- // ---------------------------------------------------------------------------
252
- // Exported runSort — structured result for programmatic use
253
- // ---------------------------------------------------------------------------
254
-
255
- function isDispatchableRoute(route) {
256
- return typeof route === 'string' && /^(forge|quench|appraise|human-appraise):/.test(route);
257
- }
258
-
259
- export function runSort({ workPath = 'WORK.md', historyPath = 'WORK.history.yaml', foundryDir = 'foundry', cycleDef, agentsDir = '.opencode/agents', mint, now = Date.now() } = {}, io = defaultIO) {
260
- if (!io.exists(workPath)) {
261
- return { route: 'blocked', details: 'WORK.md not found' };
262
- }
263
-
264
- const workText = io.readFile(workPath);
265
- const frontmatter = parseFrontmatter(workText);
266
-
267
- const cycle = frontmatter.cycle;
268
- const stages = frontmatter.stages;
269
- const maxIterations = frontmatter['max-iterations'] ?? 3;
270
- const humanAppraiseEnabled = frontmatter['human-appraise'] === true;
271
- const deadlockAppraise = frontmatter['deadlock-appraise'] !== false; // default true
272
- const deadlockIterations = frontmatter['deadlock-iterations'] ?? 5;
273
-
274
- if (!cycle) return { route: 'blocked', details: 'No cycle in WORK.md frontmatter' };
275
- if (!stages || !Array.isArray(stages)) return { route: 'blocked', details: 'No stages in WORK.md frontmatter' };
276
- if (!findFirst(stages, 'forge')) return { route: 'blocked', details: 'stages must include at least one forge stage' };
277
-
278
- const artefacts = parseArtefactsTable(workText);
279
- const history = loadHistory(historyPath, cycle, io);
280
- const feedback = parseFeedback(workText, cycle, artefacts);
281
-
282
- // Micro-commit enforcement: if any prior stage ran (history non-empty),
283
- // all tool-managed files must be committed before the next sort call.
284
- // The first sort of a cycle has empty history — WORK.md may be untracked
285
- // or dirty at that point, which is fine.
286
- if (history.length > 0) {
287
- const dirty = getDirtyToolManagedFiles(io);
288
- if (dirty.length > 0) {
289
- return {
290
- route: 'violation',
291
- details: `Uncommitted tool-managed files since last sort: ${dirty.join(', ')}. Call foundry_git_commit for the prior stage before invoking sort again.`,
292
- };
293
- }
294
- }
295
-
296
- // File modification enforcement
297
- const nonSortHistory = history.filter(e => baseStage(e.stage || '') !== 'sort');
298
- if (nonSortHistory.length > 0) {
299
- const lastEntry = nonSortHistory[nonSortHistory.length - 1];
300
- const lastBase = baseStage(lastEntry.stage || '');
301
- const resolvedCycleDef = cycleDef || frontmatter['cycle-def'] || `${foundryDir}/cycles/${cycle}.md`;
302
- const result = checkModifiedFiles(lastBase, foundryDir, resolvedCycleDef, cycle, io);
303
- if (!result.ok) {
304
- return { route: 'violation', details: `File modification violation after ${lastBase} stage: ${result.violations.join(', ')}` };
305
- }
306
- }
307
-
308
- // Tag validation
309
- const tagErrors = validateTags(workText, foundryDir);
310
- if (tagErrors.length > 0) {
311
- const details = tagErrors.map(e => `line ${e.line}: ${e.message}`).join('; ');
312
- return { route: 'violation', details: `Feedback tag validation failed: ${details}` };
313
- }
314
-
315
- const route = determineRoute(stages, history, feedback, maxIterations, {
316
- humanAppraise: humanAppraiseEnabled,
317
- deadlockAppraise,
318
- deadlockIterations,
319
- cycle,
320
- });
321
-
322
- // Model resolution
323
- let model = null;
324
- const routeBase = baseStage(route);
325
- if (frontmatter.models && frontmatter.models[routeBase]) {
326
- const modelId = frontmatter.models[routeBase];
327
- model = `foundry-${modelId.replace(/[/.]/g, '-')}`;
328
-
329
- // Fail-fast: required subagent file must exist
330
- const agentPath = `${agentsDir}/${model}.md`;
331
- if (!io.exists(agentPath)) {
332
- return {
333
- route: 'violation',
334
- details: `Missing required subagent: ${model}.md is not present in ${agentsDir}/. Run the refresh-agents skill to regenerate agent files, then restart.`,
335
- };
336
- }
337
- }
338
-
339
- const result = { route, ...(model ? { model } : {}) };
340
- if (mint && isDispatchableRoute(route)) {
341
- const token = mint({ route, cycle, exp: now + 10 * 60 * 1000 });
342
- if (token) result.token = token;
343
- }
344
- return result;
345
- }
346
-
347
- // ---------------------------------------------------------------------------
348
- // Exports (for testing) — keep main() private
349
- // ---------------------------------------------------------------------------
350
-
351
- export { parseArtefactsTable } from './lib/artefacts.js';
352
- export { loadHistory } from './lib/history.js';
353
- export { parseFeedback, parseFeedbackItem } from './lib/feedback.js';
354
-
355
- export {
356
- baseStage,
357
- findFirst,
358
- nextInRoute,
359
- parseFrontmatter,
360
- determineRoute,
361
- nextAfterQuench,
362
- nextAfterAppraise,
363
- globMatch,
364
- getModifiedFiles,
365
- getAllowedPatterns,
366
- checkModifiedFiles,
367
- getDirtyToolManagedFiles,
368
- };
369
-
370
-
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Validate feedback tags in WORK.md.
5
- *
6
- * Checks that every feedback item tag:
7
- * 1. Matches allowed syntax: #validation, #law:<id>, or #hitl
8
- * 2. For #law:<id>, the law id exists in foundry/laws/*.md or the
9
- * relevant artefact type's laws.md
10
- *
11
- * Usage:
12
- * node scripts/validate-tags.js [--work WORK.md] [--foundry-dir foundry]
13
- *
14
- * Exit 0 and prints "OK" if all tags are valid.
15
- * Exit 1 and prints invalid items to stderr otherwise.
16
- */
17
-
18
- import { readFileSync, existsSync } from 'fs';
19
- import { parseArgs } from 'util';
20
- import { validateTags } from './lib/tags.js';
21
-
22
- function main() {
23
- const { values } = parseArgs({
24
- options: {
25
- work: { type: 'string', default: 'WORK.md' },
26
- 'foundry-dir': { type: 'string', default: 'foundry' },
27
- },
28
- });
29
-
30
- const workPath = values.work;
31
- const foundryDir = values['foundry-dir'];
32
-
33
- if (!existsSync(workPath)) {
34
- process.stderr.write('ERROR: WORK.md not found\n');
35
- process.exit(2);
36
- }
37
-
38
- const workText = readFileSync(workPath, 'utf-8');
39
- const errors = validateTags(workText, foundryDir);
40
-
41
- if (errors.length === 0) {
42
- console.log('OK');
43
- process.exit(0);
44
- }
45
-
46
- process.stderr.write(`Tag validation failed (${errors.length} issue${errors.length > 1 ? 's' : ''}):\n`);
47
- for (const err of errors) {
48
- process.stderr.write(` line ${err.line}: ${err.message}\n`);
49
- process.stderr.write(` ${err.raw}\n`);
50
- }
51
- process.exit(1);
52
- }
53
-
54
- main();
@@ -1,111 +0,0 @@
1
- ---
2
- name: add-law
3
- type: atomic
4
- description: Creates a new law, checking for conflicts with existing laws.
5
- ---
6
-
7
- # Add Law
8
-
9
- You help the user create a new law. You ensure it's well-scoped, doesn't conflict with existing laws, and ends up in the right file.
10
-
11
- ## Prerequisites
12
-
13
- Before running this skill, verify both of the following:
14
-
15
- 1. The `foundry/` directory exists in the project root. If it does not exist, stop and tell the user:
16
-
17
- > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
18
-
19
- 2. The current git branch is not a work branch. Run `git rev-parse --abbrev-ref HEAD` — if it starts with `work/`, stop and tell the user:
20
-
21
- > You're on a work branch (`<branch>`). Foundry configuration changes must be made on the base branch (usually `main`). Complete or discard the in-flight flow (`foundry_git_finish`, or switch branches and delete it), then re-run this skill from the base branch.
22
-
23
- ## Protocol
24
-
25
- ### 1. Determine scope
26
-
27
- If the user specifies where the law applies:
28
- - "global law" → goes in `foundry/laws/` (ask which file, or create a new one)
29
- - "law for X artefacts" → goes in `foundry/artefacts/<type>/laws.md`
30
-
31
- If the user doesn't specify, ask:
32
-
33
- > Should this law apply globally to all artefact types, or to a specific type?
34
-
35
- If they name a type, verify it exists in `foundry/artefacts/`. If it doesn't, tell them and ask if they want to create the artefact type first.
36
-
37
- ### 2. Draft the law
38
-
39
- Write the law following the standard format:
40
-
41
- ```markdown
42
- ## <law-id>
43
-
44
- <What this law checks — one or two sentences.>
45
-
46
- Passing: <What a passing artefact looks like.>
47
- Failing: <What a failing artefact looks like.>
48
- ```
49
-
50
- The `law-id` (heading) should be:
51
- - Lowercase, hyphenated
52
- - Short but descriptive
53
- - Unique across all laws (global and type-specific)
54
-
55
- ### 3. Check for conflicts
56
-
57
- Read all existing laws that would apply to the same artefact types:
58
- - All files in `foundry/laws/` (global)
59
- - `foundry/artefacts/<type>/laws.md` if the law is type-specific
60
- - If the law is global, also read all `foundry/artefacts/*/laws.md` since a global law applies everywhere
61
-
62
- For each existing law, check:
63
- - Does the new law contradict an existing law? (e.g., "must be formal" vs "must be conversational")
64
- - Does the new law duplicate an existing law? (same criterion, different wording)
65
- - Does the new law overlap with an existing law? (partially covers the same ground)
66
-
67
- If any conflict is found, present it to the user:
68
-
69
- > The new law `<new-id>` may conflict with existing law `<existing-id>`:
70
- > - New: <summary of new law>
71
- > - Existing: <summary of existing law>
72
- > - Conflict: <what the conflict is>
73
- >
74
- > Options:
75
- > 1. Proceed anyway (both laws will apply)
76
- > 2. Replace the existing law with the new one
77
- > 3. Rephrase the new law to avoid the conflict
78
- > 4. Cancel
79
-
80
- ### 4. Refine with the user
81
-
82
- Present the drafted law to the user before writing it. Ask:
83
-
84
- > Here's the draft law:
85
- >
86
- > ## <law-id>
87
- >
88
- > <law content>
89
- >
90
- > Does this capture what you want, or should we adjust the wording?
91
-
92
- Iterate until the user is happy.
93
-
94
- ### 5. Write the law
95
-
96
- Append the law to the appropriate file:
97
- - Global: the specified file in `foundry/laws/`, or a new file
98
- - Type-specific: `foundry/artefacts/<type>/laws.md`
99
-
100
- If the target file doesn't exist yet, create it with a top-level heading.
101
-
102
- ### 6. Verify uniqueness
103
-
104
- After writing, confirm the law id is unique. If there's a collision, ask the user to rename.
105
-
106
- ## What you do NOT do
107
-
108
- - You do not write the law without showing the user first
109
- - You do not skip the conflict check
110
- - You do not silently overwrite existing laws
111
- - You do not create artefact types — that is a separate skill
@@ -1,88 +0,0 @@
1
- ---
2
- name: forge
3
- type: atomic
4
- description: Produces or revises an artefact, guided by WORK.md and the foundry cycle definition.
5
- ---
6
-
7
- # Forge
8
-
9
- You produce or revise artefacts. You read the work file to understand the goal and feedback, and the foundry cycle definition to understand what you're producing and what inputs you can read.
10
-
11
- ## Prerequisites
12
-
13
- Before running this skill, verify that the `foundry/` directory exists in the project root. If it does not exist, stop and tell the user:
14
-
15
- > Foundry is not initialized in this project. Run the `init-foundry` skill first to create the foundry/ directory structure.
16
-
17
- ## Stage lifecycle (mandatory)
18
-
19
- Forge runs inside an enforced stage. Your **first** and **last** tool calls are fixed:
20
-
21
- 1. **First:** `foundry_stage_begin({stage, cycle, token})` — the orchestrator hands you `stage`, `cycle`, and an opaque `token` string in the dispatch prompt. Copy the token verbatim; never invent, edit, or re-sign it. No other tool call is permitted before this one. Any writes before `stage_begin` will be blocked by preconditions.
22
- 2. **Last:** `foundry_stage_end({summary})` — return control to the orchestrator. After `stage_end`, the orchestrator calls `foundry_stage_finalize` which scans the disk and registers your output artefact. **You do not register artefacts yourself.**
23
-
24
- ## Protocol
25
-
26
- ### First generation (no artefact registered yet)
27
-
28
- 1. `foundry_stage_begin(...)` with the token from the dispatch prompt.
29
- 2. `foundry_workfile_get` — understand the goal.
30
- 3. `foundry_config_cycle` — understand what to produce and what inputs are available.
31
- 4. `foundry_config_artefact_type` with the output type ID — get the artefact type definition, especially its `file-patterns`.
32
- 5. `foundry_config_laws` — get all applicable laws (global + type-specific).
33
- 6. If the cycle declares `inputs`, discover input files by filesystem scan:
34
- - For each type listed in `inputs`, call `foundry_config_artefact_type` to get its `file-patterns`.
35
- - Glob the working tree against those patterns to enumerate candidate input files.
36
- - Read the goal (from `foundry_workfile_get`) and select the files that are relevant to this run. If the goal names specific files or slugs, use those; if it describes a category ("all the auth tests"), select the matching subset; if it's open-ended, you may consume all candidates or ask the user when the set is clearly ambiguous.
37
- - Read the selected files for context.
38
- 7. Produce the artefact, respecting all applicable laws from the start.
39
- 8. Write the artefact file to a location that matches the artefact type's `file-patterns`.
40
- 9. `foundry_stage_end({summary})`.
41
-
42
- ### Revision (feedback exists)
43
-
44
- 1. `foundry_stage_begin(...)`.
45
- 2. `foundry_feedback_list` — find unresolved feedback for the artefact.
46
- 3. Read the artefact file.
47
- 4. If the cycle declares `inputs`, discover them via filesystem scan against each input type's `file-patterns` (same protocol as first-generation step 6). Re-read the relevant files — they may have changed on disk since the previous iteration (nothing in this cycle wrote to them, but the user may have modified them between iterations).
48
- 5. For each unresolved feedback item, either:
49
- - Address it and call `foundry_feedback_action` (marks item `actioned`), or
50
- - Call `foundry_feedback_wontfix` with a justification — available only for `law:` / `human` tags (validation feedback must be actioned).
51
- 6. Update the artefact file.
52
- 7. `foundry_stage_end({summary})`.
53
-
54
- ## Write invariant
55
-
56
- Forge may only write to:
57
- - Files matching the output artefact type's `file-patterns`.
58
- - `WORK.md` and `WORK.history.yaml` (tool-managed).
59
-
60
- Everything else on disk — including files of the cycle's input types, files of unrelated artefact types, and files outside any artefact type — is read-only for this stage. This is not an honor-system rule: `foundry_stage_finalize` returns `{error: 'unexpected_files'}` and `sort`'s `checkModifiedFiles` routes a violation on the next call. Either outcome marks the cycle's target artefact `blocked` and you do not get a retry.
61
-
62
- When a cycle's output type overlaps with one of its input types (e.g. a `refine-haiku` cycle with input `haiku` and output `haiku`), the overlap is intentional: the cycle's job is to modify existing files of that type. The write invariant still holds — you may only touch files matching the output type's patterns, which in this case includes the files you read as inputs.
63
-
64
- ## Unresolved feedback
65
-
66
- An item is unresolved if it is:
67
- - `open` — not yet addressed
68
- - `rejected` — actioned or wont-fixed but rejected by appraiser, effectively re-opened
69
-
70
- An item is resolved if it is `approved`.
71
-
72
- ## #human feedback
73
-
74
- Feedback tagged `human` (from the human-appraise stage) takes absolute priority:
75
- - You MUST address it — you cannot wont-fix `#human` feedback.
76
- - When `#human` feedback contradicts LLM appraiser feedback on the same topic, follow the human's direction.
77
- - Acknowledge the human's input in your revision.
78
-
79
- ## What you do NOT do
80
-
81
- - You do not add feedback — that is the quench and appraise skills' job. (`foundry_feedback_add` is blocked for you at the tool layer.)
82
- - You do not `foundry_feedback_resolve` — that belongs to quench/appraise/human-appraise.
83
- - You do not register artefacts — `foundry_stage_finalize` handles that automatically.
84
- - You do not call `foundry_history_append` or `foundry_git_commit` — the sort skill does.
85
- - You do not evaluate or score the artefact.
86
- - You do not mark feedback as actioned unless you actually changed the artefact to address it.
87
- - You do not wont-fix validation feedback.
88
- - You do not write to any file outside the output artefact type's `file-patterns` (plus `WORK.md` / `WORK.history.yaml`). Input files are read-only unless the output type's patterns happen to cover them.