wyrm-mcp 7.2.0 → 7.2.2

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 (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
@@ -1,1113 +1,77 @@
1
- /**
2
- * Capture domain — ToolSpec contract v2 (v7 F3 T026, hot-path extraction).
3
- *
4
- * The WRITE FUNNEL (spec FR-4): wyrm_capture + everything it absorbs per the
5
- * T022 mode map — wyrm_remember (mode=direct), wyrm_auto_capture (extract),
6
- * wyrm_distill (session), wyrm_harvest (artifacts), wyrm_import_git/pr/rules
7
- * (import source=…), wyrm_spec_register (spec), wyrm_scaffold_save
8
- * (checklist). All moved VERBATIM from the index.ts dispatch switch +
9
- * buildAllTools(); the capture shim (src/handlers/shims.ts) keeps resolving
10
- * each mode onto THESE names, and the registry is checked before the switch,
11
- * so the shim table needs zero changes. ALIASES ROUTE TO THE SAME HANDLER
12
- * CODE PATHS — these specs ARE those paths, never reimplementations.
13
- *
14
- * The v7 F2 (T012) daemonOr canonical write seams moved with their handlers
15
- * unchanged (artifact_add ×4, quest_add, truth_set — source-locked by
16
- * tests/daemon-writer.test.ts, repointed here in the same commit).
17
- *
18
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
19
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
20
- */
21
- import { join as pathJoin } from 'path';
22
- import { TOOL_ANNOTATIONS } from '../tool-annotations.js';
23
- import { renderResult, withGlyph } from '../render.js';
24
- import { ValidationError, asEnum } from '../validate.js';
25
- import { daemonOr } from '../daemon-writer.js';
26
- import { classifyCapture } from '../capture.js';
27
- import { extractCandidates, candidateToArtifact, escapeLikePattern } from '../auto-capture.js';
28
- import { parseTrace, segmentsToText } from '../trace-harvest.js';
29
- import { getActor, runWithActor } from './boundary.js';
30
- import { harvestProjects } from '../harvest.js';
31
- import { readFileSync, readdirSync, statSync } from 'fs';
32
- import { join as pathJoin2 } from 'path';
33
- import { homedir } from 'os';
34
- import { readSpecDir, specTaskSignature } from '../spec-kit.js';
35
- export const captureToolSpecs = [
36
- {
37
- name: "wyrm_capture",
38
- description: "Use to save anything worth keeping - the single write funnel. Auto-classifies notes, decisions, and lessons into quest, ground truth, or memory; risky writes queue for wyrm_review. mode=direct (store a proven pattern or lesson learned so future agents reuse it), extract (pull memories from a conversation transcript or freeform text), trace (harvest a session JSONL / tool-call log into run-tagged review candidates), session (close out a work session into reusable knowledge), artifacts (harvest a repo's READMEs and recent commits into durable facts), import source=git|pr|rules, spec (turn a tasks.md into tracked work items), checklist. Keywords: remember this, jot down, write down, future reference.",
39
- // v7 F3 (T022): the WRITE FUNNEL (spec FR-4) — wyrm_capture absorbs
40
- // remember/auto_capture/distill/harvest/import_*/spec_register/
41
- // scaffold_save via mode=…; resolveShimCall (src/handlers/shims.ts)
42
- // translates each mode onto the original dispatcher case (thin shim,
43
- // same code path). The 6.x modes auto|quest|truth|memory keep their
44
- // classification-override semantics on capture's own case; 'classify'
45
- // is the spec name for 'auto'. mode=trace|debrief arrive with wyrm_run
46
- // (T027) and are not advertised before they exist. `content` is only
47
- // required by the classify path, so the schema-level required is
48
- // relaxed (additive: every previously valid call stays valid);
49
- // per-mode requirements are enforced by the underlying cases.
50
- inputSchema: {
51
- type: "object",
52
- properties: {
53
- // v7 F3 (T026): property prose compressed to fund the hot-path
54
- // outputSchemas under the 8K default-surface pin (T022/T025 trade).
55
- content: { type: "string", description: "classify/rules" },
56
- project_id: { type: "number", description: "classify/import/spec" },
57
- tags: { type: "array", items: { type: "string" } },
58
- mode: { type: "string", enum: ["auto", "quest", "truth", "memory", "classify", "direct", "extract", "trace", "session", "artifacts", "import", "spec", "checklist"], description: "quest|truth|memory force the type" },
59
- source: { type: "string", enum: ["git", "pr", "rules"], description: "import" },
60
- projectPath: { type: "string", description: "non-classify" },
61
- kind: { type: "string", description: "direct" },
62
- problem: { type: "string", description: "direct" },
63
- validatedFix: { type: "string", description: "direct" },
64
- // T027 budget trim: whyItWorked/outcome (direct-mode niche) still
65
- // pass through undeclared — the T022 lean-union rule.
66
- text: { type: "string", description: "extract" },
67
- sessionId: { type: "string", description: "session" },
68
- candidates: { type: "array", items: { type: "object" }, description: "session" },
69
- dryRun: { type: "boolean", description: "artifacts" },
70
- commits: { type: "array", items: { type: "object" }, description: "import git" },
71
- title: { type: "string", description: "import pr" },
72
- body: { type: "string", description: "import pr" },
73
- specDir: { type: "string", description: "spec" },
74
- problemType: { type: "string", description: "checklist" },
75
- whenToUse: { type: "string", description: "checklist" },
76
- steps: { type: "array", items: { type: "string" }, description: "checklist" },
77
- },
78
- required: [],
79
- },
80
- outputSchema: {
81
- type: "object",
82
- properties: {
83
- status: { type: "string", enum: ["captured", "queued_for_review"] },
84
- type: { type: "string" },
85
- subtype: { type: "string" },
86
- confidence: { type: "number" },
87
- reasoning: { type: "string" },
88
- id: { type: "integer" },
89
- ref: { type: "string" },
90
- needs_review: { type: "boolean" },
91
- reason: { type: "string" },
92
- artifact_id: { type: "integer" },
93
- conflicts_with: { type: "array" },
94
- advisory_conflicts: { type: "array" },
95
- },
96
- required: ["status"],
97
- },
98
- annotations: TOOL_ANNOTATIONS.wyrm_capture,
99
- aliases: [],
100
- handler: async (args, { store, raw, memory, truths, cache }) => {
101
- const { content: capContent, project_id: capProjId, tags: capTags, mode: capMode } = args;
102
- // Determine classification
103
- let classified = classifyCapture(capContent);
104
- if (capMode && capMode !== 'auto') {
105
- const subtypeMap = { quest: 'quest', truth: 'decision', memory: 'pattern' };
106
- classified = {
107
- type: capMode,
108
- subtype: subtypeMap[capMode] ?? capMode,
109
- confidence: 100,
110
- reasoning: `Mode override: ${capMode}`,
111
- };
112
- }
113
- const { type: capType, subtype: capSubtype, confidence: capConf, reasoning: capReasoning } = classified;
114
- const effectiveProjId = capProjId ?? null;
115
- let storedId = 0;
116
- let typeShort = '';
117
- let needsReview = false;
118
- let advisoryConflicts;
119
- if (capType === 'quest') {
120
- if (effectiveProjId === null) {
121
- return { content: [{ type: "text", text: `Cannot capture quest without project_id. Please provide project_id.` }], isError: true };
122
- }
123
- // v7 F2 (T012): canonical write seam — capture's quest branch.
124
- // Classification (classifyCapture) is deterministic and stays
125
- // CLIENT-side; only the resulting write hops to the daemon.
126
- const quest = await daemonOr('quest_add', effectiveProjId, { title: capContent.slice(0, 200), description: '', priority: 'medium', tags: capTags?.join(',') }, () => store.addQuest(effectiveProjId, capContent.slice(0, 200), '', 'medium', capTags?.join(',')));
127
- storedId = quest.id;
128
- typeShort = 'quest';
129
- cache.invalidate('wyrm_all_quests');
130
- cache.invalidate('wyrm_stats');
131
- }
132
- else if (capType === 'truth') {
133
- if (effectiveProjId === null) {
134
- return { content: [{ type: "text", text: `Cannot capture truth without project_id. Please provide project_id.` }], isError: true };
135
- }
136
- // Structural contradiction check: same project + same category
137
- if (capConf >= 80) {
138
- const capCategory = capSubtype ?? 'other';
139
- const existingTruths = truths.getCurrent(effectiveProjId);
140
- const sameCategory = existingTruths.filter(t => t.category === capCategory);
141
- if (sameCategory.length > 0) {
142
- // Route to review queue — possible supersession
143
- // v7 F2 (T012): canonical write seam (conflict-check stays a local READ).
144
- const conflictInput = {
145
- kind: 'pattern',
146
- problem: `Potential conflict with ${sameCategory.length} existing truth(s) in category "${capCategory}"`,
147
- validatedFix: capContent,
148
- whyItWorked: 'Pending review — possible supersession',
149
- tags: capTags ?? [],
150
- confidence: capConf / 100,
151
- needsReview: 1,
152
- };
153
- const conflictArtifact = await daemonOr('artifact_add', effectiveProjId, conflictInput, () => memory.add(effectiveProjId, conflictInput));
154
- // Machine-first outcome: the body IS the 6.x JSON payload; the
155
- // text channel is its serialization (templateless renderResult).
156
- return renderResult({
157
- status: 'queued_for_review',
158
- reason: 'conflict_check',
159
- artifact_id: conflictArtifact.id,
160
- conflicts_with: sameCategory.map(t => ({ id: t.id, content: t.value.slice(0, 80) })),
161
- });
162
- }
163
- }
164
- if (capMode !== 'truth' && capConf < 100) {
165
- // Store as artifact pending review
166
- // v7 F2 (T012): canonical write seam.
167
- const reviewInput = {
168
- kind: 'pattern',
169
- problem: capContent,
170
- validatedFix: '',
171
- whyItWorked: '',
172
- tags: capTags ?? [],
173
- confidence: capConf / 100,
174
- needsReview: 1,
175
- };
176
- const artifact = await daemonOr('artifact_add', effectiveProjId, reviewInput, () => memory.add(effectiveProjId, reviewInput));
177
- storedId = artifact.id;
178
- typeShort = 'mem';
179
- needsReview = true;
180
- }
181
- else {
182
- // v7 F2 (T012): canonical write seam — capture's truth branch.
183
- const capTruthInput = {
184
- category: 'decision',
185
- key: capContent.slice(0, 60),
186
- value: capContent,
187
- };
188
- const truth = await daemonOr('truth_set', effectiveProjId, capTruthInput, () => truths.set(effectiveProjId, capTruthInput));
189
- storedId = truth.id;
190
- typeShort = 'truth';
191
- cache.invalidate('wyrm_truth_get');
192
- cache.invalidate('wyrm_context_build');
193
- }
194
- }
195
- else {
196
- // memory
197
- if (effectiveProjId === null) {
198
- return { content: [{ type: "text", text: `Cannot capture memory without project_id. Please provide project_id.` }], isError: true };
199
- }
200
- const shouldAutoCreate = capConf >= 75;
201
- // v7 F2 (T012): canonical write seam — capture's memory branch.
202
- const capMemInput = {
203
- kind: capSubtype,
204
- problem: capContent,
205
- validatedFix: '',
206
- whyItWorked: '',
207
- tags: capTags ?? [],
208
- confidence: capConf / 100,
209
- needsReview: shouldAutoCreate ? 0 : 1,
210
- };
211
- const artifact = await daemonOr('artifact_add', effectiveProjId, capMemInput, () => memory.add(effectiveProjId, capMemInput));
212
- storedId = artifact.id;
213
- typeShort = 'mem';
214
- needsReview = !shouldAutoCreate;
215
- // Advisory conflict check for heuristics (non-blocking)
216
- if (capSubtype === 'heuristic' && capConf >= 75 && effectiveProjId !== null) {
217
- try {
218
- const sanitizedCap = capContent.replace(/['"*]/g, ' ').trim().split(/\s+/).slice(0, 5).join(' ');
219
- const heuristicConflicts = raw().prepare(`
1
+ import{join as N}from"path";import{TOOL_ANNOTATIONS as E}from"../tool-annotations.js";import{renderResult as q,withGlyph as I}from"../render.js";import{ValidationError as U,asEnum as H}from"../validate.js";import{daemonOr as A}from"../daemon-writer.js";import{classifyCapture as B}from"../capture.js";import{extractCandidates as M,candidateToArtifact as L,escapeLikePattern as D}from"../auto-capture.js";import{parseTrace as K,segmentsToText as J}from"../trace-harvest.js";import{getActor as Y,runWithActor as Q}from"./boundary.js";import{harvestProjects as X}from"../harvest.js";import{readFileSync as W,readdirSync as G,statSync as R}from"fs";import{join as O}from"path";import{homedir as V}from"os";import{readSpecDir as z,specTaskSignature as Z}from"../spec-kit.js";const ye=[{name:"wyrm_capture",description:"Use to save anything worth keeping - the single write funnel. Auto-classifies notes, decisions, and lessons into quest, ground truth, or memory; risky writes queue for wyrm_review. mode=direct (store a proven pattern or lesson learned so future agents reuse it), extract (pull memories from a conversation transcript or freeform text), trace (harvest a session JSONL / tool-call log into run-tagged review candidates), session (close out a work session into reusable knowledge), artifacts (harvest a repo's READMEs and recent commits into durable facts), import source=git|pr|rules, spec (turn a tasks.md into tracked work items), checklist. Keywords: remember this, jot down, write down, future reference.",inputSchema:{type:"object",properties:{content:{type:"string",description:"classify/rules"},project_id:{type:"number",description:"classify/import/spec"},tags:{type:"array",items:{type:"string"}},mode:{type:"string",enum:["auto","quest","truth","memory","classify","direct","extract","trace","session","artifacts","import","spec","checklist"],description:"quest|truth|memory force the type"},source:{type:"string",enum:["git","pr","rules"],description:"import"},projectPath:{type:"string",description:"non-classify"},kind:{type:"string",description:"direct"},problem:{type:"string",description:"direct"},validatedFix:{type:"string",description:"direct"},text:{type:"string",description:"extract"},sessionId:{type:"string",description:"session"},candidates:{type:"array",items:{type:"object"},description:"session"},dryRun:{type:"boolean",description:"artifacts"},commits:{type:"array",items:{type:"object"},description:"import git"},title:{type:"string",description:"import pr"},body:{type:"string",description:"import pr"},specDir:{type:"string",description:"spec"},problemType:{type:"string",description:"checklist"},whenToUse:{type:"string",description:"checklist"},steps:{type:"array",items:{type:"string"},description:"checklist"}},required:[]},outputSchema:{type:"object",properties:{status:{type:"string",enum:["captured","queued_for_review"]},type:{type:"string"},subtype:{type:"string"},confidence:{type:"number"},reasoning:{type:"string"},id:{type:"integer"},ref:{type:"string"},needs_review:{type:"boolean"},reason:{type:"string"},artifact_id:{type:"integer"},conflicts_with:{type:"array"},advisory_conflicts:{type:"array"}},required:["status"]},annotations:E.wyrm_capture,aliases:[],handler:async(v,{store:w,raw:j,memory:m,truths:_,cache:g})=>{const{content:c,project_id:f,tags:i,mode:u}=v;let a=B(c);u&&u!=="auto"&&(a={type:u,subtype:{quest:"quest",truth:"decision",memory:"pattern"}[u]??u,confidence:100,reasoning:`Mode override: ${u}`});const{type:d,subtype:p,confidence:s,reasoning:b}=a,e=f??null;let t=0,l="",o=!1,h;if(d==="quest"){if(e===null)return{content:[{type:"text",text:"Cannot capture quest without project_id. Please provide project_id."}],isError:!0};t=(await A("quest_add",e,{title:c.slice(0,200),description:"",priority:"medium",tags:i?.join(",")},()=>w.addQuest(e,c.slice(0,200),"","medium",i?.join(",")))).id,l="quest",g.invalidate("wyrm_all_quests"),g.invalidate("wyrm_stats")}else if(d==="truth"){if(e===null)return{content:[{type:"text",text:"Cannot capture truth without project_id. Please provide project_id."}],isError:!0};if(s>=80){const n=p??"other",$=_.getCurrent(e).filter(k=>k.category===n);if($.length>0){const k={kind:"pattern",problem:`Potential conflict with ${$.length} existing truth(s) in category "${n}"`,validatedFix:c,whyItWorked:"Pending review \u2014 possible supersession",tags:i??[],confidence:s/100,needsReview:1},S=await A("artifact_add",e,k,()=>m.add(e,k));return q({status:"queued_for_review",reason:"conflict_check",artifact_id:S.id,conflicts_with:$.map(y=>({id:y.id,content:y.value.slice(0,80)}))})}}if(u!=="truth"&&s<100){const n={kind:"pattern",problem:c,validatedFix:"",whyItWorked:"",tags:i??[],confidence:s/100,needsReview:1};t=(await A("artifact_add",e,n,()=>m.add(e,n))).id,l="mem",o=!0}else{const n={category:"decision",key:c.slice(0,60),value:c};t=(await A("truth_set",e,n,()=>_.set(e,n))).id,l="truth",g.invalidate("wyrm_truth_get"),g.invalidate("wyrm_context_build")}}else{if(e===null)return{content:[{type:"text",text:"Cannot capture memory without project_id. Please provide project_id."}],isError:!0};const n=s>=75,x={kind:p,problem:c,validatedFix:"",whyItWorked:"",tags:i??[],confidence:s/100,needsReview:n?0:1};if(t=(await A("artifact_add",e,x,()=>m.add(e,x))).id,l="mem",o=!n,p==="heuristic"&&s>=75&&e!==null)try{const k=c.replace(/['"*]/g," ").trim().split(/\s+/).slice(0,5).join(" "),S=j().prepare(`
220
2
  SELECT a.id, a.problem FROM memory_artifacts a
221
3
  JOIN memory_artifacts_fts fts ON a.id = fts.rowid
222
4
  WHERE fts MATCH ? AND a.project_id = ? AND a.kind = 'heuristic' AND a.needs_review = 0
223
5
  LIMIT 3
224
- `).all(sanitizedCap, effectiveProjId);
225
- if (heuristicConflicts.length > 0) {
226
- advisoryConflicts = heuristicConflicts.map(h => ({ id: h.id, content: h.problem.slice(0, 80) }));
227
- }
228
- }
229
- catch {
230
- // FTS may fail on short queries — advisory only, ignore
231
- }
232
- }
233
- }
234
- const body = {
235
- status: 'captured',
236
- type: capType,
237
- subtype: capSubtype,
238
- confidence: capConf,
239
- reasoning: capReasoning,
240
- id: storedId,
241
- ref: `${typeShort}:${storedId}`,
242
- needs_review: needsReview,
243
- ...(advisoryConflicts && advisoryConflicts.length > 0 ? { advisory_conflicts: advisoryConflicts } : {}),
244
- };
245
- return renderResult(body, (b, g) => {
246
- let text = `Captured as ${b.type}: ${b.subtype}\nConfidence: ${b.confidence}% | ${b.reasoning}\nID: ${b.ref}`;
247
- if (b.needs_review)
248
- text += '\n' + withGlyph(g.warn, 'Stored for review -- run `wyrm_review` to activate');
249
- const adv = b.advisory_conflicts;
250
- if (adv && adv.length > 0) {
251
- text += '\n\n' + withGlyph(g.warn, `Advisory: ${adv.length} similar heuristic(s) found:`) + '\n' +
252
- adv.map(c => ` ${g.bullet} [${c.id}] ${c.content}`).join('\n');
253
- }
254
- return text;
255
- });
256
- },
257
- },
258
- {
259
- name: "wyrm_remember",
260
- description: "Store a distilled knowledge artifact — proven patterns, lessons learned, anti-patterns, and reasoning traces. These are recalled automatically by wyrm_context_build to help AI models apply past knowledge to new tasks. Example: wyrm_remember({ projectPath: '/home/user/api', kind: 'anti_pattern', problem: 'Jest suite hung after adding the SSE server', validatedFix: 'close() the server in afterAll — its keep-alive timer held the event loop open', whyItWorked: 'open handles block jest exit', outcome: 'positive', tags: ['jest', 'sse'] })",
261
- inputSchema: {
262
- type: "object",
263
- properties: {
264
- projectPath: { type: "string", description: "Project this knowledge belongs to" },
265
- kind: {
266
- type: "string",
267
- enum: ["reasoning_trace", "lesson", "pattern", "anti_pattern", "heuristic"],
268
- description: "reasoning_trace: a solved problem with steps; lesson: a general insight; pattern: a proven approach; anti_pattern: what NOT to do; heuristic: a rule of thumb",
269
- },
270
- problem: { type: "string", description: "What was being solved or observed (be specific and searchable)" },
271
- constraints: { type: "string", description: "What conditions or constraints applied" },
272
- validatedFix: { type: "string", description: "What actually worked — the validated solution or approach" },
273
- whyItWorked: { type: "string", description: "The insight behind WHY this worked (key for transfer learning)" },
274
- outcome: { type: "string", enum: ["positive", "negative", "neutral"], description: "Was this approach successful?" },
275
- tags: { type: "array", items: { type: "string" }, description: "Searchable tags (e.g. ['auth', 'rate-limit', 'typescript'])" },
276
- confidence: { type: "number", description: "Confidence level 0.0–1.0 (default: 1.0)" },
277
- sourceSessionId: { type: "number", description: "Session ID where this was discovered (optional)" },
278
- },
279
- required: ["projectPath", "kind", "problem"],
280
- },
281
- outputSchema: {
282
- type: "object",
283
- properties: {
284
- id: { type: "integer" },
285
- kind: { type: "string", enum: ["reasoning_trace", "lesson", "pattern", "anti_pattern", "heuristic"] },
286
- problem: { type: "string" },
287
- validated_fix: { type: ["string", "null"] },
288
- why_it_worked: { type: ["string", "null"] },
289
- confidence: { type: "number" },
290
- tags: { type: ["string", "null"] },
291
- },
292
- required: ["id", "kind", "problem", "validated_fix", "why_it_worked", "confidence", "tags"],
293
- },
294
- annotations: TOOL_ANNOTATIONS.wyrm_remember,
295
- aliases: [],
296
- handler: async (args, { store, memory }) => {
297
- const { projectPath: rPath, kind: rKind, problem: rProblem, constraints: rConst, validatedFix: rFix, whyItWorked: rWhy, outcome: rOutcome, tags: rTags, confidence: rConf, sourceSessionId: rSessId } = args;
298
- // Closed enum, mirrors the memory_artifacts.kind schema CHECK:
299
- // boundary-enforced (the MCP SDK treats inputSchema as advisory) so a
300
- // typo'd kind is a clean structured client error, never a daemon-side
301
- // SQLITE_CONSTRAINT round trip.
302
- const rSafeKind = asEnum('kind', rKind, ['reasoning_trace', 'lesson', 'pattern', 'anti_pattern', 'heuristic']);
303
- if (rSafeKind === undefined)
304
- throw new ValidationError('kind', 'is required');
305
- const rProject = store.getProject(rPath);
306
- if (!rProject)
307
- return { content: [{ type: "text", text: `Project not found: ${rPath}` }], isError: true };
308
- // v7 F2 (T012): canonical write seam (capture family). Project
309
- // resolution above is a READ and stays local; only the write hops.
310
- const rInput = {
311
- kind: rSafeKind,
312
- problem: rProblem,
313
- constraints: rConst,
314
- validatedFix: rFix,
315
- whyItWorked: rWhy,
316
- outcome: rOutcome,
317
- tags: rTags,
318
- confidence: rConf,
319
- sourceSessionId: rSessId,
320
- };
321
- const artifact = await daemonOr('artifact_add', rProject.id, rInput, () => memory.add(rProject.id, rInput));
322
- const body = {
323
- id: artifact.id,
324
- kind: artifact.kind,
325
- problem: artifact.problem,
326
- validated_fix: artifact.validated_fix ?? null,
327
- why_it_worked: artifact.why_it_worked ?? null,
328
- confidence: artifact.confidence,
329
- tags: artifact.tags ?? null,
330
- };
331
- return renderResult(body, (b, g) => {
332
- let text = withGlyph(g.brand, '**Memory Stored**') + '\n\n';
333
- text += `${g.bullet} **Kind:** ${b.kind}\n`;
334
- text += `${g.bullet} **ID:** ${b.id}\n`;
335
- text += `${g.bullet} **Problem:** ${b.problem}\n`;
336
- if (b.validated_fix)
337
- text += `${g.bullet} **Solution:** ${b.validated_fix}\n`;
338
- if (b.why_it_worked)
339
- text += `${g.bullet} **Why it worked:** ${b.why_it_worked}\n`;
340
- text += `${g.bullet} **Confidence:** ${(b.confidence * 100).toFixed(0)}%\n`;
341
- if (b.tags)
342
- text += `${g.bullet} **Tags:** ${b.tags}\n`;
343
- text += `\n_Use \`wyrm_recall\` to retrieve similar memories, or \`wyrm_context_build\` to assemble a task brief._`;
344
- return text;
345
- });
346
- },
347
- },
348
- {
349
- name: "wyrm_auto_capture",
350
- description: "Auto-extract durable memories from freeform text (a session transcript, notes, a chat) into the REVIEW QUEUE. A LOCAL extractor — a configurable Ollama model (WYRM_EXTRACT_MODEL / the `model` arg) if set, else a deterministic heuristic — pulls out truths, failures, decisions, patterns, and lessons as needs_review candidates; approve/reject with wyrm_review (nothing auto-trusted). Closes the 'just talk, it remembers' gap without a cloud LLM. Deduped + idempotent. Example: wyrm_auto_capture({ projectPath: '/home/user/api', text: 'Deploy failed twice because wrangler.toml pointed at the old KV id; switched to the new binding and decided to pin binding names in CI.' })",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- projectPath: { type: "string", description: "Project to file the extracted candidates under" },
355
- text: { type: "string", description: "Freeform text to extract durable memories from" },
356
- model: { type: "string", description: "Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic extraction" },
357
- },
358
- required: ["projectPath", "text"],
359
- },
360
- outputSchema: {
361
- type: "object",
362
- properties: {
363
- method: { type: "string" },
364
- model: { type: ["string", "null"] },
365
- added: { type: "integer" },
366
- skipped: { type: "integer" },
367
- candidates: {
368
- type: "array",
369
- items: {
370
- type: "object",
371
- properties: { kind: { type: "string" }, text: { type: "string" } },
372
- required: ["kind", "text"],
373
- },
374
- },
375
- },
376
- required: ["method", "model", "added", "skipped", "candidates"],
377
- },
378
- annotations: TOOL_ANNOTATIONS.wyrm_auto_capture,
379
- aliases: [],
380
- handler: async (args, { store, raw, memory, cache }) => {
381
- const { projectPath: acPath, text: acText, model: acModel } = args;
382
- const acProject = store.getProject(acPath);
383
- if (!acProject)
384
- return { content: [{ type: "text", text: `Project not found: ${acPath}` }], isError: true };
385
- if (!acText || acText.trim().length < 20)
386
- return { content: [{ type: "text", text: "Provide at least ~20 chars of text to extract from." }], isError: true };
387
- const { candidates, method, model: usedModel } = await extractCandidates(acText, { model: acModel });
388
- const rawDb = raw();
389
- let added = 0, skipped = 0;
390
- for (const c of candidates) {
391
- const a = candidateToArtifact(c);
392
- const sig = a.tags[a.tags.length - 1]; // the 'ax:' dedup signature
393
- // Security pass #1: sig is text-derived — %/_ must be LITERAL in the
394
- // dedup probe (escapeLikePattern + ESCAPE), or wildcard-bearing input
395
- // broad-matches other ax: tags and silently suppresses capture.
396
- if (rawDb.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(acProject.id, '%' + escapeLikePattern(sig) + '%')) {
397
- skipped++;
398
- continue;
399
- }
400
- memory.add(acProject.id, { kind: a.kind, problem: a.problem, tags: a.tags, confidence: a.confidence, needsReview: 1, createdBy: 'auto-extract' });
401
- added++;
402
- }
403
- if (added > 0)
404
- cache.invalidate();
405
- const body = {
406
- method,
407
- model: usedModel ?? null,
408
- added,
409
- skipped,
410
- candidates: candidates.slice(0, 8).map((c) => ({ kind: c.kind, text: c.text.slice(0, 80) })),
411
- };
412
- return renderResult(body, (b, g) => {
413
- const lines = b.candidates.map((c) => ` ${g.bullet} [${c.kind}] ${c.text}`).join('\n');
414
- return withGlyph(g.brand, `Auto-capture (${b.method}${b.model ? ` - ${b.model}` : ''}) -- ${b.added} candidate(s) -> review queue, ${b.skipped} already present.`) +
415
- `\n${lines}\n\nApprove/reject with wyrm_review.`;
416
- });
417
- },
418
- },
419
- {
420
- name: "wyrm_capture_trace",
421
- description: "Harvest a harness's working trace into run-tagged review-queue candidates — OFFLINE. Ingests a Claude Code session transcript (JSONL), a ~/.dragon/traces file/dir, or WYRM_TRACE_TOOL_CALLS event output. The SAME local extractor as auto-capture (Ollama WYRM_EXTRACT_MODEL if set, else deterministic — never a cloud LLM) pulls durable truths/failures/decisions/patterns/lessons; secrets (API keys, tokens, passwords) are REDACTED before anything is stored; candidates land needs_review=1 stamped with the run_id (arg or ambient envelope). Approve/reject with wyrm_review; deduped + idempotent. Pass `trace` (inline text) OR `path` (a trace file or the ~/.dragon/traces dir). Reached via wyrm_capture({ mode: 'trace' }).",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- projectPath: { type: "string", description: "Project to file the candidates under" },
426
- trace: { type: "string", description: "Inline trace text (JSONL transcript / dragon records / tool-call events)" },
427
- path: { type: "string", description: "Path to a trace file, or the ~/.dragon/traces directory (newest file is read); omit ~ for absolute" },
428
- format: { type: "string", enum: ["auto", "claude-jsonl", "dragon", "tool-calls"], description: "Trace format (default auto-detect)" },
429
- run_id: { type: "string", description: "Tag candidates with this run (default: ambient envelope run_id)" },
430
- model: { type: "string", description: "Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic" },
431
- },
432
- required: ["projectPath"],
433
- },
434
- outputSchema: {
435
- type: "object",
436
- properties: {
437
- method: { type: "string" },
438
- model: { type: ["string", "null"] },
439
- format: { type: "string" },
440
- source: { type: "string" },
441
- run_id: { type: ["string", "null"] },
442
- segments: { type: "integer" },
443
- added: { type: "integer" },
444
- skipped: { type: "integer" },
445
- candidates: {
446
- type: "array",
447
- items: {
448
- type: "object",
449
- properties: { kind: { type: "string" }, text: { type: "string" } },
450
- required: ["kind", "text"],
451
- },
452
- },
453
- },
454
- required: ["method", "model", "format", "source", "run_id", "segments", "added", "skipped", "candidates"],
455
- },
456
- annotations: TOOL_ANNOTATIONS.wyrm_capture_trace,
457
- aliases: [],
458
- handler: async (args, { store, raw, memory, cache }) => {
459
- const { projectPath: trPath, trace: trInline, path: trFilePath, format: trFormat, run_id: trRunArg, model: trModel } = args;
460
- const trProject = store.getProject(trPath);
461
- if (!trProject)
462
- return { content: [{ type: "text", text: `Project not found: ${trPath}` }], isError: true };
463
- // Run attribution (F2): explicit arg wins, else the ambient envelope.
464
- const ambient = getActor();
465
- const runId = (typeof trRunArg === 'string' && trRunArg.trim()) ? trRunArg.trim().slice(0, 64) : ambient.run_id;
466
- // Resolve the trace BYTES: inline `trace` wins; else read `path`. A
467
- // directory (the ~/.dragon/traces case) reads its NEWEST regular file.
468
- // Article VII: path is operator-supplied; we only READ, never write/escape.
469
- let rawTrace = '';
470
- let source = 'inline';
471
- if (typeof trInline === 'string' && trInline.trim()) {
472
- rawTrace = trInline;
473
- source = 'inline';
474
- }
475
- else if (typeof trFilePath === 'string' && trFilePath.trim()) {
476
- // Expand a leading ~ to the home dir (the documented ~/.dragon/traces shape).
477
- let resolved = trFilePath.trim();
478
- if (resolved === '~' || resolved.startsWith('~/'))
479
- resolved = pathJoin2(homedir(), resolved.slice(1));
480
- try {
481
- const st = statSync(resolved);
482
- if (st.isDirectory()) {
483
- const files = readdirSync(resolved)
484
- .map((f) => pathJoin2(resolved, f))
485
- .filter((p) => { try {
486
- return statSync(p).isFile();
487
- }
488
- catch {
489
- return false;
490
- } })
491
- .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
492
- if (files.length === 0)
493
- return { content: [{ type: "text", text: `No trace files in directory: ${resolved}` }], isError: true };
494
- rawTrace = readFileSync(files[0], 'utf-8');
495
- source = files[0];
496
- }
497
- else {
498
- rawTrace = readFileSync(resolved, 'utf-8');
499
- source = resolved;
500
- }
501
- }
502
- catch (e) {
503
- return { content: [{ type: "text", text: `Could not read trace path "${resolved}": ${e.message}` }], isError: true };
504
- }
505
- }
506
- else {
507
- return { content: [{ type: "text", text: "Provide a `trace` (inline text) or a `path` (trace file or ~/.dragon/traces dir)." }], isError: true };
508
- }
509
- const { segments, format: usedFormat } = parseTrace(rawTrace, trFormat ?? 'auto');
510
- const text = segmentsToText(segments);
511
- if (text.trim().length < 20) {
512
- const emptyBody = {
513
- method: 'deterministic', model: null, format: usedFormat, source,
514
- run_id: runId, segments: segments.length, added: 0, skipped: 0, candidates: [],
515
- };
516
- return renderResult(emptyBody, (b, g) => withGlyph(g.brand, `Trace harvest (${b.format}) -- no durable content extracted from ${b.segments} segment(s).`));
517
- }
518
- const { candidates, method, model: usedModel } = await extractCandidates(text, { model: trModel });
519
- const rawDb = raw();
520
- let added = 0, skipped = 0;
521
- for (const c of candidates) {
522
- const a = candidateToArtifact(c);
523
- const sig = a.tags[a.tags.length - 1]; // the 'ax:' dedup signature
524
- // Security pass #1: sig is trace-text-derived — %/_ must be LITERAL in
525
- // the dedup probe (escapeLikePattern + ESCAPE), or a wildcard-bearing
526
- // segment broad-matches other ax: tags and silently suppresses capture.
527
- if (rawDb.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(trProject.id, '%' + escapeLikePattern(sig) + '%')) {
528
- skipped++;
529
- continue;
530
- }
531
- // RUN-SCOPED candidate (T040): stamp the ambient/explicit run on the
532
- // row (memory.add reads the envelope) and tag run:<id> + 'trace' so the
533
- // review queue slices per run, mirroring the wyrm_run debrief shape.
534
- const tags = runId ? [...a.tags, 'trace', `run:${runId}`] : [...a.tags, 'trace'];
535
- runWithActor({ agent_id: ambient.agent_id, run_id: runId, source: runId ? 'param' : ambient.source }, () => memory.add(trProject.id, { kind: a.kind, problem: a.problem, tags, confidence: a.confidence, needsReview: 1, createdBy: 'trace-harvest' }));
536
- added++;
537
- }
538
- if (added > 0)
539
- cache.invalidate();
540
- const body = {
541
- method,
542
- model: usedModel ?? null,
543
- format: usedFormat,
544
- source,
545
- run_id: runId,
546
- segments: segments.length,
547
- added,
548
- skipped,
549
- candidates: candidates.slice(0, 8).map((c) => ({ kind: c.kind, text: c.text.slice(0, 80) })),
550
- };
551
- return renderResult(body, (b, g) => {
552
- const lines = b.candidates.map((c) => ` ${g.bullet} [${c.kind}] ${c.text}`).join('\n');
553
- return withGlyph(g.brand, `Trace harvest (${b.format}, ${b.method}${b.model ? ` - ${b.model}` : ''}) -- ${b.segments} segment(s) -> ${b.added} candidate(s) to review queue, ${b.skipped} already present${b.run_id ? ` [run:${b.run_id}]` : ''}.`) +
554
- (lines ? `\n${lines}` : '') + `\n\nApprove/reject with wyrm_review.`;
555
- });
556
- },
557
- },
558
- {
559
- name: "wyrm_distill",
560
- description: "Distill a work session into candidate memory artifacts for review. Parses the session's objectives, decisions, and outcomes into structured artifacts (lessons, patterns, anti-patterns) and queues them for approval via wyrm_review. Use at session end to extract institutional knowledge.",
561
- inputSchema: {
562
- type: "object",
563
- properties: {
564
- projectPath: { type: "string", description: "Project root path" },
565
- sessionId: { type: "string", description: "Session ID to distill" },
566
- candidates: {
567
- type: "array",
568
- description: "Pre-parsed candidate artifacts to store for review",
569
- items: {
570
- type: "object",
571
- properties: {
572
- kind: { type: "string", enum: ["lesson", "pattern", "anti_pattern", "heuristic", "reasoning_trace"] },
573
- title: { type: "string" },
574
- content: { type: "string" },
575
- tags: { type: "array", items: { type: "string" } },
576
- confidence: { type: "number" },
577
- },
578
- required: ["kind", "title", "content"],
579
- },
580
- },
581
- },
582
- required: ["projectPath", "candidates"],
583
- },
584
- outputSchema: {
585
- type: "object",
586
- properties: {
587
- count: { type: "integer" },
588
- artifacts: {
589
- type: "array",
590
- items: {
591
- type: "object",
592
- properties: {
593
- id: { type: "integer" },
594
- kind: { type: "string" },
595
- title: { type: "string" },
596
- },
597
- required: ["id", "kind", "title"],
598
- },
599
- },
600
- },
601
- required: ["count", "artifacts"],
602
- },
603
- annotations: TOOL_ANNOTATIONS.wyrm_distill,
604
- aliases: [],
605
- handler: (args, { store, memory }) => {
606
- const { projectPath: dPath, candidates } = args;
607
- const dProject = store.getProject(dPath);
608
- if (!dProject)
609
- return { content: [{ type: "text", text: `Project not found: ${dPath}` }], isError: true };
610
- if (!candidates || candidates.length === 0) {
611
- return { content: [{ type: "text", text: `**Distill**\n\nNo candidates provided. Provide an array of candidate artifacts to distill.` }], isError: true };
612
- }
613
- const artifacts = [];
614
- for (const c of candidates) {
615
- const artifact = memory.add(dProject.id, {
616
- kind: c.kind,
617
- problem: c.title,
618
- validatedFix: c.content,
619
- tags: c.tags ?? [],
620
- confidence: c.confidence ?? 0.7,
621
- needsReview: 1,
622
- });
623
- artifacts.push({ id: artifact.id, kind: c.kind, title: c.title });
624
- }
625
- const body = { count: artifacts.length, artifacts };
626
- return renderResult(body, (b, g) => {
627
- let text = withGlyph(g.brand, `**Distillation Complete** -- ${b.count} artifact${b.count !== 1 ? 's' : ''} queued for review`) + '\n\n';
628
- text += b.artifacts.map((a) => `${g.bullet} [#${a.id}] **${a.kind}**: ${a.title}`).join('\n');
629
- text += `\n\nUse \`wyrm_review\` with each ID to approve or reject.`;
630
- return text;
631
- });
632
- },
633
- },
634
- {
635
- name: "wyrm_harvest",
636
- description: "Auto-populate memory from artifacts you already produce. Walks a project (or ALL registered projects) and harvests durable facts from its docs (README/CLAUDE/AGENTS/ARCHITECTURE) plus recent commit subjects (git log) into the REVIEW QUEUE — approve/reject with wyrm_review, nothing is auto-trusted. Idempotent (re-runs skip what's already harvested). Use dryRun to preview. The fix for a thin corpus.",
637
- inputSchema: {
638
- type: "object",
639
- properties: {
640
- projectPath: { type: "string", description: "Harvest one project; omit to harvest ALL registered projects" },
641
- dryRun: { type: "boolean", description: "Preview what would be harvested without writing anything" },
642
- gitLimit: { type: "number", description: "How many recent commits to scan per project (default 30, max 200)" },
643
- includeCode: { type: "boolean", description: "Also harvest CODE: tech-stack facts from manifests (package.json/Cargo.toml/…) + TODO/FIXME markers into the review queue, AND index code symbols (functions/classes/types) for search. Heavier — scope to one project for the full code pass." },
644
- },
645
- },
646
- outputSchema: {
647
- type: "object",
648
- properties: {
649
- dry_run: { type: "boolean" },
650
- projects_scanned: { type: "integer" },
651
- added: { type: "integer" },
652
- skipped: { type: "integer" },
653
- top: {
654
- type: "array",
655
- items: {
656
- type: "object",
657
- properties: {
658
- project: { type: "string" },
659
- added: { type: "integer" },
660
- skipped: { type: "integer" },
661
- },
662
- required: ["project", "added", "skipped"],
663
- },
664
- },
665
- sample: { type: "array", items: { type: "string" } },
666
- code_symbols: {
667
- type: ["object", "null"],
668
- properties: {
669
- symbols: { type: "integer" },
670
- projects: { type: "integer" },
671
- files: { type: "integer" },
672
- },
673
- required: ["symbols", "projects", "files"],
674
- },
675
- },
676
- required: ["dry_run", "projects_scanned", "added", "skipped", "top", "sample", "code_symbols"],
677
- },
678
- annotations: TOOL_ANNOTATIONS.wyrm_harvest,
679
- aliases: [],
680
- handler: (args, { store, raw, memory, symbols, cache }) => {
681
- const { projectPath, dryRun, gitLimit, includeCode } = args;
682
- const rawDb = raw();
683
- const deps = {
684
- // Security pass #1: harvest sigs are doc/git-text-derived — same
685
- // LIKE-escape rule as the ax: probe above.
686
- existsBySig: (pid, sig) => !!rawDb.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(pid, '%' + escapeLikePattern(sig) + '%'),
687
- addCandidate: (pid, item) => {
688
- memory.add(pid, { kind: item.kind, problem: item.text, tags: [...item.tags, item.sig], confidence: item.confidence, needsReview: 1, createdBy: 'harvest' });
689
- },
690
- };
691
- let projects;
692
- if (projectPath) {
693
- const p = store.getProject(projectPath);
694
- if (!p)
695
- return { content: [{ type: "text", text: "Project not found" }], isError: true };
696
- projects = [{ id: p.id, name: p.name, path: p.path }];
697
- }
698
- else {
699
- projects = store.getAllProjects(500).map((p) => ({ id: p.id, name: p.name, path: p.path }));
700
- }
701
- const { reports, totalAdded, totalSkipped } = harvestProjects(deps, projects, { dryRun, gitLimit, includeCode });
702
- if (!dryRun && totalAdded > 0)
703
- cache.invalidate();
704
- // Code STRUCTURE → symbol index (searchable functions/classes/types), not the review queue.
705
- let codeSymbols = null;
706
- if (includeCode && !dryRun) {
707
- let files = 0, syms = 0, indexed = 0;
708
- for (const p of projects) {
709
- try {
710
- const r = symbols.indexProject(p.id, p.path);
711
- files += r.files;
712
- syms += r.symbols;
713
- indexed++;
714
- }
715
- catch { /* skip unreadable */ }
716
- }
717
- codeSymbols = { symbols: syms, projects: indexed, files };
718
- }
719
- const top = reports.filter((r) => r.added > 0).sort((a, b) => b.added - a.added).slice(0, 15)
720
- .map((r) => ({ project: r.project, added: r.added, skipped: r.skipped }));
721
- const sample = reports.flatMap((r) => r.sample ?? []).slice(0, 6);
722
- const body = {
723
- dry_run: !!dryRun,
724
- projects_scanned: projects.length,
725
- added: totalAdded,
726
- skipped: totalSkipped,
727
- top,
728
- sample,
729
- code_symbols: codeSymbols,
730
- };
731
- return renderResult(body, (b, g) => {
732
- const symbolNote = b.code_symbols
733
- ? `\nIndexed ${b.code_symbols.symbols} code symbols across ${b.code_symbols.projects} project(s) (${b.code_symbols.files} files) -> searchable via wyrm_symbol_search.`
734
- : '';
735
- const topLines = b.top.map((r) => ` +${String(r.added).padStart(3)} (skip ${r.skipped}) ${r.project}`).join('\n');
736
- const sampleLines = b.sample.map((s) => ` ${g.bullet} ${s}`).join('\n');
737
- return withGlyph(g.brand, `Harvest ${b.dry_run ? '(dry run) ' : ''}-- ${b.added} candidate(s) -> review queue across ${b.projects_scanned} project(s); ${b.skipped} already present.`) +
738
- `${symbolNote}\n${topLines}` +
739
- (sampleLines ? `\n\nSample:\n${sampleLines}` : '') +
740
- `\n\nApprove/reject with wyrm_review.`;
741
- });
742
- },
743
- },
744
- {
745
- name: "wyrm_import_git",
746
- description: "Import a list of commits into the Wyrm memory review queue. Useful for onboarding a project's history or capturing recent work.",
747
- inputSchema: {
748
- type: "object",
749
- properties: {
750
- project_id: { type: "number", description: "Project ID" },
751
- commits: {
752
- type: "array",
753
- description: "Array of commit objects",
754
- items: {
755
- type: "object",
756
- properties: {
757
- message: { type: "string" },
758
- diff_summary: { type: "string" },
759
- author: { type: "string" },
760
- date: { type: "string" },
761
- },
762
- required: ["message"],
763
- },
764
- },
765
- auto_approve: { type: "boolean", description: "Skip review queue and activate immediately (default: false)" },
766
- },
767
- required: ["project_id", "commits"],
768
- },
769
- outputSchema: {
770
- type: "object",
771
- properties: {
772
- captured: { type: "integer" },
773
- skipped: { type: "integer" },
774
- auto_approved: { type: "boolean" },
775
- },
776
- required: ["captured", "skipped", "auto_approved"],
777
- },
778
- annotations: TOOL_ANNOTATIONS.wyrm_import_git,
779
- aliases: [],
780
- handler: (args, { store, memory }) => {
781
- const { project_id: gitProjId, commits: gitCommits, auto_approve: gitAutoApprove } = args;
782
- const gitProject = store.getProjectById(gitProjId);
783
- if (!gitProject)
784
- return { content: [{ type: "text", text: `Project ID ${gitProjId} not found.` }], isError: true };
785
- let captured = 0, skippedCount = 0;
786
- for (const commit of gitCommits) {
787
- const msg = commit.message ?? '';
788
- if (/^Merge /i.test(msg)) {
789
- skippedCount++;
790
- continue;
791
- }
792
- if (/^(chore|bump|release|version)/i.test(msg)) {
793
- skippedCount++;
794
- continue;
795
- }
796
- let artifactKind = 'pattern';
797
- if (/^fix(\(.+\))?:/i.test(msg))
798
- artifactKind = 'lesson';
799
- else if (/^feat(\(.+\))?:/i.test(msg))
800
- artifactKind = 'pattern';
801
- else if (/^refactor(\(.+\))?:/i.test(msg))
802
- artifactKind = 'heuristic';
803
- const typePrefix = msg.split(':')[0] ?? 'commit';
804
- memory.add(gitProjId, {
805
- kind: artifactKind,
806
- problem: msg,
807
- validatedFix: commit.diff_summary ?? '',
808
- whyItWorked: `Committed by ${commit.author ?? 'unknown'} on ${commit.date ?? 'unknown'}`,
809
- tags: ['git', 'commit', typePrefix.toLowerCase()],
810
- confidence: 0.6,
811
- needsReview: gitAutoApprove ? 0 : 1,
812
- });
813
- captured++;
814
- }
815
- const body = { captured, skipped: skippedCount, auto_approved: !!gitAutoApprove };
816
- return renderResult(body, (b, g) => withGlyph(g.brand, '**Git Import Complete**') + '\n\n' +
817
- `${g.bullet} **Captured:** ${b.captured}\n` +
818
- `${g.bullet} **Skipped:** ${b.skipped} (merges/chores)\n` +
819
- `${g.bullet} **Review needed:** ${b.auto_approved ? 'No (auto-approved)' : `Yes -- run \`wyrm_review\` to activate ${b.captured} artifact${b.captured !== 1 ? 's' : ''}`}`);
820
- },
821
- },
822
- {
823
- name: "wyrm_import_pr",
824
- description: "Import a pull request (title, body, review comments) into the Wyrm memory review queue to extract patterns and heuristics.",
825
- inputSchema: {
826
- type: "object",
827
- properties: {
828
- project_id: { type: "number", description: "Project ID" },
829
- title: { type: "string", description: "PR title" },
830
- body: { type: "string", description: "PR description / body" },
831
- review_comments: { type: "array", items: { type: "string" }, description: "Review comment strings" },
832
- auto_approve: { type: "boolean", description: "Skip review queue (default: false)" },
833
- },
834
- required: ["project_id", "title", "body"],
835
- },
836
- outputSchema: {
837
- type: "object",
838
- properties: {
839
- created: { type: "integer" },
840
- auto_approved: { type: "boolean" },
841
- },
842
- required: ["created", "auto_approved"],
843
- },
844
- annotations: TOOL_ANNOTATIONS.wyrm_import_pr,
845
- aliases: [],
846
- handler: (args, { store, memory }) => {
847
- const { project_id: prProjId, title: prTitle, body: prBody, review_comments: prComments, auto_approve: prAutoApprove } = args;
848
- const prProject = store.getProjectById(prProjId);
849
- if (!prProject)
850
- return { content: [{ type: "text", text: `Project ID ${prProjId} not found.` }], isError: true };
851
- let prCreated = 0;
852
- const prNeedsReview = prAutoApprove ? 0 : 1;
853
- memory.add(prProjId, {
854
- kind: 'pattern',
855
- problem: `${prTitle}\n\n${prBody}`,
856
- validatedFix: '',
857
- whyItWorked: '',
858
- tags: ['git', 'pr'],
859
- confidence: 0.65,
860
- needsReview: prNeedsReview,
861
- });
862
- prCreated++;
863
- for (const comment of (prComments ?? [])) {
864
- if (/\b(should|must|always|never|prefer|avoid|use|don't)\b/i.test(comment)) {
865
- memory.add(prProjId, {
866
- kind: 'heuristic',
867
- problem: comment,
868
- validatedFix: '',
869
- whyItWorked: '',
870
- tags: ['git', 'pr', 'review'],
871
- confidence: 0.65,
872
- needsReview: prNeedsReview,
873
- });
874
- prCreated++;
875
- }
876
- }
877
- const body = { created: prCreated, auto_approved: !!prAutoApprove };
878
- return renderResult(body, (b, g) => withGlyph(g.brand, '**PR Import Complete**') + '\n\n' +
879
- `${g.bullet} **Artifacts created:** ${b.created}\n` +
880
- `${g.bullet} **Review needed:** ${b.auto_approved ? 'No (auto-approved)' : 'Yes -- run `wyrm_review` to activate'}`);
881
- },
882
- },
883
- {
884
- name: "wyrm_import_rules",
885
- description: "Import a .cursorrules / copilot-instructions file into Wyrm. Parses by section and creates ground truths (for constraint/rule language) or memory artifacts (for general guidelines).",
886
- inputSchema: {
887
- type: "object",
888
- properties: {
889
- project_id: { type: "number", description: "Project ID" },
890
- content: { type: "string", description: "Full text of the rules file" },
891
- format: { type: "string", enum: ["cursorrules", "copilot", "plain"], description: "Source format (default: plain)" },
892
- source_file: { type: "string", description: "Filename for provenance (optional)" },
893
- },
894
- required: ["project_id", "content"],
895
- },
896
- outputSchema: {
897
- type: "object",
898
- properties: {
899
- format: { type: "string" },
900
- source: { type: "string" },
901
- truths_created: { type: "integer" },
902
- artifacts_created: { type: "integer" },
903
- },
904
- required: ["format", "source", "truths_created", "artifacts_created"],
905
- },
906
- annotations: TOOL_ANNOTATIONS.wyrm_import_rules,
907
- aliases: [],
908
- handler: (args, { store, memory, truths }) => {
909
- const { project_id: ruleProjId, content: ruleContent, format: ruleFormat, source_file: ruleSrc } = args;
910
- const rulesProject = store.getProjectById(ruleProjId);
911
- if (!rulesProject)
912
- return { content: [{ type: "text", text: `Project ID ${ruleProjId} not found.` }], isError: true };
913
- const fmt = ruleFormat ?? 'plain';
914
- const src = ruleSrc ?? 'rules';
915
- const baseTags = ['imported', fmt, src];
916
- // Section-based parsing: prefer markdown headings, fall back to paragraph breaks
917
- const byHeadings = ruleContent.split(/\n(?=#)/);
918
- const byParagraphs = ruleContent.split(/\n\n+/);
919
- const chunks = (byHeadings.length >= byParagraphs.length ? byHeadings : byParagraphs)
920
- .map((c) => c.trim())
921
- .filter((c) => c.length >= 15);
922
- let ruleTruthsCreated = 0, ruleArtifactsCreated = 0;
923
- for (const chunk of chunks) {
924
- if (/\b(always|never|must|use|don't|avoid|prefer)\b/i.test(chunk)) {
925
- truths.set(ruleProjId, {
926
- category: 'constraint',
927
- key: chunk.slice(0, 50).replace(/\n/g, ' '),
928
- value: chunk,
929
- source: src,
930
- });
931
- ruleTruthsCreated++;
932
- }
933
- else {
934
- memory.add(ruleProjId, {
935
- kind: 'heuristic',
936
- problem: chunk,
937
- validatedFix: '',
938
- whyItWorked: '',
939
- tags: baseTags,
940
- confidence: 0.7,
941
- needsReview: 1,
942
- });
943
- ruleArtifactsCreated++;
944
- }
945
- }
946
- const body = { format: fmt, source: src, truths_created: ruleTruthsCreated, artifacts_created: ruleArtifactsCreated };
947
- return renderResult(body, (b, g) => withGlyph(g.brand, `**Rules Import Complete** (${b.format})`) + '\n\n' +
948
- `${g.bullet} **Ground truths created:** ${b.truths_created} (active immediately)\n` +
949
- `${g.bullet} **Memory artifacts created:** ${b.artifacts_created} (pending review)\n` +
950
- `${g.bullet} **Source:** ${b.source}\n\nRun \`wyrm_review\` to approve the memory artifacts.`);
951
- },
952
- },
953
- {
954
- name: "wyrm_spec_register",
955
- description: "Make spec-kit Wyrm-native. Reads a spec directory (spec.md title/summary + tasks.md task list) and creates one Wyrm quest per task, linked to a project, with a stored spec→project link. Idempotent — re-running updates the same quests (deduped by a per-spec-task signature) instead of duplicating.",
956
- inputSchema: {
957
- type: "object",
958
- properties: {
959
- specDir: { type: "string", description: "Absolute path to the spec directory (e.g. '.../dragon-platform/specs/001-ghostmesh')" },
960
- projectPath: { type: "string", description: "Path of the Wyrm project to attach the spec + quests to (must already be registered)" },
961
- priority: { type: "string", enum: ["critical", "high", "medium", "low"], description: "Priority for the generated quests (default 'medium')" },
962
- },
963
- required: ["specDir", "projectPath"],
964
- },
965
- outputSchema: {
966
- type: "object",
967
- properties: {
968
- spec_dir: { type: "string" },
969
- title: { type: ["string", "null"] },
970
- project: { type: "string" },
971
- tasks_parsed: { type: "integer" },
972
- quests_created: { type: "integer" },
973
- quests_updated: { type: "integer" },
974
- quest_ids: { type: "array", items: { type: "integer" } },
975
- },
976
- required: ["spec_dir", "title", "project", "tasks_parsed", "quests_created", "quests_updated", "quest_ids"],
977
- },
978
- annotations: TOOL_ANNOTATIONS.wyrm_spec_register,
979
- aliases: [],
980
- handler: (args, { store, indexing }) => {
981
- const { specDir, projectPath, priority } = args;
982
- // 6.x dispatcher-level requireArgs(['specDir','projectPath']) moved
983
- // inline with the handler (same blank-rejection semantics).
984
- const missing = ['specDir', 'projectPath'].filter((f) => {
985
- const v = args[f];
986
- return !v && v !== 0 && v !== false;
987
- });
988
- if (missing.length > 0) {
989
- return { content: [{ type: "text", text: `Missing required arguments: ${missing.join(', ')}` }], isError: true };
990
- }
991
- const project = store.getProject(projectPath);
992
- if (!project) {
993
- // T026: was a non-error advisory text in 6.x — domain not-founds are
994
- // isError per the goals precedent (schema-exempt).
995
- return { content: [{ type: "text", text: `**Spec Register**: Project not found at "${projectPath}". Register it first (wyrm_scan_projects / wyrm_session_start).` }], isError: true };
996
- }
997
- const parsed = readSpecDir(specDir);
998
- if (parsed.tasks.length === 0) {
999
- const body = {
1000
- spec_dir: specDir, title: parsed.title ?? null, project: project.name,
1001
- tasks_parsed: 0, quests_created: 0, quests_updated: 0, quest_ids: [],
1002
- };
1003
- return renderResult(body, (b, g) => withGlyph(g.brand, `**Spec Register**: No tasks found in ${pathJoin(b.spec_dir, 'tasks.md')}. Nothing to create.`));
1004
- }
1005
- const questPriority = priority || 'medium';
1006
- let created = 0;
1007
- let updated = 0;
1008
- const questIds = [];
1009
- for (const task of parsed.tasks) {
1010
- const signature = specTaskSignature(specDir, task.id);
1011
- const title = `${task.id}: ${task.title}`;
1012
- const tags = signature; // the dedupe signature lives in the quest tags
1013
- const existing = store.findQuestBySpecSignature(project.id, signature);
1014
- if (existing) {
1015
- store.updateQuestFields(existing.id, { title, tags });
1016
- questIds.push(existing.id);
1017
- updated++;
1018
- }
1019
- else {
1020
- const quest = store.addQuest(project.id, title, `spec-kit task from ${specDir}`, questPriority, tags);
1021
- indexing()?.enqueue('quest', quest.id, project.id);
1022
- questIds.push(quest.id);
1023
- created++;
1024
- }
1025
- }
1026
- // Persist the spec→project link (idempotent upsert).
1027
- const spec = store.upsertSpec(project.id, specDir, parsed.title, parsed.summary, parsed.tasks.length);
1028
- const body = {
1029
- spec_dir: spec.spec_dir,
1030
- title: spec.title ?? null,
1031
- project: project.name,
1032
- tasks_parsed: parsed.tasks.length,
1033
- quests_created: created,
1034
- quests_updated: updated,
1035
- quest_ids: questIds,
1036
- };
1037
- return renderResult(body, (b, g) => {
1038
- let text = withGlyph(g.brand, '**Spec Registered**') + '\n\n';
1039
- text += `Spec: ${b.title || '(untitled)'}\n`;
1040
- if (spec.summary)
1041
- text += `Summary: ${spec.summary}\n`;
1042
- text += `Dir: ${b.spec_dir}\n`;
1043
- text += `Project: ${b.project}\n`;
1044
- text += `Tasks parsed: ${b.tasks_parsed}\n`;
1045
- text += `Quests created: ${b.quests_created} - updated: ${b.quests_updated}\n`;
1046
- text += `\n\`\`\`json\n${JSON.stringify({
1047
- spec: b.spec_dir,
1048
- title: b.title,
1049
- project: b.project,
1050
- tasksParsed: b.tasks_parsed,
1051
- questsCreated: b.quests_created,
1052
- questsUpdated: b.quests_updated,
1053
- questIds: b.quest_ids,
1054
- }, null, 2)}\n\`\`\``;
1055
- return text;
1056
- });
1057
- },
1058
- },
1059
- {
1060
- name: "wyrm_scaffold_save",
1061
- description: "Save a reasoning scaffold — a structured checklist that guides step-by-step thinking for a specific problem type. Scaffolds are automatically surfaced by wyrm_context_build when the task matches, helping AI models apply proven reasoning patterns.",
1062
- inputSchema: {
1063
- type: "object",
1064
- properties: {
1065
- projectPath: { type: "string", description: "Project path (optional, null for global scaffolds)" },
1066
- problemType: { type: "string", description: "Short slug for the problem type (e.g. 'api-design', 'security-review', 'db-migration')" },
1067
- description: { type: "string", description: "What kind of task this scaffold helps with" },
1068
- whenToUse: { type: "string", description: "Conditions that indicate this scaffold should be applied" },
1069
- steps: { type: "array", items: { type: "string" }, description: "Ordered checklist steps to follow" },
1070
- verificationSteps: { type: "array", items: { type: "string" }, description: "Steps to verify the work is correct" },
1071
- doNotApplyIf: { type: "string", description: "Conditions where this scaffold does NOT apply" },
1072
- tags: { type: "array", items: { type: "string" }, description: "Searchable tags" },
1073
- },
1074
- required: ["problemType", "description", "whenToUse", "steps"],
1075
- },
1076
- outputSchema: {
1077
- type: "object",
1078
- properties: {
1079
- id: { type: "integer" },
1080
- problem_type: { type: "string" },
1081
- description: { type: "string" },
1082
- steps: { type: "integer" },
1083
- },
1084
- required: ["id", "problem_type", "description", "steps"],
1085
- },
1086
- annotations: TOOL_ANNOTATIONS.wyrm_scaffold_save,
1087
- aliases: [],
1088
- handler: (args, { store, scaffolds, cache }) => {
1089
- const { projectPath: spPath, problemType, description: spDesc, whenToUse, steps, verificationSteps, doNotApplyIf, tags: spTags } = args;
1090
- const spProjectId = spPath ? store.getProject(spPath)?.id ?? undefined : undefined;
1091
- const scaffold = scaffolds.save({
1092
- projectId: spProjectId,
1093
- problemType: problemType,
1094
- description: spDesc,
1095
- whenToUse: whenToUse,
1096
- steps,
1097
- verificationSteps: verificationSteps ?? undefined,
1098
- doNotApplyIf: doNotApplyIf ? [doNotApplyIf] : undefined,
1099
- tags: spTags ?? [],
1100
- });
1101
- cache.invalidate('wyrm_scaffold_get');
1102
- cache.invalidate('wyrm_context_build');
1103
- const body = { id: scaffold.id, problem_type: problemType, description: spDesc, steps: steps.length };
1104
- return renderResult(body, (b, g) => withGlyph(g.brand, '**Reasoning Scaffold Saved**') + '\n\n' +
1105
- `${g.bullet} **Problem Type:** ${b.problem_type}\n` +
1106
- `${g.bullet} **Description:** ${b.description}\n` +
1107
- `${g.bullet} **Steps:** ${b.steps} checklist items\n` +
1108
- `${g.bullet} **ID:** ${b.id}\n\n` +
1109
- `This scaffold will be surfaced by \`wyrm_context_build\` when tasks match "${b.problem_type}".`);
1110
- },
1111
- },
1112
- ];
1113
- //# sourceMappingURL=capture.js.map
6
+ `).all(k,e);S.length>0&&(h=S.map(y=>({id:y.id,content:y.problem.slice(0,80)})))}catch{}}const r={status:"captured",type:d,subtype:p,confidence:s,reasoning:b,id:t,ref:`${l}:${t}`,needs_review:o,...h&&h.length>0?{advisory_conflicts:h}:{}};return q(r,(n,x)=>{let $=`Captured as ${n.type}: ${n.subtype}
7
+ Confidence: ${n.confidence}% | ${n.reasoning}
8
+ ID: ${n.ref}`;n.needs_review&&($+=`
9
+ `+I(x.warn,"Stored for review -- run `wyrm_review` to activate"));const k=n.advisory_conflicts;return k&&k.length>0&&($+=`
10
+
11
+ `+I(x.warn,`Advisory: ${k.length} similar heuristic(s) found:`)+`
12
+ `+k.map(S=>` ${x.bullet} [${S.id}] ${S.content}`).join(`
13
+ `)),$})}},{name:"wyrm_remember",description:"Store a distilled knowledge artifact \u2014 proven patterns, lessons learned, anti-patterns, and reasoning traces. These are recalled automatically by wyrm_context_build to help AI models apply past knowledge to new tasks. Example: wyrm_remember({ projectPath: '/home/user/api', kind: 'anti_pattern', problem: 'Jest suite hung after adding the SSE server', validatedFix: 'close() the server in afterAll \u2014 its keep-alive timer held the event loop open', whyItWorked: 'open handles block jest exit', outcome: 'positive', tags: ['jest', 'sse'] })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project this knowledge belongs to"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"],description:"reasoning_trace: a solved problem with steps; lesson: a general insight; pattern: a proven approach; anti_pattern: what NOT to do; heuristic: a rule of thumb"},problem:{type:"string",description:"What was being solved or observed (be specific and searchable)"},constraints:{type:"string",description:"What conditions or constraints applied"},validatedFix:{type:"string",description:"What actually worked \u2014 the validated solution or approach"},whyItWorked:{type:"string",description:"The insight behind WHY this worked (key for transfer learning)"},outcome:{type:"string",enum:["positive","negative","neutral"],description:"Was this approach successful?"},tags:{type:"array",items:{type:"string"},description:"Searchable tags (e.g. ['auth', 'rate-limit', 'typescript'])"},confidence:{type:"number",description:"Confidence level 0.0\u20131.0 (default: 1.0)"},sourceSessionId:{type:"number",description:"Session ID where this was discovered (optional)"}},required:["projectPath","kind","problem"]},outputSchema:{type:"object",properties:{id:{type:"integer"},kind:{type:"string",enum:["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]},problem:{type:"string"},validated_fix:{type:["string","null"]},why_it_worked:{type:["string","null"]},confidence:{type:"number"},tags:{type:["string","null"]}},required:["id","kind","problem","validated_fix","why_it_worked","confidence","tags"]},annotations:E.wyrm_remember,aliases:[],handler:async(v,{store:w,memory:j})=>{const{projectPath:m,kind:_,problem:g,constraints:c,validatedFix:f,whyItWorked:i,outcome:u,tags:a,confidence:d,sourceSessionId:p}=v,s=H("kind",_,["reasoning_trace","lesson","pattern","anti_pattern","heuristic"]);if(s===void 0)throw new U("kind","is required");const b=w.getProject(m);if(!b)return{content:[{type:"text",text:`Project not found: ${m}`}],isError:!0};const e={kind:s,problem:g,constraints:c,validatedFix:f,whyItWorked:i,outcome:u,tags:a,confidence:d,sourceSessionId:p},t=await A("artifact_add",b.id,e,()=>j.add(b.id,e)),l={id:t.id,kind:t.kind,problem:t.problem,validated_fix:t.validated_fix??null,why_it_worked:t.why_it_worked??null,confidence:t.confidence,tags:t.tags??null};return q(l,(o,h)=>{let r=I(h.brand,"**Memory Stored**")+`
14
+
15
+ `;return r+=`${h.bullet} **Kind:** ${o.kind}
16
+ `,r+=`${h.bullet} **ID:** ${o.id}
17
+ `,r+=`${h.bullet} **Problem:** ${o.problem}
18
+ `,o.validated_fix&&(r+=`${h.bullet} **Solution:** ${o.validated_fix}
19
+ `),o.why_it_worked&&(r+=`${h.bullet} **Why it worked:** ${o.why_it_worked}
20
+ `),r+=`${h.bullet} **Confidence:** ${(o.confidence*100).toFixed(0)}%
21
+ `,o.tags&&(r+=`${h.bullet} **Tags:** ${o.tags}
22
+ `),r+="\n_Use `wyrm_recall` to retrieve similar memories, or `wyrm_context_build` to assemble a task brief._",r})}},{name:"wyrm_auto_capture",description:"Auto-extract durable memories from freeform text (a session transcript, notes, a chat) into the REVIEW QUEUE. A LOCAL extractor \u2014 a configurable Ollama model (WYRM_EXTRACT_MODEL / the `model` arg) if set, else a deterministic heuristic \u2014 pulls out truths, failures, decisions, patterns, and lessons as needs_review candidates; approve/reject with wyrm_review (nothing auto-trusted). Closes the 'just talk, it remembers' gap without a cloud LLM. Deduped + idempotent. Example: wyrm_auto_capture({ projectPath: '/home/user/api', text: 'Deploy failed twice because wrangler.toml pointed at the old KV id; switched to the new binding and decided to pin binding names in CI.' })",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the extracted candidates under"},text:{type:"string",description:"Freeform text to extract durable memories from"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic extraction"}},required:["projectPath","text"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","added","skipped","candidates"]},annotations:E.wyrm_auto_capture,aliases:[],handler:async(v,{store:w,raw:j,memory:m,cache:_})=>{const{projectPath:g,text:c,model:f}=v,i=w.getProject(g);if(!i)return{content:[{type:"text",text:`Project not found: ${g}`}],isError:!0};if(!c||c.trim().length<20)return{content:[{type:"text",text:"Provide at least ~20 chars of text to extract from."}],isError:!0};const{candidates:u,method:a,model:d}=await M(c,{model:f}),p=j();let s=0,b=0;for(const t of u){const l=L(t),o=l.tags[l.tags.length-1];if(p.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(i.id,"%"+D(o)+"%")){b++;continue}m.add(i.id,{kind:l.kind,problem:l.problem,tags:l.tags,confidence:l.confidence,needsReview:1,createdBy:"auto-extract"}),s++}s>0&&_.invalidate();const e={method:a,model:d??null,added:s,skipped:b,candidates:u.slice(0,8).map(t=>({kind:t.kind,text:t.text.slice(0,80)}))};return q(e,(t,l)=>{const o=t.candidates.map(h=>` ${l.bullet} [${h.kind}] ${h.text}`).join(`
23
+ `);return I(l.brand,`Auto-capture (${t.method}${t.model?` - ${t.model}`:""}) -- ${t.added} candidate(s) -> review queue, ${t.skipped} already present.`)+`
24
+ ${o}
25
+
26
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_capture_trace",description:"Harvest a harness's working trace into run-tagged review-queue candidates \u2014 OFFLINE. Ingests a Claude Code session transcript (JSONL), a ~/.dragon/traces file/dir, or WYRM_TRACE_TOOL_CALLS event output. The SAME local extractor as auto-capture (Ollama WYRM_EXTRACT_MODEL if set, else deterministic \u2014 never a cloud LLM) pulls durable truths/failures/decisions/patterns/lessons; secrets (API keys, tokens, passwords) are REDACTED before anything is stored; candidates land needs_review=1 stamped with the run_id (arg or ambient envelope). Approve/reject with wyrm_review; deduped + idempotent. Pass `trace` (inline text) OR `path` (a trace file or the ~/.dragon/traces dir). Reached via wyrm_capture({ mode: 'trace' }).",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project to file the candidates under"},trace:{type:"string",description:"Inline trace text (JSONL transcript / dragon records / tool-call events)"},path:{type:"string",description:"Path to a trace file, or the ~/.dragon/traces directory (newest file is read); omit ~ for absolute"},format:{type:"string",enum:["auto","claude-jsonl","dragon","tool-calls"],description:"Trace format (default auto-detect)"},run_id:{type:"string",description:"Tag candidates with this run (default: ambient envelope run_id)"},model:{type:"string",description:"Optional Ollama model for extraction (overrides WYRM_EXTRACT_MODEL); omit for deterministic"}},required:["projectPath"]},outputSchema:{type:"object",properties:{method:{type:"string"},model:{type:["string","null"]},format:{type:"string"},source:{type:"string"},run_id:{type:["string","null"]},segments:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},candidates:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["method","model","format","source","run_id","segments","added","skipped","candidates"]},annotations:E.wyrm_capture_trace,aliases:[],handler:async(v,{store:w,raw:j,memory:m,cache:_})=>{const{projectPath:g,trace:c,path:f,format:i,run_id:u,model:a}=v,d=w.getProject(g);if(!d)return{content:[{type:"text",text:`Project not found: ${g}`}],isError:!0};const p=Y(),s=typeof u=="string"&&u.trim()?u.trim().slice(0,64):p.run_id;let b="",e="inline";if(typeof c=="string"&&c.trim())b=c,e="inline";else if(typeof f=="string"&&f.trim()){let y=f.trim();(y==="~"||y.startsWith("~/"))&&(y=O(V(),y.slice(1)));try{if(R(y).isDirectory()){const T=G(y).map(C=>O(y,C)).filter(C=>{try{return R(C).isFile()}catch{return!1}}).sort((C,F)=>R(F).mtimeMs-R(C).mtimeMs);if(T.length===0)return{content:[{type:"text",text:`No trace files in directory: ${y}`}],isError:!0};b=W(T[0],"utf-8"),e=T[0]}else b=W(y,"utf-8"),e=y}catch(P){return{content:[{type:"text",text:`Could not read trace path "${y}": ${P.message}`}],isError:!0}}}else return{content:[{type:"text",text:"Provide a `trace` (inline text) or a `path` (trace file or ~/.dragon/traces dir)."}],isError:!0};const{segments:t,format:l}=K(b,i??"auto"),o=J(t);if(o.trim().length<20){const y={method:"deterministic",model:null,format:l,source:e,run_id:s,segments:t.length,added:0,skipped:0,candidates:[]};return q(y,(P,T)=>I(T.brand,`Trace harvest (${P.format}) -- no durable content extracted from ${P.segments} segment(s).`))}const{candidates:h,method:r,model:n}=await M(o,{model:a}),x=j();let $=0,k=0;for(const y of h){const P=L(y),T=P.tags[P.tags.length-1];if(x.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(d.id,"%"+D(T)+"%")){k++;continue}const C=s?[...P.tags,"trace",`run:${s}`]:[...P.tags,"trace"];Q({agent_id:p.agent_id,run_id:s,source:s?"param":p.source},()=>m.add(d.id,{kind:P.kind,problem:P.problem,tags:C,confidence:P.confidence,needsReview:1,createdBy:"trace-harvest"})),$++}$>0&&_.invalidate();const S={method:r,model:n??null,format:l,source:e,run_id:s,segments:t.length,added:$,skipped:k,candidates:h.slice(0,8).map(y=>({kind:y.kind,text:y.text.slice(0,80)}))};return q(S,(y,P)=>{const T=y.candidates.map(C=>` ${P.bullet} [${C.kind}] ${C.text}`).join(`
27
+ `);return I(P.brand,`Trace harvest (${y.format}, ${y.method}${y.model?` - ${y.model}`:""}) -- ${y.segments} segment(s) -> ${y.added} candidate(s) to review queue, ${y.skipped} already present${y.run_id?` [run:${y.run_id}]`:""}.`)+(T?`
28
+ ${T}`:"")+`
29
+
30
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_distill",description:"Distill a work session into candidate memory artifacts for review. Parses the session's objectives, decisions, and outcomes into structured artifacts (lessons, patterns, anti-patterns) and queues them for approval via wyrm_review. Use at session end to extract institutional knowledge.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root path"},sessionId:{type:"string",description:"Session ID to distill"},candidates:{type:"array",description:"Pre-parsed candidate artifacts to store for review",items:{type:"object",properties:{kind:{type:"string",enum:["lesson","pattern","anti_pattern","heuristic","reasoning_trace"]},title:{type:"string"},content:{type:"string"},tags:{type:"array",items:{type:"string"}},confidence:{type:"number"}},required:["kind","title","content"]}}},required:["projectPath","candidates"]},outputSchema:{type:"object",properties:{count:{type:"integer"},artifacts:{type:"array",items:{type:"object",properties:{id:{type:"integer"},kind:{type:"string"},title:{type:"string"}},required:["id","kind","title"]}}},required:["count","artifacts"]},annotations:E.wyrm_distill,aliases:[],handler:(v,{store:w,memory:j})=>{const{projectPath:m,candidates:_}=v,g=w.getProject(m);if(!g)return{content:[{type:"text",text:`Project not found: ${m}`}],isError:!0};if(!_||_.length===0)return{content:[{type:"text",text:`**Distill**
31
+
32
+ No candidates provided. Provide an array of candidate artifacts to distill.`}],isError:!0};const c=[];for(const i of _){const u=j.add(g.id,{kind:i.kind,problem:i.title,validatedFix:i.content,tags:i.tags??[],confidence:i.confidence??.7,needsReview:1});c.push({id:u.id,kind:i.kind,title:i.title})}const f={count:c.length,artifacts:c};return q(f,(i,u)=>{let a=I(u.brand,`**Distillation Complete** -- ${i.count} artifact${i.count!==1?"s":""} queued for review`)+`
33
+
34
+ `;return a+=i.artifacts.map(d=>`${u.bullet} [#${d.id}] **${d.kind}**: ${d.title}`).join(`
35
+ `),a+="\n\nUse `wyrm_review` with each ID to approve or reject.",a})}},{name:"wyrm_harvest",description:"Auto-populate memory from artifacts you already produce. Walks a project (or ALL registered projects) and harvests durable facts from its docs (README/CLAUDE/AGENTS/ARCHITECTURE) plus recent commit subjects (git log) into the REVIEW QUEUE \u2014 approve/reject with wyrm_review, nothing is auto-trusted. Idempotent (re-runs skip what's already harvested). Use dryRun to preview. The fix for a thin corpus.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Harvest one project; omit to harvest ALL registered projects"},dryRun:{type:"boolean",description:"Preview what would be harvested without writing anything"},gitLimit:{type:"number",description:"How many recent commits to scan per project (default 30, max 200)"},includeCode:{type:"boolean",description:"Also harvest CODE: tech-stack facts from manifests (package.json/Cargo.toml/\u2026) + TODO/FIXME markers into the review queue, AND index code symbols (functions/classes/types) for search. Heavier \u2014 scope to one project for the full code pass."}}},outputSchema:{type:"object",properties:{dry_run:{type:"boolean"},projects_scanned:{type:"integer"},added:{type:"integer"},skipped:{type:"integer"},top:{type:"array",items:{type:"object",properties:{project:{type:"string"},added:{type:"integer"},skipped:{type:"integer"}},required:["project","added","skipped"]}},sample:{type:"array",items:{type:"string"}},code_symbols:{type:["object","null"],properties:{symbols:{type:"integer"},projects:{type:"integer"},files:{type:"integer"}},required:["symbols","projects","files"]}},required:["dry_run","projects_scanned","added","skipped","top","sample","code_symbols"]},annotations:E.wyrm_harvest,aliases:[],handler:(v,{store:w,raw:j,memory:m,symbols:_,cache:g})=>{const{projectPath:c,dryRun:f,gitLimit:i,includeCode:u}=v,a=j(),d={existsBySig:(r,n)=>!!a.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(r,"%"+D(n)+"%"),addCandidate:(r,n)=>{m.add(r,{kind:n.kind,problem:n.text,tags:[...n.tags,n.sig],confidence:n.confidence,needsReview:1,createdBy:"harvest"})}};let p;if(c){const r=w.getProject(c);if(!r)return{content:[{type:"text",text:"Project not found"}],isError:!0};p=[{id:r.id,name:r.name,path:r.path}]}else p=w.getAllProjects(500).map(r=>({id:r.id,name:r.name,path:r.path}));const{reports:s,totalAdded:b,totalSkipped:e}=X(d,p,{dryRun:f,gitLimit:i,includeCode:u});!f&&b>0&&g.invalidate();let t=null;if(u&&!f){let r=0,n=0,x=0;for(const $ of p)try{const k=_.indexProject($.id,$.path);r+=k.files,n+=k.symbols,x++}catch{}t={symbols:n,projects:x,files:r}}const l=s.filter(r=>r.added>0).sort((r,n)=>n.added-r.added).slice(0,15).map(r=>({project:r.project,added:r.added,skipped:r.skipped})),o=s.flatMap(r=>r.sample??[]).slice(0,6),h={dry_run:!!f,projects_scanned:p.length,added:b,skipped:e,top:l,sample:o,code_symbols:t};return q(h,(r,n)=>{const x=r.code_symbols?`
36
+ Indexed ${r.code_symbols.symbols} code symbols across ${r.code_symbols.projects} project(s) (${r.code_symbols.files} files) -> searchable via wyrm_symbol_search.`:"",$=r.top.map(S=>` +${String(S.added).padStart(3)} (skip ${S.skipped}) ${S.project}`).join(`
37
+ `),k=r.sample.map(S=>` ${n.bullet} ${S}`).join(`
38
+ `);return I(n.brand,`Harvest ${r.dry_run?"(dry run) ":""}-- ${r.added} candidate(s) -> review queue across ${r.projects_scanned} project(s); ${r.skipped} already present.`)+`${x}
39
+ ${$}`+(k?`
40
+
41
+ Sample:
42
+ ${k}`:"")+`
43
+
44
+ Approve/reject with wyrm_review.`})}},{name:"wyrm_import_git",description:"Import a list of commits into the Wyrm memory review queue. Useful for onboarding a project's history or capturing recent work.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},commits:{type:"array",description:"Array of commit objects",items:{type:"object",properties:{message:{type:"string"},diff_summary:{type:"string"},author:{type:"string"},date:{type:"string"}},required:["message"]}},auto_approve:{type:"boolean",description:"Skip review queue and activate immediately (default: false)"}},required:["project_id","commits"]},outputSchema:{type:"object",properties:{captured:{type:"integer"},skipped:{type:"integer"},auto_approved:{type:"boolean"}},required:["captured","skipped","auto_approved"]},annotations:E.wyrm_import_git,aliases:[],handler:(v,{store:w,memory:j})=>{const{project_id:m,commits:_,auto_approve:g}=v;if(!w.getProjectById(m))return{content:[{type:"text",text:`Project ID ${m} not found.`}],isError:!0};let f=0,i=0;for(const a of _){const d=a.message??"";if(/^Merge /i.test(d)){i++;continue}if(/^(chore|bump|release|version)/i.test(d)){i++;continue}let p="pattern";/^fix(\(.+\))?:/i.test(d)?p="lesson":/^feat(\(.+\))?:/i.test(d)?p="pattern":/^refactor(\(.+\))?:/i.test(d)&&(p="heuristic");const s=d.split(":")[0]??"commit";j.add(m,{kind:p,problem:d,validatedFix:a.diff_summary??"",whyItWorked:`Committed by ${a.author??"unknown"} on ${a.date??"unknown"}`,tags:["git","commit",s.toLowerCase()],confidence:.6,needsReview:g?0:1}),f++}return q({captured:f,skipped:i,auto_approved:!!g},(a,d)=>I(d.brand,"**Git Import Complete**")+`
45
+
46
+ ${d.bullet} **Captured:** ${a.captured}
47
+ ${d.bullet} **Skipped:** ${a.skipped} (merges/chores)
48
+ ${d.bullet} **Review needed:** ${a.auto_approved?"No (auto-approved)":`Yes -- run \`wyrm_review\` to activate ${a.captured} artifact${a.captured!==1?"s":""}`}`)}},{name:"wyrm_import_pr",description:"Import a pull request (title, body, review comments) into the Wyrm memory review queue to extract patterns and heuristics.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},title:{type:"string",description:"PR title"},body:{type:"string",description:"PR description / body"},review_comments:{type:"array",items:{type:"string"},description:"Review comment strings"},auto_approve:{type:"boolean",description:"Skip review queue (default: false)"}},required:["project_id","title","body"]},outputSchema:{type:"object",properties:{created:{type:"integer"},auto_approved:{type:"boolean"}},required:["created","auto_approved"]},annotations:E.wyrm_import_pr,aliases:[],handler:(v,{store:w,memory:j})=>{const{project_id:m,title:_,body:g,review_comments:c,auto_approve:f}=v;if(!w.getProjectById(m))return{content:[{type:"text",text:`Project ID ${m} not found.`}],isError:!0};let u=0;const a=f?0:1;j.add(m,{kind:"pattern",problem:`${_}
49
+
50
+ ${g}`,validatedFix:"",whyItWorked:"",tags:["git","pr"],confidence:.65,needsReview:a}),u++;for(const p of c??[])/\b(should|must|always|never|prefer|avoid|use|don't)\b/i.test(p)&&(j.add(m,{kind:"heuristic",problem:p,validatedFix:"",whyItWorked:"",tags:["git","pr","review"],confidence:.65,needsReview:a}),u++);return q({created:u,auto_approved:!!f},(p,s)=>I(s.brand,"**PR Import Complete**")+`
51
+
52
+ ${s.bullet} **Artifacts created:** ${p.created}
53
+ ${s.bullet} **Review needed:** ${p.auto_approved?"No (auto-approved)":"Yes -- run `wyrm_review` to activate"}`)}},{name:"wyrm_import_rules",description:"Import a .cursorrules / copilot-instructions file into Wyrm. Parses by section and creates ground truths (for constraint/rule language) or memory artifacts (for general guidelines).",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID"},content:{type:"string",description:"Full text of the rules file"},format:{type:"string",enum:["cursorrules","copilot","plain"],description:"Source format (default: plain)"},source_file:{type:"string",description:"Filename for provenance (optional)"}},required:["project_id","content"]},outputSchema:{type:"object",properties:{format:{type:"string"},source:{type:"string"},truths_created:{type:"integer"},artifacts_created:{type:"integer"}},required:["format","source","truths_created","artifacts_created"]},annotations:E.wyrm_import_rules,aliases:[],handler:(v,{store:w,memory:j,truths:m})=>{const{project_id:_,content:g,format:c,source_file:f}=v;if(!w.getProjectById(_))return{content:[{type:"text",text:`Project ID ${_} not found.`}],isError:!0};const u=c??"plain",a=f??"rules",d=["imported",u,a],p=g.split(/\n(?=#)/),s=g.split(/\n\n+/),b=(p.length>=s.length?p:s).map(o=>o.trim()).filter(o=>o.length>=15);let e=0,t=0;for(const o of b)/\b(always|never|must|use|don't|avoid|prefer)\b/i.test(o)?(m.set(_,{category:"constraint",key:o.slice(0,50).replace(/\n/g," "),value:o,source:a}),e++):(j.add(_,{kind:"heuristic",problem:o,validatedFix:"",whyItWorked:"",tags:d,confidence:.7,needsReview:1}),t++);return q({format:u,source:a,truths_created:e,artifacts_created:t},(o,h)=>I(h.brand,`**Rules Import Complete** (${o.format})`)+`
54
+
55
+ ${h.bullet} **Ground truths created:** ${o.truths_created} (active immediately)
56
+ ${h.bullet} **Memory artifacts created:** ${o.artifacts_created} (pending review)
57
+ ${h.bullet} **Source:** ${o.source}
58
+
59
+ Run \`wyrm_review\` to approve the memory artifacts.`)}},{name:"wyrm_spec_register",description:"Make spec-kit Wyrm-native. Reads a spec directory (spec.md title/summary + tasks.md task list) and creates one Wyrm quest per task, linked to a project, with a stored spec\u2192project link. Idempotent \u2014 re-running updates the same quests (deduped by a per-spec-task signature) instead of duplicating.",inputSchema:{type:"object",properties:{specDir:{type:"string",description:"Absolute path to the spec directory (e.g. '.../dragon-platform/specs/001-ghostmesh')"},projectPath:{type:"string",description:"Path of the Wyrm project to attach the spec + quests to (must already be registered)"},priority:{type:"string",enum:["critical","high","medium","low"],description:"Priority for the generated quests (default 'medium')"}},required:["specDir","projectPath"]},outputSchema:{type:"object",properties:{spec_dir:{type:"string"},title:{type:["string","null"]},project:{type:"string"},tasks_parsed:{type:"integer"},quests_created:{type:"integer"},quests_updated:{type:"integer"},quest_ids:{type:"array",items:{type:"integer"}}},required:["spec_dir","title","project","tasks_parsed","quests_created","quests_updated","quest_ids"]},annotations:E.wyrm_spec_register,aliases:[],handler:(v,{store:w,indexing:j})=>{const{specDir:m,projectPath:_,priority:g}=v,c=["specDir","projectPath"].filter(e=>{const t=v[e];return!t&&t!==0&&t!==!1});if(c.length>0)return{content:[{type:"text",text:`Missing required arguments: ${c.join(", ")}`}],isError:!0};const f=w.getProject(_);if(!f)return{content:[{type:"text",text:`**Spec Register**: Project not found at "${_}". Register it first (wyrm_scan_projects / wyrm_session_start).`}],isError:!0};const i=z(m);if(i.tasks.length===0){const e={spec_dir:m,title:i.title??null,project:f.name,tasks_parsed:0,quests_created:0,quests_updated:0,quest_ids:[]};return q(e,(t,l)=>I(l.brand,`**Spec Register**: No tasks found in ${N(t.spec_dir,"tasks.md")}. Nothing to create.`))}const u=g||"medium";let a=0,d=0;const p=[];for(const e of i.tasks){const t=Z(m,e.id),l=`${e.id}: ${e.title}`,o=t,h=w.findQuestBySpecSignature(f.id,t);if(h)w.updateQuestFields(h.id,{title:l,tags:o}),p.push(h.id),d++;else{const r=w.addQuest(f.id,l,`spec-kit task from ${m}`,u,o);j()?.enqueue("quest",r.id,f.id),p.push(r.id),a++}}const s=w.upsertSpec(f.id,m,i.title,i.summary,i.tasks.length),b={spec_dir:s.spec_dir,title:s.title??null,project:f.name,tasks_parsed:i.tasks.length,quests_created:a,quests_updated:d,quest_ids:p};return q(b,(e,t)=>{let l=I(t.brand,"**Spec Registered**")+`
60
+
61
+ `;return l+=`Spec: ${e.title||"(untitled)"}
62
+ `,s.summary&&(l+=`Summary: ${s.summary}
63
+ `),l+=`Dir: ${e.spec_dir}
64
+ `,l+=`Project: ${e.project}
65
+ `,l+=`Tasks parsed: ${e.tasks_parsed}
66
+ `,l+=`Quests created: ${e.quests_created} - updated: ${e.quests_updated}
67
+ `,l+=`
68
+ \`\`\`json
69
+ ${JSON.stringify({spec:e.spec_dir,title:e.title,project:e.project,tasksParsed:e.tasks_parsed,questsCreated:e.quests_created,questsUpdated:e.quests_updated,questIds:e.quest_ids},null,2)}
70
+ \`\`\``,l})}},{name:"wyrm_scaffold_save",description:"Save a reasoning scaffold \u2014 a structured checklist that guides step-by-step thinking for a specific problem type. Scaffolds are automatically surfaced by wyrm_context_build when the task matches, helping AI models apply proven reasoning patterns.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path (optional, null for global scaffolds)"},problemType:{type:"string",description:"Short slug for the problem type (e.g. 'api-design', 'security-review', 'db-migration')"},description:{type:"string",description:"What kind of task this scaffold helps with"},whenToUse:{type:"string",description:"Conditions that indicate this scaffold should be applied"},steps:{type:"array",items:{type:"string"},description:"Ordered checklist steps to follow"},verificationSteps:{type:"array",items:{type:"string"},description:"Steps to verify the work is correct"},doNotApplyIf:{type:"string",description:"Conditions where this scaffold does NOT apply"},tags:{type:"array",items:{type:"string"},description:"Searchable tags"}},required:["problemType","description","whenToUse","steps"]},outputSchema:{type:"object",properties:{id:{type:"integer"},problem_type:{type:"string"},description:{type:"string"},steps:{type:"integer"}},required:["id","problem_type","description","steps"]},annotations:E.wyrm_scaffold_save,aliases:[],handler:(v,{store:w,scaffolds:j,cache:m})=>{const{projectPath:_,problemType:g,description:c,whenToUse:f,steps:i,verificationSteps:u,doNotApplyIf:a,tags:d}=v,p=_?w.getProject(_)?.id??void 0:void 0,s=j.save({projectId:p,problemType:g,description:c,whenToUse:f,steps:i,verificationSteps:u??void 0,doNotApplyIf:a?[a]:void 0,tags:d??[]});m.invalidate("wyrm_scaffold_get"),m.invalidate("wyrm_context_build");const b={id:s.id,problem_type:g,description:c,steps:i.length};return q(b,(e,t)=>I(t.brand,"**Reasoning Scaffold Saved**")+`
71
+
72
+ ${t.bullet} **Problem Type:** ${e.problem_type}
73
+ ${t.bullet} **Description:** ${e.description}
74
+ ${t.bullet} **Steps:** ${e.steps} checklist items
75
+ ${t.bullet} **ID:** ${e.id}
76
+
77
+ This scaffold will be surfaced by \`wyrm_context_build\` when tasks match "${e.problem_type}".`)}}];export{ye as captureToolSpecs};