@really-knows-ai/foundry 2.3.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +180 -369
  2. package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
  3. package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
  4. package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
  5. package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
  6. package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
  7. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
  8. package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
  9. package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
  10. package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
  11. package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
  12. package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
  13. package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
  14. package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
  15. package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
  16. package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
  17. package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
  18. package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
  19. package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
  20. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
  21. package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
  22. package/dist/.opencode/plugins/foundry.js +105 -0
  23. package/dist/CHANGELOG.md +533 -0
  24. package/dist/LICENSE +21 -0
  25. package/dist/README.md +278 -0
  26. package/dist/docs/README.md +59 -0
  27. package/dist/docs/architecture.md +433 -0
  28. package/dist/docs/concepts.md +395 -0
  29. package/dist/docs/getting-started.md +344 -0
  30. package/dist/docs/memory-maintenance.md +176 -0
  31. package/dist/docs/tools.md +1411 -0
  32. package/dist/docs/work-spec.md +283 -0
  33. package/dist/scripts/lib/artefacts.js +151 -0
  34. package/dist/scripts/lib/assay/loader.js +151 -0
  35. package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
  36. package/dist/scripts/lib/assay/permissions.js +52 -0
  37. package/dist/scripts/lib/assay/run.js +219 -0
  38. package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
  39. package/dist/scripts/lib/attestation/attest.js +111 -0
  40. package/dist/scripts/lib/attestation/canonical-json.js +109 -0
  41. package/dist/scripts/lib/attestation/hash.js +17 -0
  42. package/dist/scripts/lib/attestation/parse.js +14 -0
  43. package/dist/scripts/lib/attestation/payload.js +106 -0
  44. package/dist/scripts/lib/attestation/render.js +16 -0
  45. package/dist/scripts/lib/attestation/verify.js +15 -0
  46. package/dist/scripts/lib/branch-guard.js +72 -0
  47. package/dist/scripts/lib/config-creators/appraiser.js +9 -0
  48. package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
  49. package/dist/scripts/lib/config-creators/cycle.js +11 -0
  50. package/dist/scripts/lib/config-creators/factory.js +49 -0
  51. package/dist/scripts/lib/config-creators/flow.js +11 -0
  52. package/dist/scripts/lib/config-validators/appraiser.js +49 -0
  53. package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
  54. package/dist/scripts/lib/config-validators/cycle.js +131 -0
  55. package/dist/scripts/lib/config-validators/flow.js +57 -0
  56. package/dist/scripts/lib/config-validators/helpers.js +96 -0
  57. package/dist/scripts/lib/config-validators/law.js +96 -0
  58. package/dist/scripts/lib/config.js +328 -0
  59. package/dist/scripts/lib/failed-flow.js +131 -0
  60. package/dist/scripts/lib/feedback-store.js +249 -0
  61. package/dist/scripts/lib/feedback-transitions.js +105 -0
  62. package/dist/scripts/lib/finalize.js +70 -0
  63. package/dist/scripts/lib/foundational-guards.js +13 -0
  64. package/dist/scripts/lib/git-bridge.js +77 -0
  65. package/dist/scripts/lib/git-finish/work-finish.js +233 -0
  66. package/dist/scripts/lib/git-policy.js +101 -0
  67. package/dist/scripts/lib/guards.js +125 -0
  68. package/dist/scripts/lib/history.js +132 -0
  69. package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
  70. package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
  71. package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
  72. package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
  73. package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
  74. package/dist/scripts/lib/memory/admin/dump.js +47 -0
  75. package/dist/scripts/lib/memory/admin/helpers.js +31 -0
  76. package/dist/scripts/lib/memory/admin/init.js +170 -0
  77. package/dist/scripts/lib/memory/admin/live-store.js +76 -0
  78. package/dist/scripts/lib/memory/admin/reembed.js +285 -0
  79. package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
  80. package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
  81. package/dist/scripts/lib/memory/admin/reset.js +24 -0
  82. package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
  83. package/dist/scripts/lib/memory/admin/validate.js +19 -0
  84. package/dist/scripts/lib/memory/config.js +149 -0
  85. package/dist/scripts/lib/memory/cozo.js +136 -0
  86. package/dist/scripts/lib/memory/drift.js +71 -0
  87. package/dist/scripts/lib/memory/embeddings.js +128 -0
  88. package/dist/scripts/lib/memory/frontmatter.js +75 -0
  89. package/dist/scripts/lib/memory/ndjson.js +84 -0
  90. package/dist/scripts/lib/memory/paths.js +25 -0
  91. package/dist/scripts/lib/memory/permissions.js +41 -0
  92. package/dist/scripts/lib/memory/prompt.js +109 -0
  93. package/dist/scripts/lib/memory/query.js +56 -0
  94. package/dist/scripts/lib/memory/reads.js +109 -0
  95. package/dist/scripts/lib/memory/schema.js +64 -0
  96. package/dist/scripts/lib/memory/search.js +73 -0
  97. package/dist/scripts/lib/memory/singleton.js +49 -0
  98. package/dist/scripts/lib/memory/store.js +162 -0
  99. package/dist/scripts/lib/memory/types.js +93 -0
  100. package/dist/scripts/lib/memory/validate.js +58 -0
  101. package/dist/scripts/lib/memory/writes.js +40 -0
  102. package/{scripts → dist/scripts}/lib/pending.js +7 -2
  103. package/dist/scripts/lib/secret.js +59 -0
  104. package/{scripts → dist/scripts}/lib/slug.js +3 -2
  105. package/dist/scripts/lib/snapshot/finish.js +103 -0
  106. package/dist/scripts/lib/snapshot/inspect.js +253 -0
  107. package/dist/scripts/lib/snapshot/render.js +55 -0
  108. package/dist/scripts/lib/sort-fs-check.js +121 -0
  109. package/dist/scripts/lib/sort-routing.js +101 -0
  110. package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
  111. package/{scripts → dist/scripts}/lib/state.js +4 -0
  112. package/dist/scripts/lib/token.js +57 -0
  113. package/dist/scripts/lib/tracing.js +59 -0
  114. package/dist/scripts/lib/ulid.js +100 -0
  115. package/dist/scripts/lib/validator-jsonl.js +162 -0
  116. package/{scripts → dist/scripts}/lib/workfile.js +38 -20
  117. package/dist/scripts/orchestrate-cycle.js +215 -0
  118. package/dist/scripts/orchestrate-phases.js +314 -0
  119. package/dist/scripts/orchestrate.js +163 -0
  120. package/dist/scripts/sort.js +278 -0
  121. package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
  122. package/{skills → dist/skills}/add-artefact-type/SKILL.md +62 -40
  123. package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
  124. package/dist/skills/add-extractor/SKILL.md +133 -0
  125. package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
  126. package/dist/skills/add-law/SKILL.md +191 -0
  127. package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
  128. package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
  129. package/{skills → dist/skills}/appraise/SKILL.md +62 -13
  130. package/dist/skills/assay/SKILL.md +72 -0
  131. package/dist/skills/change-embedding-model/SKILL.md +58 -0
  132. package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
  133. package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
  134. package/dist/skills/dry-run/SKILL.md +116 -0
  135. package/{skills → dist/skills}/flow/SKILL.md +15 -2
  136. package/dist/skills/forge/SKILL.md +121 -0
  137. package/dist/skills/human-appraise/SKILL.md +153 -0
  138. package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
  139. package/dist/skills/init-memory/SKILL.md +92 -0
  140. package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
  141. package/dist/skills/quench/SKILL.md +99 -0
  142. package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
  143. package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
  144. package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
  145. package/dist/skills/reset-memory/SKILL.md +54 -0
  146. package/dist/skills/upgrade-foundry/SKILL.md +191 -0
  147. package/package.json +34 -17
  148. package/.opencode/plugins/foundry.js +0 -761
  149. package/CHANGELOG.md +0 -100
  150. package/docs/concepts.md +0 -122
  151. package/docs/getting-started.md +0 -187
  152. package/docs/work-spec.md +0 -207
  153. package/scripts/lib/artefacts.js +0 -124
  154. package/scripts/lib/config.js +0 -175
  155. package/scripts/lib/feedback-transitions.js +0 -25
  156. package/scripts/lib/feedback.js +0 -440
  157. package/scripts/lib/finalize.js +0 -41
  158. package/scripts/lib/history.js +0 -59
  159. package/scripts/lib/secret.js +0 -23
  160. package/scripts/lib/tags.js +0 -108
  161. package/scripts/lib/token.js +0 -26
  162. package/scripts/orchestrate.js +0 -418
  163. package/scripts/sort.js +0 -370
  164. package/scripts/validate-tags.js +0 -54
  165. package/skills/add-law/SKILL.md +0 -111
  166. package/skills/forge/SKILL.md +0 -88
  167. package/skills/human-appraise/SKILL.md +0 -82
  168. package/skills/quench/SKILL.md +0 -62
  169. package/skills/upgrade-foundry/SKILL.md +0 -216
  170. /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
@@ -0,0 +1,104 @@
1
+ // withStore — shared memory-tool helper. Resolves store, vocabulary,
2
+ // permissions and embedder from plugin context.
3
+
4
+ import { getOrOpenStore, getContext } from '../../../scripts/lib/memory/singleton.js';
5
+ import { syncStore } from '../../../scripts/lib/memory/store.js';
6
+ import { getCycleDefinition } from '../../../scripts/lib/config.js';
7
+ import { resolvePermissions } from '../../../scripts/lib/memory/permissions.js';
8
+ import { embed as memEmbed } from '../../../scripts/lib/memory/embeddings.js';
9
+ import { makeMemoryIO, makeIO } from './helpers.js';
10
+ import { readActiveStage } from '../../../scripts/lib/state.js';
11
+
12
+ // Resolve the cycle id to scope this memory call to. Prefers an explicit
13
+ // `context.cycle` (used by orchestrate-driven dispatch); otherwise falls back
14
+ // to `.foundry/active-stage.json` so that tool calls made inside an active
15
+ // stage with a minimal context (e.g. `{ worktree }`) still respect cycle
16
+ // permissions. Returns `{ cycleId, fromActiveStage }` where `cycleId` may be
17
+ // null when neither source provides one (out-of-stage call → unscoped).
18
+ function resolveCycleId(context) {
19
+ if (context.cycle) return { cycleId: context.cycle, fromActiveStage: false };
20
+ try {
21
+ const syncIo = makeIO(context.worktree);
22
+ const active = readActiveStage(syncIo);
23
+ if (active && active.cycle) return { cycleId: active.cycle, fromActiveStage: true };
24
+ } catch {
25
+ // Treat unreadable/corrupt active-stage as no active stage. The stage
26
+ // guard checks elsewhere already cover the integrity case for stage
27
+ // tools; memory tools continue with unscoped behaviour so unrelated
28
+ // direct calls stay available.
29
+ }
30
+ return { cycleId: null, fromActiveStage: false };
31
+ }
32
+
33
+ // Build an embedder function from the embeddings config, or null when
34
+ // embeddings are disabled.
35
+ function createEmbedder(embeddingsCfg) {
36
+ if (!embeddingsCfg || !embeddingsCfg.enabled) return null;
37
+ return (inputs) => memEmbed({ config: embeddingsCfg, inputs });
38
+ }
39
+
40
+ // Build a write-capable embedder. Requires both a working embedder and
41
+ // schema-declared vector dimensions (provisioned by init-memory).
42
+ function createWriteEmbedder(embedder, schemaEmbeddings) {
43
+ if (!embedder) return null;
44
+ if (!schemaEmbeddings || !schemaEmbeddings.dimensions) return null;
45
+ return embedder;
46
+ }
47
+
48
+ // Resolve cycle-scoped permissions. When the cycle definition cannot be
49
+ // loaded and the call originated from an active stage, the error is
50
+ // rethrown; otherwise permissions fall back to null (full access).
51
+ async function resolveCyclePermissions(cycleId, fromActiveStage, io, vocabulary) {
52
+ try {
53
+ const cycleDef = await getCycleDefinition('foundry', cycleId, io);
54
+ return resolvePermissions({ cycleFrontmatter: cycleDef.frontmatter, vocabulary });
55
+ } catch (err) {
56
+ if (fromActiveStage) {
57
+ throw new Error(
58
+ `active stage references cycle '${cycleId}' but its definition could not be loaded: ${err.message ?? err}`,
59
+ { cause: err },
60
+ );
61
+ }
62
+ return null;
63
+ }
64
+ }
65
+
66
+ // Extract embeddings config and schema from a possibly-null context.
67
+ function getContextEmbeddings(ctx) {
68
+ return {
69
+ embeddingsCfg: ctx ? ctx.config?.embeddings : null,
70
+ schemaEmbeddings: ctx ? ctx.schema?.embeddings : null,
71
+ };
72
+ }
73
+
74
+ // Build a sync callback that reconciles the store when the call is
75
+ // unscoped (no active cycle).
76
+ function makeSyncCallback(cycleId, store, io) {
77
+ return async () => {
78
+ if (!cycleId) await syncStore({ store, io });
79
+ };
80
+ }
81
+
82
+ export async function withStore(context) {
83
+ const io = makeMemoryIO(context.worktree);
84
+ const store = await getOrOpenStore({ worktreeRoot: context.worktree, io });
85
+ const ctx = getContext(context.worktree);
86
+ const { embeddingsCfg, schemaEmbeddings } = getContextEmbeddings(ctx);
87
+ const embedder = createEmbedder(embeddingsCfg);
88
+ const writeEmbedder = createWriteEmbedder(embedder, schemaEmbeddings);
89
+ const { cycleId, fromActiveStage } = resolveCycleId(context);
90
+ let permissions = null;
91
+ if (cycleId) {
92
+ permissions = await resolveCyclePermissions(cycleId, fromActiveStage, io, ctx.vocabulary);
93
+ }
94
+ const syncIfOutOfCycle = makeSyncCallback(cycleId, store, io);
95
+ return {
96
+ io,
97
+ store,
98
+ vocabulary: ctx.vocabulary,
99
+ permissions,
100
+ embedder,
101
+ writeEmbedder,
102
+ syncIfOutOfCycle,
103
+ };
104
+ }
@@ -0,0 +1,286 @@
1
+ import { putEntity, relate as memRelate, unrelate as memUnrelate } from '../../../scripts/lib/memory/writes.js';
2
+ import { getEntity, listEntities, neighbours as memNeighbours } from '../../../scripts/lib/memory/reads.js';
3
+ import { runQuery } from '../../../scripts/lib/memory/query.js';
4
+ import { checkEntityRead, checkEntityWrite, checkEdgeRead, checkEdgeWrite } from '../../../scripts/lib/memory/permissions.js';
5
+ import { search as memSearch } from '../../../scripts/lib/memory/search.js';
6
+ import { withStore } from './memory-helpers.js';
7
+ import { errorJson, makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
8
+ import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
9
+
10
+ const gateNotFailed = notFailedGuard(makeIO);
11
+ const MAX_NEIGHBOUR_DEPTH = 5;
12
+ const MAX_SEARCH_K = 100;
13
+ const SPECIAL_CHARS = { '#': 'line', '"': 'string', "'": 'string' };
14
+ function skipLineComment(datalog, start) {
15
+ let end = start;
16
+ while (end < datalog.length && datalog[end] !== '\n') {
17
+ end += 1;
18
+ }
19
+ if (end < datalog.length) {
20
+ return { nextIndex: end + 1, output: '\n' };
21
+ }
22
+ return { nextIndex: end, output: '' };
23
+ }
24
+ function skipBlockComment(datalog, start) {
25
+ let out = '';
26
+ let i = start;
27
+ while (i < datalog.length) {
28
+ if (datalog[i] === '*' && datalog[i + 1] === '/') {
29
+ return { nextIndex: i + 2, output: out + ' ' };
30
+ }
31
+ if (datalog[i] === '\n') {
32
+ out += '\n';
33
+ }
34
+ i += 1;
35
+ }
36
+ return { nextIndex: i, output: out };
37
+ }
38
+ function readStringLiteral(datalog, start, quote) {
39
+ let out = quote;
40
+ let i = start + 1;
41
+ let escaped = false;
42
+ while (i < datalog.length) {
43
+ const ch = datalog[i];
44
+ out += ch;
45
+ if (escaped) {
46
+ escaped = false;
47
+ } else if (ch === '\\') {
48
+ escaped = true;
49
+ } else if (ch === quote) {
50
+ return { nextIndex: i + 1, output: out };
51
+ }
52
+ i += 1;
53
+ }
54
+ return { nextIndex: i, output: out };
55
+ }
56
+ function classifyChar(ch, next) {
57
+ if (ch in SPECIAL_CHARS) return SPECIAL_CHARS[ch];
58
+ if (ch === '/') {
59
+ if (next === '*') return 'block';
60
+ }
61
+ return 'normal';
62
+ }
63
+ function stripQueryComments(datalog) {
64
+ let out = '';
65
+ let i = 0;
66
+ while (i < datalog.length) {
67
+ const kind = classifyChar(datalog[i], datalog[i + 1]);
68
+ if (kind === 'line') {
69
+ const result = skipLineComment(datalog, i + 1);
70
+ out += result.output;
71
+ i = result.nextIndex;
72
+ } else if (kind === 'block') {
73
+ const result = skipBlockComment(datalog, i + 2);
74
+ out += result.output;
75
+ i = result.nextIndex;
76
+ } else if (kind === 'string') {
77
+ const result = readStringLiteral(datalog, i, datalog[i]);
78
+ out += result.output;
79
+ i = result.nextIndex;
80
+ } else {
81
+ out += datalog[i];
82
+ i += 1;
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ function referencedStoredRelations(datalog) {
88
+ const matches = stripQueryComments(datalog).matchAll(/\*([a-z][a-z0-9_]*)\s*\{/g);
89
+ const names = new Set();
90
+ for (const match of matches) {
91
+ const name = match[1];
92
+ if (name.startsWith('ent_') || name.startsWith('edge_')) names.add(name);
93
+ }
94
+ return [...names];
95
+ }
96
+ function validateNeighbourDepth(depth) {
97
+ if ((depth ?? 1) > MAX_NEIGHBOUR_DEPTH) {
98
+ throw new Error(`depth must be ≤ ${MAX_NEIGHBOUR_DEPTH}`);
99
+ }
100
+ }
101
+ function filterEdgeTypes(edgeTypes, permissions) {
102
+ if (!permissions) return edgeTypes;
103
+ return edgeTypes.filter((e) => checkEdgeRead(permissions, e));
104
+ }
105
+ function filterNeighbourResult(result, permissions) {
106
+ if (!permissions) return result;
107
+ return {
108
+ entities: result.entities.filter((e) => checkEntityRead(permissions, e.type)),
109
+ edges: result.edges.filter((e) =>
110
+ checkEntityRead(permissions, e.from_type) && checkEntityRead(permissions, e.to_type),
111
+ ),
112
+ };
113
+ }
114
+ function validateSearchK(k) {
115
+ if ((k ?? 5) > MAX_SEARCH_K) {
116
+ throw new Error(`k must be ≤ ${MAX_SEARCH_K}`);
117
+ }
118
+ }
119
+ function resolveSearchTypes(typeFilter, permissions, vocabulary) {
120
+ const types = typeFilter && typeFilter.length > 0
121
+ ? typeFilter
122
+ : Object.keys(vocabulary.entities);
123
+ if (!permissions) return types;
124
+ return types.filter((t) => checkEntityRead(permissions, t));
125
+ }
126
+ function createWriteTool(tool, name, description, ops) {
127
+ return tool({
128
+ description,
129
+ args: ops.makeArgs(tool),
130
+ execute: guarded(name, [flowBranchGuard, gateNotFailed], async (args, context) => {
131
+ try {
132
+ const { store, vocabulary, permissions, writeEmbedder, syncIfOutOfCycle } = await withStore(context);
133
+ if (permissions && !ops.checkPerm(permissions, args)) {
134
+ return errorJson(new Error(ops.errMsg(context.cycle, args)));
135
+ }
136
+ await ops.doOp(store, args, vocabulary, { embedder: writeEmbedder });
137
+ await syncIfOutOfCycle();
138
+ return JSON.stringify({ ok: true });
139
+ } catch (err) { return errorJson(err); }
140
+ }, { branchIo: branchIoFactory, io: asyncIoFactory }),
141
+ });
142
+ }
143
+ function createPutTool(tool) {
144
+ return createWriteTool(tool, 'foundry_memory_put', 'Upsert an entity into flow memory. Value must be ≤4KB.', {
145
+ makeArgs: (t) => ({
146
+ type: t.schema.string().describe('Entity type (must be declared)'),
147
+ name: t.schema.string().describe('Entity name (unique within type)'),
148
+ value: t.schema.string().describe('Free-text intrinsic description (≤4KB)'),
149
+ }),
150
+ checkPerm: (p, a) => checkEntityWrite(p, a.type),
151
+ doOp: (s, a, v, o) => putEntity(s, a, v, o),
152
+ errMsg: (c, a) => `cycle '${c}' does not have write permission on entity type '${a.type}'`,
153
+ });
154
+ }
155
+ function createRelateTool(tool) {
156
+ return createWriteTool(tool, 'foundry_memory_relate', 'Upsert an edge between two entities.', {
157
+ makeArgs: (t) => ({
158
+ from_type: t.schema.string(), from_name: t.schema.string(),
159
+ edge_type: t.schema.string(), to_type: t.schema.string(), to_name: t.schema.string(),
160
+ }),
161
+ checkPerm: (p, a) => checkEdgeWrite(p, a.edge_type),
162
+ doOp: (s, a, v) => memRelate(s, a, v),
163
+ errMsg: (c, a) => `cycle '${c}' does not have write permission on edge type '${a.edge_type}'`,
164
+ });
165
+ }
166
+ function createUnrelateTool(tool) {
167
+ return createWriteTool(tool, 'foundry_memory_unrelate', 'Delete an edge between two entities.', {
168
+ makeArgs: (t) => ({
169
+ from_type: t.schema.string(), from_name: t.schema.string(),
170
+ edge_type: t.schema.string(), to_type: t.schema.string(), to_name: t.schema.string(),
171
+ }),
172
+ checkPerm: (p, a) => checkEdgeWrite(p, a.edge_type),
173
+ doOp: (s, a, v) => memUnrelate(s, a, v),
174
+ errMsg: (c, a) => `cycle '${c}' does not have write permission on edge type '${a.edge_type}'`,
175
+ });
176
+ }
177
+ function createGetTool(tool) {
178
+ return tool({
179
+ description: 'Fetch a single entity by composite key (type, name).',
180
+ args: { type: tool.schema.string(), name: tool.schema.string() },
181
+ async execute(args, context) {
182
+ try {
183
+ const { store, permissions } = await withStore(context);
184
+ if (permissions && !checkEntityRead(permissions, args.type)) {
185
+ return JSON.stringify(null);
186
+ }
187
+ const ent = await getEntity(store, args);
188
+ return JSON.stringify(ent);
189
+ } catch (err) { return errorJson(err); }
190
+ },
191
+ });
192
+ }
193
+ function createListTool(tool) {
194
+ return tool({
195
+ description: 'List all entities of a given type.',
196
+ args: { type: tool.schema.string() },
197
+ async execute(args, context) {
198
+ try {
199
+ const { store, permissions } = await withStore(context);
200
+ if (permissions && !checkEntityRead(permissions, args.type)) {
201
+ return JSON.stringify([]);
202
+ }
203
+ const out = await listEntities(store, args);
204
+ return JSON.stringify(out);
205
+ } catch (err) { return errorJson(err); }
206
+ },
207
+ });
208
+ }
209
+ function createNeighboursTool(tool) {
210
+ return tool({
211
+ description: 'Bounded graph traversal from an entity. Returns entities and edges within `depth` hops.',
212
+ args: {
213
+ type: tool.schema.string(), name: tool.schema.string(),
214
+ depth: tool.schema.number().optional().describe('Default 1'),
215
+ edge_types: tool.schema.array(tool.schema.string()).optional().describe('Restrict traversal to named edges'),
216
+ },
217
+ async execute(args, context) {
218
+ try {
219
+ validateNeighbourDepth(args.depth);
220
+ const { store, vocabulary, permissions } = await withStore(context);
221
+ if (permissions && !checkEntityRead(permissions, args.type)) {
222
+ return JSON.stringify({ entities: [], edges: [] });
223
+ }
224
+ const edgeTypesInput = args.edge_types ?? Object.keys(vocabulary.edges);
225
+ const filteredEdgeTypes = filterEdgeTypes(edgeTypesInput, permissions);
226
+ const result = await memNeighbours(store, { ...args, edge_types: filteredEdgeTypes }, vocabulary);
227
+ return JSON.stringify(filterNeighbourResult(result, permissions));
228
+ } catch (err) { return errorJson(err); }
229
+ },
230
+ });
231
+ }
232
+ function createQueryTool(tool) {
233
+ return tool({
234
+ description: 'Arbitrary read-only Cozo Datalog query. Rejects :put, :rm, :create, ::remove. Returns {headers, rows}.',
235
+ args: { datalog: tool.schema.string().describe('Cozo Datalog query (read-only)') },
236
+ async execute(args, context) {
237
+ try {
238
+ const { store, vocabulary, permissions } = await withStore(context);
239
+ if (permissions) {
240
+ const allowed = new Set([
241
+ ...[...permissions.readTypes].map((t) => `ent_${t}`),
242
+ ...Object.keys(vocabulary.edges).filter((e) => checkEdgeRead(permissions, e)).map((e) => `edge_${e}`),
243
+ ]);
244
+ const referenced = referencedStoredRelations(args.datalog);
245
+ for (const r of referenced) {
246
+ if (!allowed.has(r)) {
247
+ return errorJson(new Error(`cycle '${context.cycle}' cannot query relation '${r}' (not in read permissions)`));
248
+ }
249
+ }
250
+ }
251
+ const out = await runQuery(store, args.datalog);
252
+ return JSON.stringify(out);
253
+ } catch (err) { return errorJson(err); }
254
+ },
255
+ });
256
+ }
257
+ function createSearchTool(tool) {
258
+ return tool({
259
+ description: 'Semantic nearest-neighbour search over entity values. Requires embeddings enabled. Performance: fetches k results from each entity type then merges to global top-k (N×k amplification). Use type_filter to limit search to specific types when possible.',
260
+ args: {
261
+ query_text: tool.schema.string(),
262
+ k: tool.schema.number().optional().describe('Default 5'),
263
+ type_filter: tool.schema.array(tool.schema.string()).optional(),
264
+ },
265
+ async execute(args, context) {
266
+ try {
267
+ validateSearchK(args.k);
268
+ const { store, permissions, embedder, vocabulary } = await withStore(context);
269
+ if (!embedder) return errorJson(new Error('embeddings are disabled in memory config'));
270
+ const types = resolveSearchTypes(args.type_filter, permissions, vocabulary);
271
+ const out = await memSearch({
272
+ store, query_text: args.query_text, k: args.k ?? 5, type_filter: types, embedder,
273
+ });
274
+ return JSON.stringify(out);
275
+ } catch (err) { return errorJson(err); }
276
+ },
277
+ });
278
+ }
279
+ export function createMemoryTools({ tool }) {
280
+ return {
281
+ foundry_memory_put: createPutTool(tool), foundry_memory_relate: createRelateTool(tool),
282
+ foundry_memory_unrelate: createUnrelateTool(tool), foundry_memory_get: createGetTool(tool),
283
+ foundry_memory_list: createListTool(tool), foundry_memory_neighbours: createNeighboursTool(tool),
284
+ foundry_memory_query: createQueryTool(tool), foundry_memory_search: createSearchTool(tool),
285
+ };
286
+ }
@@ -0,0 +1,159 @@
1
+ import path from 'path';
2
+ import { execFileSync } from 'child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { readFileSync, writeFileSync } from 'fs';
5
+ import { signToken } from '../../../scripts/lib/token.js';
6
+ import { readOrCreateSecret } from '../../../scripts/lib/secret.js';
7
+ import { getCycleDefinition, getArtefactType } from '../../../scripts/lib/config.js';
8
+ import { addArtefactRow } from '../../../scripts/lib/artefacts.js';
9
+ import { stageBaseOf } from '../../../scripts/lib/stage-guard.js';
10
+ import { finalizeStage } from '../../../scripts/lib/finalize.js';
11
+ import { commitWithPolicy } from '../../../scripts/lib/git-bridge.js';
12
+ import { makeIO, makeExec, buildCyclePromptExtras } from './helpers.js';
13
+ import { requireNotFailed } from '../../../scripts/lib/failed-flow.js';
14
+ import { requireOnFlowBranch } from '../../../scripts/lib/branch-guard.js';
15
+
16
+ function createMint(secret, pending) {
17
+ return ({ route, cycle, exp }) => {
18
+ const nonce = randomUUID();
19
+ const payload = { route, cycle, nonce, exp };
20
+ pending.add(nonce, payload);
21
+ return signToken(payload, secret);
22
+ };
23
+ }
24
+
25
+ function createGitBridge(cwd) {
26
+ const runGit = (argv) => execFileSync('git', argv, { cwd, encoding: 'utf8' });
27
+ return {
28
+ commit: (msg, opts = {}) => {
29
+ const sha = commitWithPolicy({
30
+ message: msg,
31
+ allowedPatterns: opts.allowedPatterns ?? [],
32
+ execFile: runGit,
33
+ });
34
+ return sha;
35
+ },
36
+ status: () => {
37
+ const out = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' }).trim();
38
+ return { clean: out === '', dirty: out.split('\n').filter(Boolean) };
39
+ },
40
+ };
41
+ }
42
+
43
+ function makeRegisterArtefact(cwd, cycleId) {
44
+ const workPath = path.join(cwd, 'WORK.md');
45
+ return ({ file, type, status }) => {
46
+ const text = readFileSync(workPath, 'utf-8');
47
+ const updated = addArtefactRow(text, { file, type, cycle: cycleId, status });
48
+ writeFileSync(workPath, updated, 'utf-8');
49
+ };
50
+ }
51
+
52
+ async function createFinalize(cwd, io) {
53
+ return async ({ cycleId, stage, baseSha }) => {
54
+ let cycleDoc;
55
+ try {
56
+ cycleDoc = await getCycleDefinition('foundry', cycleId, io);
57
+ } catch (e) {
58
+ return { ok: false, error: e.message };
59
+ }
60
+ const outputType = cycleDoc.frontmatter['output-type'];
61
+ const cycleDef = { outputArtefactType: outputType };
62
+ const artefactTypes = {};
63
+ if (outputType) {
64
+ try {
65
+ const artDoc = await getArtefactType('foundry', outputType, io);
66
+ artefactTypes[outputType] = { filePatterns: artDoc.frontmatter['file-patterns'] || [] };
67
+ } catch (e) {
68
+ return {
69
+ ok: false,
70
+ error: `missing_artefact_type: ${outputType} (${e.message})`,
71
+ };
72
+ }
73
+ }
74
+ const result = finalizeStage({
75
+ cwd,
76
+ baseSha,
77
+ stageBase: stageBaseOf(stage),
78
+ cycleDef,
79
+ artefactTypes,
80
+ io,
81
+ registerArtefact: makeRegisterArtefact(cwd, cycleId),
82
+ });
83
+ return result;
84
+ };
85
+ }
86
+
87
+ function getCycleId(result) {
88
+ if (result.cycle) return result.cycle;
89
+ if (typeof result.stage !== 'string') return null;
90
+ return result.stage.split(':')[1];
91
+ }
92
+
93
+ async function injectDispatchPromptExtras(result, cwd) {
94
+ if (!result) return;
95
+ if (result.action !== 'dispatch') return;
96
+ if (typeof result.prompt !== 'string') return;
97
+ const cycleId = getCycleId(result);
98
+ const extras = await buildCyclePromptExtras({ worktree: cwd, cycleId, stage: result.stage });
99
+ if (!extras) return;
100
+ result.prompt = `${result.prompt}\n\n${extras}`;
101
+ }
102
+
103
+ export function createOrchestrateTool({ tool, pending }) {
104
+ return {
105
+ foundry_orchestrate: tool({
106
+ description: 'Run the next step of the current cycle. Call with no args on first invocation; call with lastResult={ok,error?} after a dispatch/human_appraise completes. Returns {action, ...} describing what the caller should do next.',
107
+ args: {
108
+ lastResult: tool.schema.object({
109
+ ok: tool.schema.boolean(),
110
+ error: tool.schema.string().optional(),
111
+ }).optional(),
112
+ cycleDef: tool.schema.string().optional().describe('Test-mode cycle definition override (path to cycle file)'),
113
+ },
114
+
115
+ async execute(args, context) {
116
+ const { runOrchestrate } = await import('../../scripts/orchestrate.js');
117
+ const io = makeIO(context.worktree);
118
+ const cwd = context.worktree;
119
+ const secret = readOrCreateSecret(context.worktree);
120
+
121
+ try {
122
+ // Branch guard. Kept inline because the orchestrate tool surfaces all errors through its violation
123
+ // envelope (see comment on the failed-flow guard below). A
124
+ // wrong-branch refusal is a more fundamental error than failed
125
+ // flow, so it runs first.
126
+ const branchGuard = requireOnFlowBranch({ exec: makeExec(cwd) });
127
+ if (!branchGuard.ok) return JSON.stringify({ error: `foundry_orchestrate: ${branchGuard.error}` });
128
+
129
+ // Failed-flow guard. Kept inline to preserve the violation envelope.
130
+ // because requireNotFailed parses WORK.md frontmatter, which throws
131
+ // on malformed YAML. The surrounding try/catch (line 30) converts
132
+ // that throw into a violation-shaped envelope per the contract
133
+ // exercised by tests/plugin/orchestrate-wrapper.test.js. A guarded()
134
+ // wrapper would let the throw escape to a plain { error } envelope
135
+ // and break that contract. orchestrate-tool is the one Phase 1.5
136
+ // exception to the inline-gate refactor.
137
+ const failedGuard = requireNotFailed(io);
138
+ if (!failedGuard.ok) return JSON.stringify({ error: `foundry_orchestrate: ${failedGuard.error}` });
139
+
140
+ const mint = createMint(secret, pending);
141
+ const git = createGitBridge(cwd);
142
+ const finalize = await createFinalize(cwd, io);
143
+
144
+ const result = await runOrchestrate({
145
+ cwd, cycleDef: args.cycleDef, git, mint, finalize,
146
+ now: () => Date.now(),
147
+ lastResult: args.lastResult ?? null,
148
+ }, io);
149
+
150
+ await injectDispatchPromptExtras(result, cwd);
151
+
152
+ return JSON.stringify(result);
153
+ } catch (e) {
154
+ return JSON.stringify({ action: 'violation', details: `orchestrate threw: ${e.message}`, recoverable: false, affected_files: [] });
155
+ }
156
+ },
157
+ }),
158
+ };
159
+ }
@@ -0,0 +1,104 @@
1
+ // Meta tools for inspecting and managing forensic snapshots produced by
2
+ // dry-run finishes. These tools are branch-agnostic per spec §11.6 — they
3
+ // carry only the foundational guards (gitRepo, foundryRoot) so authors
4
+ // can list/show/delete/prune snapshots from any branch including main.
5
+
6
+ import {
7
+ listSnapshots,
8
+ showSnapshot,
9
+ deleteSnapshot,
10
+ pruneSnapshots,
11
+ } from '../../../scripts/lib/snapshot/inspect.js';
12
+ import { requireGitRepo, requireFoundryRoot } from '../../../scripts/lib/foundational-guards.js';
13
+ import { guarded } from '../../../scripts/lib/guards.js';
14
+ import { makeIO, makeAsyncIO, errorJson, branchIoFactory, asyncIoFactory } from './helpers.js';
15
+
16
+ // --- guard helpers ---------------------------------------------------------
17
+
18
+ function gitRepoGuard(_args, context) {
19
+ return requireGitRepo(makeIO(context.worktree));
20
+ }
21
+
22
+ function foundryRootGuard(_args, context) {
23
+ return requireFoundryRoot(makeIO(context.worktree));
24
+ }
25
+
26
+ const GUARDS = [gitRepoGuard, foundryRootGuard];
27
+ const TRACE_OPTS = { branchIo: branchIoFactory, io: asyncIoFactory };
28
+
29
+ // --- execute handlers -------------------------------------------------------
30
+
31
+ function makeListExecute() {
32
+ return guarded('foundry_snapshot_list', GUARDS, async (_args, context) => {
33
+ try {
34
+ return JSON.stringify(await listSnapshots({ io: makeAsyncIO(context.worktree) }));
35
+ } catch (err) {
36
+ return errorJson(err);
37
+ }
38
+ }, TRACE_OPTS);
39
+ }
40
+
41
+ function makeShowExecute() {
42
+ return guarded('foundry_snapshot_show', GUARDS, async (args, context) => {
43
+ try {
44
+ return JSON.stringify(await showSnapshot({ runId: args.runId, io: makeAsyncIO(context.worktree) }));
45
+ } catch (err) {
46
+ return errorJson(err);
47
+ }
48
+ }, TRACE_OPTS);
49
+ }
50
+
51
+ function makeDeleteExecute() {
52
+ return guarded('foundry_snapshot_delete', GUARDS, async (args, context) => {
53
+ try {
54
+ return JSON.stringify(await deleteSnapshot({
55
+ runId: args.runId, io: makeAsyncIO(context.worktree), confirm: args.confirm === true,
56
+ }));
57
+ } catch (err) {
58
+ return errorJson(err);
59
+ }
60
+ }, TRACE_OPTS);
61
+ }
62
+
63
+ function makePruneExecute() {
64
+ return guarded('foundry_snapshot_prune', GUARDS, async (args, context) => {
65
+ if (!Number.isInteger(args.olderThanDays) || args.olderThanDays <= 0) {
66
+ return JSON.stringify({ ok: false, error: 'olderThanDays must be a positive integer' });
67
+ }
68
+ try {
69
+ return JSON.stringify(await pruneSnapshots({
70
+ olderThanDays: args.olderThanDays, io: makeAsyncIO(context.worktree),
71
+ confirm: args.confirm === true, now: Date.now(),
72
+ }));
73
+ } catch (err) {
74
+ return errorJson(err);
75
+ }
76
+ }, TRACE_OPTS);
77
+ }
78
+
79
+ // --- tool factory ----------------------------------------------------------
80
+
81
+ export function createSnapshotTools({ tool }) {
82
+ return {
83
+ foundry_snapshot_list: tool({
84
+ description: 'List forensic snapshots from past dry-run finishes.',
85
+ args: {},
86
+ execute: makeListExecute(),
87
+ }),
88
+ foundry_snapshot_show: tool({
89
+ description: 'Show a structured summary of one forensic snapshot.',
90
+ args: { runId: tool.schema.string() },
91
+ execute: makeShowExecute(),
92
+ }),
93
+ foundry_snapshot_delete: tool({
94
+ description: 'Delete a forensic snapshot directory. Requires {confirm: true} to actually remove.',
95
+ args: { runId: tool.schema.string(), confirm: tool.schema.boolean().optional() },
96
+ execute: makeDeleteExecute(),
97
+ }),
98
+ foundry_snapshot_prune: tool({
99
+ description: 'Prune forensic snapshots older than `olderThanDays`. Requires {confirm: true} to actually remove.',
100
+ args: { olderThanDays: tool.schema.number(), confirm: tool.schema.boolean().optional() },
101
+ execute: makePruneExecute(),
102
+ }),
103
+ };
104
+ }