@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,340 @@
1
+ // Shared helpers for the Foundry plugin. Pure functions — no plugin deps.
2
+
3
+ import path from 'path';
4
+ import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync, mkdirSync, renameSync, rmSync, statSync } from 'fs';
5
+ import { execFileSync } from 'child_process';
6
+ import { getCycleDefinition } from '../../../scripts/lib/config.js';
7
+ import { getOrOpenStore, getContext } from '../../../scripts/lib/memory/singleton.js';
8
+ import { resolvePermissions } from '../../../scripts/lib/memory/permissions.js';
9
+ import { renderMemoryPrompt } from '../../../scripts/lib/memory/prompt.js';
10
+ import { loadExtractor } from '../../../scripts/lib/assay/loader.js';
11
+ import { requireOnFlowBranch } from '../../../scripts/lib/branch-guard.js';
12
+
13
+ // Track flow files we've already warned about to avoid spamming stderr
14
+ const warnedFlowFiles = new Set();
15
+
16
+ // -- Frontmatter parsing helpers --
17
+
18
+ function isFieldSeparator(char) {
19
+ return char === ' ' || char === '\t';
20
+ }
21
+
22
+ function parseFrontmatterField(fm, fieldName) {
23
+ const prefix = `${fieldName}:`;
24
+ for (const line of fm.split('\n')) {
25
+ if (!line.startsWith(prefix)) continue;
26
+ const rest = line.slice(prefix.length);
27
+ if (rest === '' || isFieldSeparator(rest[0])) {
28
+ return rest.trim();
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function parseStartingCycles(fm) {
35
+ const lines = fm.split('\n');
36
+ const scIndex = lines.findIndex(line => line.trimEnd() === 'starting-cycles:');
37
+ if (scIndex < 0) return [];
38
+ const items = [];
39
+ for (let i = scIndex + 1; i < lines.length; i++) {
40
+ const trimmed = lines[i].trimStart();
41
+ if (trimmed.startsWith('-')) {
42
+ const content = trimmed.slice(1).trimStart();
43
+ if (content) items.push(content);
44
+ } else {
45
+ break;
46
+ }
47
+ }
48
+ return items;
49
+ }
50
+
51
+ function parseFlowFrontmatter(text, entry) {
52
+ const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
53
+ if (!fmMatch) return null;
54
+ const fm = fmMatch[1];
55
+ const id = parseFrontmatterField(fm, 'id') || entry.replace(/\.md$/, '');
56
+ const name = parseFrontmatterField(fm, 'name') || id;
57
+ const startingCycles = parseStartingCycles(fm);
58
+ return { id, name, startingCycles };
59
+ }
60
+
61
+ function parseFlowFile(entry, flowsDir) {
62
+ try {
63
+ const text = readFileSync(path.join(flowsDir, entry), 'utf-8');
64
+ return parseFlowFrontmatter(text, entry);
65
+ } catch (err) {
66
+ if (!warnedFlowFiles.has(entry)) {
67
+ console.warn(`Warning: Skipping malformed flow file ${entry}: ${err.message}`);
68
+ warnedFlowFiles.add(entry);
69
+ }
70
+ const id = entry.replace(/\.md$/, '');
71
+ return { id, name: id, startingCycles: [], error: err.message };
72
+ }
73
+ }
74
+
75
+ function isFlowFile(entry) {
76
+ return entry.endsWith('.md') && entry !== '.gitkeep';
77
+ }
78
+
79
+ export function listFlows(foundryDir) {
80
+ const flowsDir = path.join(foundryDir, 'flows');
81
+ if (!existsSync(flowsDir)) return [];
82
+ const flows = [];
83
+ for (const entry of readdirSync(flowsDir)) {
84
+ if (!isFlowFile(entry)) continue;
85
+ const parsed = parseFlowFile(entry, flowsDir);
86
+ if (parsed) flows.push(parsed);
87
+ }
88
+ return flows;
89
+ }
90
+
91
+ // -- Bootstrap content helpers --
92
+
93
+ function buildFoundryNotInitializedMessage() {
94
+ return `<FOUNDRY_CONTEXT>
95
+ Foundry is installed but not initialised in this project. There is no foundry/ directory.
96
+
97
+ To set up Foundry, use the \`init-foundry\` skill. This will create the foundry/ directory structure
98
+ and guide you through defining artefact types, laws, appraisers, cycles, and flows.
99
+ </FOUNDRY_CONTEXT>`;
100
+ }
101
+
102
+ function buildFlowList(flows) {
103
+ if (flows.length === 0) {
104
+ return '- (no flows defined yet — use the `add-flow` skill to create one)';
105
+ }
106
+ return flows.map(f => {
107
+ const sc = f.startingCycles.length > 0 ? ` — starting cycles: ${f.startingCycles.join(', ')}` : '';
108
+ return `- \`${f.id}\` — ${f.name}${sc}`;
109
+ }).join('\n');
110
+ }
111
+
112
+ function buildFoundryInitializedMessage(flowList, packageRoot) {
113
+ return `<FOUNDRY_CONTEXT>
114
+ Foundry is active in this project. The foundry/ directory contains the project's artefact definitions,
115
+ laws, appraisers, cycles, and flows.
116
+
117
+ Foundry is a skill-driven framework for governed artefact generation and evaluation.
118
+ The pipeline: assay (populate memory) → forge (produce) → quench (deterministic checks) → appraise (subjective evaluation) → human-appraise (human review) → iterate.
119
+
120
+ ## Defined flows
121
+
122
+ ${flowList}
123
+
124
+ **CRITICAL ROUTING RULE:** When the user references any flow above — by id (e.g. "creative-flow"),
125
+ by name (e.g. "Creative Flow"), or by clear paraphrase (e.g. "the creative flow", "use the creative pipeline") —
126
+ invoke the \`flow\` skill DIRECTLY with that flow's id. Do NOT invoke brainstorming, do NOT explore the
127
+ codebase, do NOT ask clarifying questions about what to build. The flow's cycles already define the
128
+ work. The user's request text (e.g. "make a haiku about X") is the goal to pass to the flow.
129
+
130
+ Brainstorming applies to NEW features being added to foundry itself (new cycles, new artefact types,
131
+ new skills). It does NOT apply to running an existing, defined flow.
132
+
133
+ ## Available skills
134
+
135
+ - **Pipeline:** assay, forge, quench, appraise, orchestrate, flow, human-appraise
136
+ - **Authoring:** add-artefact-type, add-law, add-appraiser, add-cycle, add-flow, add-memory-entity-type, add-memory-edge-type, add-extractor, init-foundry
137
+ - **Maintenance:** upgrade-foundry, refresh-agents, list-agents, init-memory, change-embedding-model, dry-run
138
+ - **Memory Admin:** drop-memory-entity-type, drop-memory-edge-type, rename-memory-entity-type, rename-memory-edge-type, reset-memory
139
+
140
+ ## Multi-model routing
141
+
142
+ Foundry uses \`foundry-*\` sub-agents defined as markdown files in \`.opencode/agents/\`.
143
+ Run the \`refresh-agents\` skill to regenerate them after adding or removing providers.
144
+ Cycle definitions can specify per-stage models via the \`models\` frontmatter map. Appraisers can override with their own \`model\` field.
145
+
146
+ All user content lives under foundry/.
147
+ Scripts are located at: ${path.join(packageRoot, 'scripts')}
148
+ </FOUNDRY_CONTEXT>`;
149
+ }
150
+
151
+ export function getBootstrapContent(directory, packageRoot) {
152
+ const foundryDir = path.join(directory, 'foundry');
153
+ const foundryExists = existsSync(foundryDir) && statSync(foundryDir).isDirectory();
154
+
155
+ if (!foundryExists) {
156
+ return buildFoundryNotInitializedMessage();
157
+ }
158
+
159
+ const flows = listFlows(foundryDir);
160
+ const flowList = buildFlowList(flows);
161
+ return buildFoundryInitializedMessage(flowList, packageRoot);
162
+ }
163
+
164
+ /**
165
+ * Factory for creating exec functions used across plugin tools.
166
+ * Returns a function that executes commands via execFileSync.
167
+ * Used by tools that need to run git or other CLI commands.
168
+ */
169
+ export function makeExec(cwd) {
170
+ return (argv) => execFileSync(argv[0], argv.slice(1), {
171
+ cwd, encoding: 'utf8', stdio: 'pipe',
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Guard function that ensures a tool is called on a flow branch (work/* or dry-run/*).
177
+ * Used by guarded() to enforce branch requirements for flow-tier mutations.
178
+ * Returns the result of requireOnFlowBranch({ exec }).
179
+ */
180
+ export function flowBranchGuard(_args, context) {
181
+ return requireOnFlowBranch({ exec: makeExec(context.worktree) });
182
+ }
183
+
184
+ export function makeIO(directory) {
185
+ const resolve = (p) => path.isAbsolute(p) ? p : path.join(directory, p);
186
+ return {
187
+ exists: (p) => existsSync(resolve(p)),
188
+ readFile: (p) => readFileSync(resolve(p), 'utf-8'),
189
+ writeFile: (p, content) => writeFileSync(resolve(p), content, 'utf-8'),
190
+ readDir: (p) => readdirSync(resolve(p)),
191
+ mkdir: (p) => mkdirSync(resolve(p), { recursive: true }),
192
+ // unlink: succeeds silently when the file is missing.
193
+ unlink: (p) => { if (existsSync(resolve(p))) unlinkSync(resolve(p)); },
194
+ rename: (from, to) => renameSync(resolve(from), resolve(to)),
195
+ // exec: run a command in the worktree and return stdout as a UTF-8 string.
196
+ // Used by sort.js (getDirtyToolManagedFiles, getModifiedFiles) for git enforcement.
197
+ // Takes an array [command, ...args] to prevent shell injection.
198
+ // Throws on non-zero exit; callers already wrap in try/catch.
199
+ exec: (argv) => execFileSync(argv[0], argv.slice(1), { cwd: directory, encoding: 'utf8', stdio: 'pipe' }),
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Factory used by guarded() to resolve the current branch.
205
+ * Returns an object exposing `exec(argv: string[]) => string` (stdout).
206
+ */
207
+ export function branchIoFactory(context) {
208
+ const cwd = context.worktree;
209
+ return {
210
+ exec: (argv) => execFileSync(argv[0], argv.slice(1),
211
+ { cwd, encoding: 'utf8', stdio: 'pipe' }),
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Factory used by guarded() for tracing IO. Returns the existing
217
+ * async IO shape (mkdirp/exists/readFile/writeFile) plus an
218
+ * `appendFile` for trace appends.
219
+ */
220
+ export function asyncIoFactory(context) {
221
+ const sync = makeIO(context.worktree);
222
+ return {
223
+ exists: async (p) => sync.exists(p),
224
+ readFile: async (p) => sync.readFile(p),
225
+ writeFile: async (p, c) => sync.writeFile(p, c),
226
+ mkdirp: async (p) => sync.mkdir(p),
227
+ appendFile: async (p, c) => {
228
+ const existing = sync.exists(p) ? sync.readFile(p) : '';
229
+ sync.writeFile(p, existing + c);
230
+ },
231
+ };
232
+ }
233
+
234
+ export function makeMemoryIO(directory) {
235
+ // Memory modules use await on every I/O op. Wrap sync fs calls in Promise-returning shims.
236
+ const sync = makeIO(directory);
237
+ return {
238
+ exists: async (p) => sync.exists(p),
239
+ readFile: async (p) => sync.readFile(p),
240
+ writeFile: async (p, c) => sync.writeFile(p, c),
241
+ readDir: async (p) => { try { return sync.readDir(p); } catch { return []; } },
242
+ mkdir: async (p) => sync.mkdir(p),
243
+ unlink: async (p) => sync.unlink(p),
244
+ rename: async (from, to) => sync.rename(from, to),
245
+ };
246
+ }
247
+
248
+ export function errorJson(err) {
249
+ return JSON.stringify({ error: err.message ?? String(err) });
250
+ }
251
+
252
+ /**
253
+ * Async IO contract consumed by the config-creators layer.
254
+ *
255
+ * The creators take an io with `exists, readFile, writeFile, mkdirp,
256
+ * readDir`. We reuse `makeIO`'s sync fs calls and wrap them in
257
+ * Promise-returning shims; `makeIO.mkdir` already passes
258
+ * `{ recursive: true }`, so it satisfies `mkdirp` semantics.
259
+ */
260
+ export function makeAsyncIO(directory) {
261
+ const sync = makeIO(directory);
262
+ const resolve = (p) => path.isAbsolute(p) ? p : path.join(directory, p);
263
+ return {
264
+ exists: async (p) => sync.exists(p),
265
+ readFile: async (p) => sync.readFile(p),
266
+ writeFile: async (p, c) => sync.writeFile(p, c),
267
+ mkdirp: async (p) => sync.mkdir(p),
268
+ readDir: async (p) => { try { return sync.readDir(p); } catch { return []; } },
269
+ // `readdir` (lowercase) alias matching the snapshot inspect.js contract.
270
+ readdir: async (p) => { try { return sync.readDir(p); } catch { return []; } },
271
+ rm: async (p, opts = {}) => {
272
+ const full = resolve(p);
273
+ if (existsSync(full)) {
274
+ rmSync(full, { recursive: !!opts.recursive, force: true });
275
+ }
276
+ },
277
+ };
278
+ }
279
+
280
+ // -- buildCyclePromptExtras helpers --
281
+
282
+ function getExtractorNames(cycleDef) {
283
+ const assayBlock = cycleDef?.frontmatter?.assay;
284
+ return Array.isArray(assayBlock?.extractors) ? assayBlock.extractors : [];
285
+ }
286
+
287
+ function shouldLoadExtractors(cycleDef, stage) {
288
+ const stageBase = typeof stage === 'string' ? stage.split(':')[0] : '';
289
+ return stageBase === 'forge' && getExtractorNames(cycleDef).length > 0;
290
+ }
291
+
292
+ async function loadExtractors(worktree, cycleDef, io) {
293
+ const extractorNames = getExtractorNames(cycleDef);
294
+ const foundryDir = path.join(worktree, 'foundry');
295
+ const extractors = [];
296
+ for (const name of extractorNames) {
297
+ try {
298
+ const ex = await loadExtractor(foundryDir, name, io);
299
+ extractors.push({ name: ex.name, body: ex.body });
300
+ } catch (err) {
301
+ if (process.env.FOUNDRY_DIAGNOSTICS === '1') {
302
+ console.error(`buildCyclePromptExtras: Failed to load extractor '${name}': ${err.message}`);
303
+ }
304
+ }
305
+ }
306
+ return extractors.length > 0 ? extractors : undefined;
307
+ }
308
+
309
+ async function buildCyclePromptInternal({ worktree, cycleId, stage }) {
310
+ const io = makeMemoryIO(worktree);
311
+ const store = await getOrOpenStore({ worktreeRoot: worktree, io });
312
+ const ctx = getContext(worktree);
313
+ if (!ctx) return '';
314
+ const cycleDef = await getCycleDefinition('foundry', cycleId, io);
315
+ const perms = resolvePermissions({ cycleFrontmatter: cycleDef.frontmatter, vocabulary: ctx.vocabulary });
316
+
317
+ let extractors;
318
+ if (shouldLoadExtractors(cycleDef, stage)) {
319
+ extractors = await loadExtractors(worktree, cycleDef, io);
320
+ }
321
+
322
+ return renderMemoryPrompt({ permissions: perms, schema: store?.schema, extractors });
323
+ }
324
+
325
+ /**
326
+ * Build the memory-vocabulary block for a cycle's dispatch prompt.
327
+ * Returns '' on any error (memory not initialised, drifted, etc.) so that
328
+ * flow dispatch never fails due to memory.
329
+ */
330
+ export async function buildCyclePromptExtras({ worktree, cycleId, stage }) {
331
+ if (!cycleId) return '';
332
+ try {
333
+ return await buildCyclePromptInternal({ worktree, cycleId, stage });
334
+ } catch (err) {
335
+ if (process.env.FOUNDRY_DIAGNOSTICS === '1') {
336
+ console.error(`buildCyclePromptExtras: Memory context failed for cycle '${cycleId}': ${err.message}`);
337
+ }
338
+ return '';
339
+ }
340
+ }
@@ -0,0 +1,20 @@
1
+ import path from 'path';
2
+ import { loadHistory } from '../../../scripts/lib/history.js';
3
+ import { makeIO } from './helpers.js';
4
+
5
+ export function createHistoryTools({ tool }) {
6
+ return {
7
+ foundry_history_list: tool({
8
+ description: 'List history entries for a cycle',
9
+ args: {
10
+ cycle: tool.schema.string().describe('Cycle name'),
11
+ },
12
+ async execute(args, context) {
13
+ const io = makeIO(context.worktree);
14
+ const historyPath = path.join(context.worktree, 'WORK.history.yaml');
15
+ const entries = loadHistory(historyPath, args.cycle, io);
16
+ return JSON.stringify(entries);
17
+ },
18
+ }),
19
+ };
20
+ }
@@ -0,0 +1,296 @@
1
+ import path from 'path';
2
+ import { existsSync, unlinkSync, renameSync } from 'fs';
3
+ import { createEntityType as admCreateEntity } from '../../../scripts/lib/memory/admin/create-entity-type.js';
4
+ import { createExtractor as admCreateExtractor } from '../../../scripts/lib/memory/admin/create-extractor.js';
5
+ import { createEdgeType as admCreateEdge } from '../../../scripts/lib/memory/admin/create-edge-type.js';
6
+ import { renameEntityType as admRenameEntity } from '../../../scripts/lib/memory/admin/rename-entity-type.js';
7
+ import { renameEdgeType as admRenameEdge } from '../../../scripts/lib/memory/admin/rename-edge-type.js';
8
+ import { dropEntityType as admDropEntity } from '../../../scripts/lib/memory/admin/drop-entity-type.js';
9
+ import { dropEdgeType as admDropEdge } from '../../../scripts/lib/memory/admin/drop-edge-type.js';
10
+ import { resetMemory as admReset } from '../../../scripts/lib/memory/admin/reset.js';
11
+ import { validateMemory as admValidate } from '../../../scripts/lib/memory/admin/validate.js';
12
+ import { dumpMemory as admDump } from '../../../scripts/lib/memory/admin/dump.js';
13
+ import { vacuumMemory as admVacuum } from '../../../scripts/lib/memory/admin/vacuum.js';
14
+ import { reembed as admReembed } from '../../../scripts/lib/memory/admin/reembed.js';
15
+ import { initMemory as admInitMemory } from '../../../scripts/lib/memory/admin/init.js';
16
+ import { loadMemoryConfig, writeMemoryConfig } from '../../../scripts/lib/memory/config.js';
17
+ import { embed as memEmbed, probeEmbeddings as memProbeEmbeddings } from '../../../scripts/lib/memory/embeddings.js';
18
+ import { withStore } from './memory-helpers.js';
19
+ import { makeMemoryIO, makeIO, makeExec, errorJson, branchIoFactory, asyncIoFactory } from './helpers.js';
20
+ import { requireNotFailed } from '../../../scripts/lib/failed-flow.js';
21
+ import { requireOnConfigBranch } from '../../../scripts/lib/branch-guard.js';
22
+ import { guarded } from '../../../scripts/lib/guards.js';
23
+
24
+ // Failed-flow guard policy for memory admin tools.
25
+ //
26
+ // Rule: any tool that mutates state (disk or live DB) is blocked when
27
+ // WORK.md has `status: failed`, because the work-branch FS is the
28
+ // source-of-truth that gets thrown away on abandon-and-retry, and any
29
+ // further mutations would be lost or compound the drift.
30
+ //
31
+ // Read-only diagnostics (`dump`, `validate`) are intentionally exempt —
32
+ // the human/LLM needs them to figure out what went wrong before
33
+ // abandoning the cycle.
34
+ //
35
+ // requireNotFailed expects a sync IO (it parses WORK.md synchronously),
36
+ // so we use makeIO here in place of the async makeMemoryIO that the
37
+ // admin tool bodies themselves consume.
38
+ function notFailedGuard(_args, context) {
39
+ return requireNotFailed(makeIO(context.worktree));
40
+ }
41
+
42
+ // Schema-mutating admin tools must run on a config/<description> branch
43
+ // so the resulting commits land on a branch that finishes via
44
+ // `foundry_git_finish` (config kind) and stay isolated from main and any
45
+ // in-progress flow work branch.
46
+ //
47
+ // Read-only tools (validate, dump) and the meta tool (vacuum) are exempt
48
+ // — vacuum touches no tracked files and the read-only tools are needed
49
+ // for diagnosis from any branch.
50
+ function configBranchGuard(_args, context) {
51
+ return requireOnConfigBranch({ exec: makeExec(context.worktree) });
52
+ }
53
+
54
+ // ── Execute handler factories ──────────────────────────────────────────
55
+
56
+ function adminExecute(adminFn) {
57
+ return guarded(adminFn.name, [configBranchGuard, notFailedGuard], async (args, context) => {
58
+ try {
59
+ const io = makeMemoryIO(context.worktree);
60
+ const out = await adminFn({ worktreeRoot: context.worktree, io, ...args });
61
+ return JSON.stringify(out);
62
+ } catch (err) { return errorJson(err); }
63
+ }, { branchIo: branchIoFactory, io: asyncIoFactory });
64
+ }
65
+
66
+ // ── Individual tool factories ──────────────────────────────────────────
67
+
68
+ function toolCreateEntityType({ tool }) {
69
+ return tool({
70
+ description: 'Create a new entity type with a prose body brief.',
71
+ args: {
72
+ name: tool.schema.string(),
73
+ body: tool.schema.string(),
74
+ },
75
+ execute: adminExecute(admCreateEntity),
76
+ });
77
+ }
78
+
79
+ function toolExtractorCreate({ tool }) {
80
+ return tool({
81
+ description: 'Create a new extractor definition under foundry/memory/extractors/.',
82
+ args: {
83
+ name: tool.schema.string(),
84
+ command: tool.schema.string(),
85
+ memoryWrite: tool.schema.array(tool.schema.string()),
86
+ body: tool.schema.string(),
87
+ timeout: tool.schema.string().optional(),
88
+ },
89
+ execute: adminExecute(admCreateExtractor),
90
+ });
91
+ }
92
+
93
+ function toolCreateEdgeType({ tool }) {
94
+ return tool({
95
+ description: 'Create a new edge type.',
96
+ args: {
97
+ name: tool.schema.string(),
98
+ sources: tool.schema.union([tool.schema.literal('any'), tool.schema.array(tool.schema.string())]),
99
+ targets: tool.schema.union([tool.schema.literal('any'), tool.schema.array(tool.schema.string())]),
100
+ body: tool.schema.string(),
101
+ },
102
+ execute: adminExecute(admCreateEdge),
103
+ });
104
+ }
105
+
106
+ function toolRenameEntityType({ tool }) {
107
+ return tool({
108
+ description: 'Rename an entity type and cascade updates to edges and rows.',
109
+ args: { from: tool.schema.string(), to: tool.schema.string() },
110
+ execute: adminExecute(admRenameEntity),
111
+ });
112
+ }
113
+
114
+ function toolRenameEdgeType({ tool }) {
115
+ return tool({
116
+ description: 'Rename an edge type.',
117
+ args: { from: tool.schema.string(), to: tool.schema.string() },
118
+ execute: adminExecute(admRenameEdge),
119
+ });
120
+ }
121
+
122
+ function toolDropEntityType({ tool }) {
123
+ return tool({
124
+ description:
125
+ 'Destructive. Delete an entity type and cascade to affected edges. Call without confirm (or confirm:false) to get a preview of what would be deleted. Pass confirm:true to actually drop.',
126
+ args: { name: tool.schema.string(), confirm: tool.schema.boolean().optional() },
127
+ execute: adminExecute(admDropEntity),
128
+ });
129
+ }
130
+
131
+ function toolDropEdgeType({ tool }) {
132
+ return tool({
133
+ description:
134
+ 'Destructive. Delete an edge type. Call without confirm (or confirm:false) to preview row count. Pass confirm:true to actually drop.',
135
+ args: { name: tool.schema.string(), confirm: tool.schema.boolean().optional() },
136
+ execute: adminExecute(admDropEdge),
137
+ });
138
+ }
139
+
140
+ function toolReset({ tool }) {
141
+ return tool({
142
+ description: 'Destructive. Purge all memory data (keeps type definitions). Requires confirm: true.',
143
+ args: { confirm: tool.schema.boolean() },
144
+ execute: adminExecute(admReset),
145
+ });
146
+ }
147
+
148
+ function toolValidate({ tool }) {
149
+ return tool({
150
+ description: 'Run load-time and drift checks; returns a report.',
151
+ args: {},
152
+ async execute(_args, context) {
153
+ try {
154
+ const io = makeMemoryIO(context.worktree);
155
+ return JSON.stringify(await admValidate({ io }));
156
+ } catch (err) { return errorJson(err); }
157
+ },
158
+ });
159
+ }
160
+
161
+ function toolInit({ tool }) {
162
+ return tool({
163
+ description:
164
+ 'Scaffold foundry/memory/: creates entities/edges/relations dirs with .gitkeep, writes config.md and schema.json, appends .gitignore entries, and optionally probes the embedding provider. Fails if foundry/memory/ already exists.',
165
+ args: {
166
+ embeddings_enabled: tool.schema.boolean().optional(),
167
+ probe: tool.schema.boolean().optional(),
168
+ },
169
+ execute: guarded('foundry_memory_init', [configBranchGuard, notFailedGuard], async (args, context) => {
170
+ try {
171
+ const io = makeMemoryIO(context.worktree);
172
+ const out = await admInitMemory({
173
+ io,
174
+ embeddingsEnabled: args.embeddings_enabled ?? true,
175
+ probe: args.probe ?? true,
176
+ });
177
+ return JSON.stringify(out);
178
+ } catch (err) { return errorJson(err); }
179
+ }, { branchIo: branchIoFactory, io: asyncIoFactory }),
180
+ });
181
+ }
182
+
183
+ function toolDump({ tool }) {
184
+ return tool({
185
+ description: 'Human-readable snapshot of memory. Optional type + name.',
186
+ args: {
187
+ type: tool.schema.string().optional(),
188
+ name: tool.schema.string().optional(),
189
+ depth: tool.schema.number().optional(),
190
+ },
191
+ async execute(args, context) {
192
+ try {
193
+ const { store, vocabulary } = await withStore(context);
194
+ const dump = await admDump({ store, vocabulary, ...args });
195
+ return JSON.stringify({ dump });
196
+ } catch (err) { return errorJson(err); }
197
+ },
198
+ });
199
+ }
200
+
201
+ function toolVacuum({ tool }) {
202
+ return tool({
203
+ description: 'Compact the Cozo database.',
204
+ args: {},
205
+ execute: guarded('foundry_memory_vacuum', [notFailedGuard], async (_args, context) => {
206
+ try {
207
+ const { store } = await withStore(context);
208
+ return JSON.stringify(await admVacuum({ store }));
209
+ } catch (err) { return errorJson(err); }
210
+ }, { branchIo: branchIoFactory, io: asyncIoFactory }),
211
+ });
212
+ }
213
+
214
+ // ── change_embedding_model helpers ─────────────────────────────────────
215
+
216
+ function buildNewEmbeddingConfig(baseConfig, args) {
217
+ return {
218
+ ...baseConfig,
219
+ enabled: true,
220
+ model: args.model,
221
+ dimensions: args.dimensions,
222
+ baseURL: args.baseURL ?? baseConfig.baseURL,
223
+ apiKey: args.apiKey ?? baseConfig.apiKey,
224
+ };
225
+ }
226
+
227
+ function validateEmbeddingProbe(probe, args) {
228
+ if (!probe.ok) return new Error(`probe failed: ${probe.error}`);
229
+ if (probe.dimensions !== args.dimensions) {
230
+ return new Error(`provider returned ${probe.dimensions}-dim vectors, config declares ${args.dimensions}`);
231
+ }
232
+ return null;
233
+ }
234
+
235
+ function makeRawIO() {
236
+ return {
237
+ exists: (p) => existsSync(p),
238
+ unlink: (p) => { if (existsSync(p)) unlinkSync(p); },
239
+ rename: (from, to) => renameSync(from, to),
240
+ };
241
+ }
242
+
243
+ function toolChangeEmbeddingModel({ tool }) {
244
+ return tool({
245
+ description: 'Swap the embedding model and re-embed all existing entities.',
246
+ args: {
247
+ model: tool.schema.string(),
248
+ dimensions: tool.schema.number(),
249
+ baseURL: tool.schema.string().optional(),
250
+ apiKey: tool.schema.string().optional(),
251
+ },
252
+ execute: guarded('foundry_memory_change_embedding_model', [configBranchGuard, notFailedGuard], async (args, context) => {
253
+ try {
254
+ const io = makeMemoryIO(context.worktree);
255
+ const currentConfig = await loadMemoryConfig('foundry', io);
256
+ const newConfig = buildNewEmbeddingConfig(currentConfig.embeddings, args);
257
+ const probe = await memProbeEmbeddings({ config: newConfig });
258
+ const probeError = validateEmbeddingProbe(probe, args);
259
+ if (probeError) return errorJson(probeError);
260
+ const dbAbsolutePath = path.join(context.worktree, 'foundry/memory/memory.db');
261
+ const embedder = (inputs) => memEmbed({ config: newConfig, inputs });
262
+ const out = await admReembed({
263
+ worktreeRoot: context.worktree,
264
+ io,
265
+ rawIO: makeRawIO(),
266
+ dbAbsolutePath,
267
+ newModel: args.model,
268
+ newDimensions: args.dimensions,
269
+ embedder,
270
+ });
271
+ await writeMemoryConfig('foundry', { embeddings: newConfig }, io);
272
+ return JSON.stringify(out);
273
+ } catch (err) { return errorJson(err); }
274
+ }, { branchIo: branchIoFactory, io: asyncIoFactory }),
275
+ });
276
+ }
277
+
278
+ // ── Export ─────────────────────────────────────────────────────────────
279
+
280
+ export function createMemoryAdminTools({ tool }) {
281
+ return {
282
+ foundry_memory_create_entity_type: toolCreateEntityType({ tool }),
283
+ foundry_extractor_create: toolExtractorCreate({ tool }),
284
+ foundry_memory_create_edge_type: toolCreateEdgeType({ tool }),
285
+ foundry_memory_rename_entity_type: toolRenameEntityType({ tool }),
286
+ foundry_memory_rename_edge_type: toolRenameEdgeType({ tool }),
287
+ foundry_memory_drop_entity_type: toolDropEntityType({ tool }),
288
+ foundry_memory_drop_edge_type: toolDropEdgeType({ tool }),
289
+ foundry_memory_reset: toolReset({ tool }),
290
+ foundry_memory_validate: toolValidate({ tool }),
291
+ foundry_memory_init: toolInit({ tool }),
292
+ foundry_memory_dump: toolDump({ tool }),
293
+ foundry_memory_vacuum: toolVacuum({ tool }),
294
+ foundry_memory_change_embedding_model: toolChangeEmbeddingModel({ tool }),
295
+ };
296
+ }