@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
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Check if a law block contains deprecated Passing:/Failing: scaffolding.
3
+ * @param {string[]} lines
4
+ * @returns {string[]} Array of error messages (empty if valid)
5
+ */
6
+ function checkForDeprecatedScaffolding(lines) {
7
+ const errors = [];
8
+ for (let i = 0; i < lines.length; i++) {
9
+ const line = lines[i].trim();
10
+ if (line.startsWith('Passing:') || line.startsWith('Failing:')) {
11
+ errors.push(`Line ${i + 1}: Deprecated scaffolding (${line.split(':')[0]}:) not allowed. Prose must be plain statements without structured fields. See spec: "No structured fields within the prose — no Passing: or Failing: scaffolding."`);
12
+ }
13
+ }
14
+ return errors;
15
+ }
16
+
17
+ /**
18
+ * Check for duplicate law IDs within blocks.
19
+ * @param {{id: string}[]} blocks
20
+ * @returns {string[]} Array of error messages (empty if valid)
21
+ */
22
+ function checkForDuplicateIds(blocks) {
23
+ const errors = [];
24
+ const seen = new Set();
25
+ for (const block of blocks) {
26
+ if (seen.has(block.id)) {
27
+ errors.push(`duplicate law id: ${block.id}`);
28
+ }
29
+ seen.add(block.id);
30
+ }
31
+ return errors;
32
+ }
33
+
34
+ /**
35
+ * Validate a law definition body.
36
+ *
37
+ * Laws derive their ID from the filename rather than frontmatter, so name
38
+ * and io are unused here but accepted for API parity with other validators.
39
+ *
40
+ * @param {object} opts
41
+ * @param {string} opts.name Slugged identifier (unused; laws use filename as ID).
42
+ * @param {string} opts.body Full markdown body.
43
+ * @param {object} [opts.io] IO adapter (unused; laws validate body only).
44
+ * @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
45
+ */
46
+ export async function validate({ body }) {
47
+ const lines = body.split('\n');
48
+ let errors = checkForDeprecatedScaffolding(lines);
49
+
50
+ if (errors.length > 0) {
51
+ return { ok: false, errors };
52
+ }
53
+
54
+ const blocks = parseLawBlocks(body);
55
+
56
+ if (blocks.length === 0) {
57
+ return { ok: false, errors: ['body must contain at least one law block (## <law-id>)'] };
58
+ }
59
+
60
+ errors = checkForDuplicateIds(blocks);
61
+ return errors.length ? { ok: false, errors } : { ok: true };
62
+ }
63
+
64
+ /**
65
+ * Parse law blocks from the body.
66
+ * Each block is a ## heading followed by its content until the next heading.
67
+ * @param {string} body
68
+ * @returns {{id: string, text: string}[]}
69
+ */
70
+ function parseLawBlocks(body) {
71
+ const blocks = [];
72
+ const lines = body.split('\n');
73
+ let currentId = null;
74
+ let currentLines = [];
75
+
76
+ const flushBlock = () => {
77
+ if (currentId) {
78
+ blocks.push({ id: currentId, text: currentLines.join('\n') });
79
+ }
80
+ };
81
+
82
+ for (const line of lines) {
83
+ const heading = line.match(/^## (.+)/);
84
+ if (!heading && currentId) {
85
+ currentLines.push(line);
86
+ }
87
+ if (heading) {
88
+ flushBlock();
89
+ currentId = heading[1].trim();
90
+ currentLines = [];
91
+ }
92
+ }
93
+ flushBlock();
94
+
95
+ return blocks;
96
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Structured reads of foundry/ directory contents.
3
+ */
4
+
5
+ import { join } from 'path';
6
+ import { parseFrontmatter } from './workfile.js';
7
+
8
+ function parseDoc(text) {
9
+ const frontmatter = parseFrontmatter(text);
10
+ const body = text.replace(/^---\n.+?\n---\n?/s, '').trim();
11
+ return { frontmatter, body };
12
+ }
13
+
14
+ export async function getCycleDefinition(foundryDir, cycleId, io) {
15
+ const path = join(foundryDir, 'cycles', `${cycleId}.md`);
16
+ if (!(await io.exists(path))) {
17
+ throw new Error(`Cycle not found: ${cycleId}`);
18
+ }
19
+ const text = await io.readFile(path);
20
+ return parseDoc(text);
21
+ }
22
+
23
+ export async function getArtefactType(foundryDir, typeId, io) {
24
+ const path = join(foundryDir, 'artefacts', typeId, 'definition.md');
25
+ if (!(await io.exists(path))) {
26
+ throw new Error(`Artefact type not found: ${typeId}`);
27
+ }
28
+ const text = await io.readFile(path);
29
+ return parseDoc(text);
30
+ }
31
+
32
+ /**
33
+ * Parse law entries from a markdown file. Each `## heading` starts a new law.
34
+ * Each law may have an optional `validators:` block with entries containing id, command, and optional failure-means.
35
+ * Validators are extracted and returned separately from prose.
36
+ * Returns: [{id, text (prose only), validators (if present)}]
37
+ */
38
+ function parseLaws(text, source) {
39
+ const laws = [];
40
+ const lines = text.split('\n');
41
+ let currentId = null;
42
+ const currentLines = [];
43
+
44
+ function flush() {
45
+ if (currentId) {
46
+ const lawText = currentLines.join('\n').trim();
47
+ const { prose, validators } = extractValidators(lawText, currentId);
48
+ const lawObj = { id: currentId, text: prose };
49
+ if (validators && validators.length > 0) {
50
+ lawObj.validators = validators;
51
+ }
52
+ laws.push(lawObj);
53
+ }
54
+ }
55
+
56
+ for (const line of lines) {
57
+ const heading = line.match(/^## (.+)/);
58
+ if (heading) {
59
+ flush();
60
+ currentId = heading[1];
61
+ currentLines.length = 0;
62
+ } else if (currentId) {
63
+ currentLines.push(line);
64
+ }
65
+ }
66
+ flush();
67
+ return laws;
68
+ }
69
+
70
+ /**
71
+ * Extract validators block from law text and return prose-only text plus parsed validators.
72
+ * @param {string} lawText - Full law text including optional validators block
73
+ * @param {string} lawId - Law ID for error messages
74
+ * @returns {{prose: string, validators: Array}} - Prose text and validators array
75
+ * @throws {Error} if validators block is malformed
76
+ */
77
+ function extractValidators(lawText, lawId) {
78
+ // Find validators: block (must start at beginning of line, followed by indented lines)
79
+ // The pattern captures lines starting with spaces/tabs (indented content)
80
+ // Using possessive quantifier to avoid backtracking: (?:...)+ instead of (...)*.
81
+ const validatorBlockMatch = lawText.match(/^validators:\n((?:[ \t]+\S.*(?:\n|$))*)/m);
82
+
83
+ if (!validatorBlockMatch || !validatorBlockMatch[1].trim()) {
84
+ return { prose: lawText, validators: null };
85
+ }
86
+
87
+ // Extract prose (everything before validators:)
88
+ const prose = lawText.substring(0, validatorBlockMatch.index).trim();
89
+
90
+ // Extract and parse validators block
91
+ const validatorBlockText = validatorBlockMatch[1];
92
+ const validators = parseValidatorBlock(validatorBlockText, lawId);
93
+
94
+ return { prose, validators };
95
+ }
96
+
97
+ /**
98
+ * Parse a validator entry from lines.
99
+ * @param {string} lawId - Law ID for error messages
100
+ * @param {Set} seenIds - Set of seen validator IDs to detect duplicates
101
+ * @param {object} validator - Validator object being built
102
+ * @throws {Error} if validator is invalid
103
+ */
104
+ function saveValidator(validator, seenIds, lawId) {
105
+ validateValidator(validator, seenIds, lawId);
106
+ return validator;
107
+ }
108
+
109
+ /**
110
+ * Parse field from a line: key: value
111
+ * @param {string} line - Line to parse
112
+ * @param {string} key - Field key to match
113
+ * @returns {string|null} - Field value or null
114
+ */
115
+ function parseField(line, key) {
116
+ const match = line.match(new RegExp(`^\\s*${key}:\\s*(.+)`));
117
+ return match ? match[1].trim() : null;
118
+ }
119
+
120
+ /**
121
+ * Handle a new validator entry line.
122
+ * @param {object} currentValidator - Current validator object or null
123
+ * @param {Array} validators - List to add saved validators to
124
+ * @param {string} line - Line to process
125
+ * @param {Set} seenIds - Set of seen IDs
126
+ * @param {string} lawId - Law ID for error messages
127
+ * @returns {object} - New validator object or null
128
+ */
129
+ function handleValidatorEntry(currentValidator, validators, line, seenIds, lawId) {
130
+ const entryMatch = line.match(/^\s*-\s*id:\s*(.+)/);
131
+ if (entryMatch) {
132
+ // Save previous validator if exists
133
+ if (currentValidator) {
134
+ validators.push(saveValidator(currentValidator, seenIds, lawId));
135
+ }
136
+ return { id: entryMatch[1].trim() };
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Process a field line in a validator entry.
143
+ * @param {object} validator - Current validator object
144
+ * @param {string} line - Line to process
145
+ */
146
+ function processValidatorField(validator, line) {
147
+ const command = parseField(line, 'command');
148
+ if (command !== null) {
149
+ validator.command = command;
150
+ return;
151
+ }
152
+
153
+ const failureMeans = parseField(line, 'failure-means');
154
+ if (failureMeans !== null) {
155
+ validator['failure-means'] = failureMeans;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Process a single line in the validators block.
161
+ * @param {object} currentValidator - Current validator or null
162
+ * @param {string} line - Line to process
163
+ * @param {string} lawId - Law ID for error messages
164
+ * @throws {Error} if validator entry not started
165
+ */
166
+ function processValidatorLine(currentValidator, line, lawId) {
167
+ if (!currentValidator) {
168
+ throw new Error(`law "${lawId}": validator entry missing required 'id'`);
169
+ }
170
+ processValidatorField(currentValidator, line);
171
+ }
172
+
173
+ /**
174
+ * Parse the validators block content (YAML-like format)
175
+ * @param {string} blockText - Text content of validators block
176
+ * @param {string} lawId - Law ID for error messages
177
+ * @returns {Array} - Array of parsed validators
178
+ * @throws {Error} if validators are malformed
179
+ */
180
+ function parseValidatorBlock(blockText, lawId) {
181
+ const validators = [];
182
+ const lines = blockText.split('\n');
183
+ let currentValidator = null;
184
+ const seenIds = new Set();
185
+
186
+ for (const line of lines) {
187
+ // Skip empty lines
188
+ if (!line.trim()) continue;
189
+
190
+ // Check for new validator entry (starts with -)
191
+ const newValidator = handleValidatorEntry(currentValidator, validators, line, seenIds, lawId);
192
+ if (newValidator) {
193
+ currentValidator = newValidator;
194
+ continue;
195
+ }
196
+
197
+ processValidatorLine(currentValidator, line, lawId);
198
+ }
199
+
200
+ // Save last validator
201
+ if (currentValidator) {
202
+ validators.push(saveValidator(currentValidator, seenIds, lawId));
203
+ }
204
+
205
+ return validators;
206
+ }
207
+
208
+ /**
209
+ * Validate a single validator entry.
210
+ * @param {object} validator - Validator object with id, command, and optional failure-means
211
+ * @param {Set} seenIds - Set of seen validator IDs to detect duplicates
212
+ * @param {string} lawId - Law ID for error messages
213
+ * @throws {Error} if validator is invalid
214
+ */
215
+ function validateValidator(validator, seenIds, lawId) {
216
+ if (!validator.id) {
217
+ throw new Error(`law "${lawId}": validator entry missing required 'id'`);
218
+ }
219
+ if (!validator.command) {
220
+ throw new Error(`law "${lawId}": validator entry missing required 'command'`);
221
+ }
222
+ if (seenIds.has(validator.id)) {
223
+ throw new Error(`law "${lawId}": duplicate validator id '${validator.id}' in law`);
224
+ }
225
+ seenIds.add(validator.id);
226
+ }
227
+
228
+ async function collectLawsFromDir(dir, io, sourcePrefix) {
229
+ if (!(await io.exists(dir))) return [];
230
+ const files = await io.readDir(dir);
231
+ const mdFiles = files.filter(f => f.endsWith('.md')).sort();
232
+ const results = [];
233
+ for (const file of mdFiles) {
234
+ const text = await io.readFile(join(dir, file));
235
+ results.push(...parseLaws(text, `${sourcePrefix}/${file}`));
236
+ }
237
+ return results;
238
+ }
239
+
240
+ /**
241
+ * Collect all laws (global and type-specific) for a given artefact type.
242
+ * Returns laws with their source and validator information.
243
+ */
244
+ async function collectAllLaws(foundryDir, io, { typeId } = {}) {
245
+ const laws = await collectLawsFromDir(join(foundryDir, 'laws'), io, 'laws');
246
+
247
+ if (typeId) {
248
+ const typeLawsPath = join(foundryDir, 'artefacts', typeId, 'laws.md');
249
+ if (await io.exists(typeLawsPath)) {
250
+ const text = await io.readFile(typeLawsPath);
251
+ laws.push(...parseLaws(text, `artefacts/${typeId}/laws.md`));
252
+ }
253
+ }
254
+
255
+ return laws;
256
+ }
257
+
258
+ export async function getLaws(foundryDir, io, { typeId } = {}) {
259
+ const laws = await collectAllLaws(foundryDir, io, { typeId });
260
+
261
+ // Return prose-only without source or validators
262
+ return laws.map(law => ({ id: law.id, text: law.text }));
263
+ }
264
+
265
+ export async function getLawsForQuench(foundryDir, io, { typeId } = {}) {
266
+ const laws = await collectAllLaws(foundryDir, io, { typeId });
267
+
268
+ // Return only laws that have validators
269
+ return laws.filter(law => law.validators && law.validators.length > 0);
270
+ }
271
+
272
+ function parseValidationEntry(line) {
273
+ const cmdMatch = line.match(/^Command:\s*(.+)/);
274
+ if (cmdMatch) return { type: 'command', value: cmdMatch[1].trim().replace(/^`|`$/g, '') };
275
+ const failMatch = line.match(/^Failure means:\s*(.+)/);
276
+ if (failMatch) return { type: 'failure', value: failMatch[1].trim() };
277
+ return null;
278
+ }
279
+
280
+ function buildValidationEntry(currentId, currentCommand, currentFailure) {
281
+ if (!currentId || !currentCommand) return null;
282
+ const entry = { id: currentId, command: currentCommand };
283
+ if (currentFailure) entry.failureMeans = currentFailure;
284
+ return entry;
285
+ }
286
+
287
+ function flushValidationEntry(entries, id, command, failure) {
288
+ const entry = buildValidationEntry(id, command, failure);
289
+ if (entry) entries.push(entry);
290
+ }
291
+
292
+ function applyParsedEntry(state, parsed) {
293
+ if (parsed?.type === 'command') state.command = parsed.value;
294
+ if (parsed?.type === 'failure') state.failure = parsed.value;
295
+ }
296
+
297
+ function handleValidationLine(line, state) {
298
+ const heading = line.match(/^## (.+)/);
299
+ if (heading) {
300
+ flushValidationEntry(state.entries, state.id, state.command, state.failure);
301
+ state.id = heading[1].trim();
302
+ state.command = null;
303
+ state.failure = null;
304
+ return;
305
+ }
306
+ if (state.id) {
307
+ applyParsedEntry(state, parseValidationEntry(line));
308
+ }
309
+ }
310
+
311
+ function internalParseValidationLines(lines) {
312
+ const state = { entries: [], id: null, command: null, failure: null };
313
+ for (const line of lines) {
314
+ handleValidationLine(line, state);
315
+ }
316
+ flushValidationEntry(state.entries, state.id, state.command, state.failure);
317
+ return state.entries;
318
+ }
319
+
320
+ /**
321
+ * @deprecated Use getLawsForQuench instead. Phase 2 migration of validation.md files will remove this.
322
+ */
323
+ export async function getValidation(foundryDir, typeId, io) {
324
+ const path = join(foundryDir, 'artefacts', typeId, 'validation.md');
325
+ if (!(await io.exists(path))) return null;
326
+ const text = await io.readFile(path);
327
+ return internalParseValidationLines(text.split('\n'));
328
+ }
329
+
330
+ /**
331
+ * @deprecated Use getLawsForQuench instead. Phase 2 migration of validation.md files will remove this.
332
+ */
333
+ export async function parseValidationLines(lines) {
334
+ return internalParseValidationLines(lines);
335
+ }
336
+
337
+ export async function getAppraisers(foundryDir, io) {
338
+ const dir = join(foundryDir, 'appraisers');
339
+ if (!(await io.exists(dir))) return [];
340
+ const files = await io.readDir(dir);
341
+ const mdFiles = files.filter(f => f.endsWith('.md')).sort();
342
+ const result = [];
343
+ for (const file of mdFiles) {
344
+ const text = await io.readFile(join(dir, file));
345
+ const { frontmatter, body } = parseDoc(text);
346
+ const entry = { id: frontmatter.id, personality: body };
347
+ if (frontmatter.model) entry.model = frontmatter.model;
348
+ result.push(entry);
349
+ }
350
+ return result;
351
+ }
352
+
353
+ export async function getFlow(foundryDir, flowId, io) {
354
+ const path = join(foundryDir, 'flows', `${flowId}.md`);
355
+ if (!(await io.exists(path))) {
356
+ throw new Error(`Flow not found: ${flowId}`);
357
+ }
358
+ const text = await io.readFile(path);
359
+ return parseDoc(text);
360
+ }
361
+
362
+ function buildAppraiserPool(allAppraisers, allowed) {
363
+ return allowed ? allAppraisers.filter(a => allowed.includes(a.id)) : allAppraisers;
364
+ }
365
+
366
+ function roundRobinSelect(pool, count) {
367
+ const result = [];
368
+ for (let i = 0; i < count; i++) {
369
+ result.push(pool[i % pool.length]);
370
+ }
371
+ return result;
372
+ }
373
+
374
+ function resolveAppraiserConfig(frontmatter, countOverride) {
375
+ const appraiserConfig = frontmatter.appraisers || {};
376
+ return {
377
+ count: countOverride || appraiserConfig.count || 3,
378
+ allowed: appraiserConfig.allowed || null,
379
+ };
380
+ }
381
+
382
+ export async function selectAppraisers(foundryDir, typeId, { io, countOverride } = {}) {
383
+ if (!io) throw new Error('selectAppraisers: io is required');
384
+
385
+ const { frontmatter } = await getArtefactType(foundryDir, typeId, io);
386
+ const { count, allowed } = resolveAppraiserConfig(frontmatter, countOverride);
387
+
388
+ const allAppraisers = await getAppraisers(foundryDir, io);
389
+ const pool = buildAppraiserPool(allAppraisers, allowed);
390
+ if (pool.length === 0) return [];
391
+
392
+ return roundRobinSelect(pool, count);
393
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Failed-flow lifecycle helpers.
3
+ *
4
+ * When a tool encounters an unrecoverable error (e.g. stage_end cannot
5
+ * flush memory to NDJSON and the on-disk source of truth is now behind
6
+ * the live DB), it marks WORK.md with `status: failed` and a `reason`.
7
+ *
8
+ * Every mutating tool guards on this state via `requireNotFailed`. This
9
+ * includes work-branch FS writers (artefacts, feedback, workfile, stage,
10
+ * orchestrate), memory writers — row-level (memory_put, memory_relate,
11
+ * memory_unrelate) and admin (create_*, rename_*, drop_*, reset, init,
12
+ * vacuum, change_embedding_model) — and `validate_run`, since validation
13
+ * commands are project-defined subprocesses with arbitrary side effects
14
+ * (linters with --fix, formatters). The rule is simple: tools that mutate
15
+ * disk or live DB state, or run unsandboxed subprocesses that could mutate
16
+ * it, stay blocked while the abandoned work-branch filesystem remains the
17
+ * source of truth.
18
+ *
19
+ * Read-only diagnostics (workfile_get, memory_list/get/neighbours/query/search,
20
+ * memory_dump, memory_validate) remain available to support diagnosis before
21
+ * the cycle is abandoned.
22
+ *
23
+ * Recovery paths: `foundry_workfile_delete` abandons the cycle; editing
24
+ * WORK.md to remove the failed status after fixing the underlying issue
25
+ * restores normal operation. `foundry_stage_retry` provides a deterministic
26
+ * rollback mechanism: it discards uncommitted memory changes, clears the
27
+ * failed status, and resets the stage state, provided the git working tree
28
+ * is clean.
29
+ */
30
+ import { parseFrontmatter, setFrontmatterField, writeFrontmatter } from './workfile.js';
31
+
32
+ const MAX_REASON_LEN = 500;
33
+
34
+ function truncateReason(reason) {
35
+ const s = String(reason ?? '');
36
+ if (s.length <= MAX_REASON_LEN) return s;
37
+ return s.slice(0, MAX_REASON_LEN) + '...';
38
+ }
39
+
40
+ /**
41
+ * @param {{exists: (p: string) => boolean, readFile: (p: string) => string}} io
42
+ * @returns {{reason: string} | null}
43
+ */
44
+ export function readFailedStatus(io) {
45
+ if (!io.exists('WORK.md')) return null;
46
+ const text = io.readFile('WORK.md');
47
+ const fm = parseFrontmatter(text);
48
+ if (fm.status !== 'failed') return null;
49
+ return { reason: fm.reason === undefined ? '' : String(fm.reason) };
50
+ }
51
+
52
+ /**
53
+ * Idempotent: sets `status: failed` and `reason` if not already failed.
54
+ * Preserves the first failure reason when called multiple times - the
55
+ * initial diagnostic reason is more valuable than cascading failures.
56
+ * @param {object} io - requires exists, readFile, writeFile
57
+ * @param {string} reason
58
+ */
59
+ export function markWorkfileFailed(io, reason) {
60
+ if (!io.exists('WORK.md')) {
61
+ throw new Error('markWorkfileFailed: WORK.md not found');
62
+ }
63
+ const text = io.readFile('WORK.md');
64
+
65
+ // Check if already failed - preserve the first failure reason
66
+ const failed = readFailedStatus(io);
67
+ if (failed) {
68
+ // Already failed - skip overwrite to preserve diagnostic first reason
69
+ return;
70
+ }
71
+
72
+ const withStatus = setFrontmatterField(text, 'status', 'failed');
73
+ const withReason = setFrontmatterField(withStatus, 'reason', truncateReason(reason));
74
+ io.writeFile('WORK.md', withReason);
75
+ }
76
+
77
+ /**
78
+ * Tool guard: returns `{ok:true}` when the flow is healthy, otherwise
79
+ * `{ok:false, error}` with a message that tells the LLM exactly how to
80
+ * escape (abandon the flow).
81
+ * @param {object} io
82
+ * @returns {{ok: true} | {ok: false, error: string}}
83
+ */
84
+ export function requireNotFailed(io) {
85
+ let failed;
86
+ try {
87
+ failed = readFailedStatus(io);
88
+ } catch {
89
+ // WORK.md is corrupted (malformed YAML) or unreadable (IO error).
90
+ // This is a trouble signal - refuse to proceed.
91
+ return {
92
+ ok: false,
93
+ error:
94
+ `WORK.md is corrupted or unreadable. ` +
95
+ `No mutating tools are permitted. Use foundry_workfile_delete({confirm: true}) ` +
96
+ `to abandon the cycle, then back out to main and delete the work branch.`,
97
+ };
98
+ }
99
+
100
+ if (!failed) return { ok: true };
101
+ const reason = failed.reason || '(no reason recorded)';
102
+ return {
103
+ ok: false,
104
+ error:
105
+ `flow is in failed state (reason: ${reason}). ` +
106
+ `No mutating tools are permitted. Use foundry_workfile_delete({confirm: true}) ` +
107
+ `to abandon the cycle, then back out to main and delete the work branch.`,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Clears the failed status from WORK.md, restoring normal operation.
113
+ * Idempotent: safe to call even if not failed.
114
+ * @param {object} io - requires exists, readFile, writeFile
115
+ */
116
+ export function clearWorkfileFailed(io) {
117
+ if (!io.exists('WORK.md')) {
118
+ throw new Error('clearWorkfileFailed: WORK.md not found');
119
+ }
120
+ const text = io.readFile('WORK.md');
121
+ const fm = parseFrontmatter(text);
122
+
123
+ // Remove status and reason fields
124
+ delete fm.status;
125
+ delete fm.reason;
126
+
127
+ // Rebuild the file with cleaned frontmatter
128
+ const fmBlock = writeFrontmatter(fm);
129
+ const body = text.replace(/^---\n.+?\n---\n?/s, '');
130
+ io.writeFile('WORK.md', body ? `${fmBlock}\n${body}` : fmBlock);
131
+ }