wyrm-mcp 9.2.2 → 9.2.3

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 (118) hide show
  1. package/dist/activation.js +1 -1
  2. package/dist/agent-loop.js +2 -2
  3. package/dist/audit.js +2 -2
  4. package/dist/autoconfig.js +6 -6
  5. package/dist/backup-cli.js +6 -0
  6. package/dist/backup.js +1 -0
  7. package/dist/bridge.js +1 -1
  8. package/dist/buddy-runner.js +1 -1
  9. package/dist/buddy.js +4 -4
  10. package/dist/capabilities.js +1 -1
  11. package/dist/causality.js +3 -3
  12. package/dist/cloud/cli.js +3 -3
  13. package/dist/cloud/sync-daemon-entrypoint.js +1 -1
  14. package/dist/cloud/sync-engine.js +8 -7
  15. package/dist/cloud-backup.js +4 -4
  16. package/dist/cloud-sync-entrypoint.js +1 -1
  17. package/dist/cloud-sync.js +1 -1
  18. package/dist/connectors/ingest.js +2 -2
  19. package/dist/constellation.js +1 -1
  20. package/dist/context-build-budgeted.js +4 -4
  21. package/dist/context-ranking.js +1 -1
  22. package/dist/daemon-writer.js +1 -1
  23. package/dist/database.js +41 -23
  24. package/dist/design.js +1 -1
  25. package/dist/erasure-cli.js +3 -0
  26. package/dist/erasure-log.js +8 -0
  27. package/dist/erasure.js +1 -0
  28. package/dist/event-replication.js +1 -1
  29. package/dist/events.js +11 -11
  30. package/dist/failure-patterns.js +67 -44
  31. package/dist/federation.js +11 -10
  32. package/dist/governance.js +1 -0
  33. package/dist/handlers/agent.js +4 -4
  34. package/dist/handlers/alias-adapters.js +1 -1
  35. package/dist/handlers/audit.js +1 -1
  36. package/dist/handlers/boundary.js +1 -1
  37. package/dist/handlers/capture.js +54 -52
  38. package/dist/handlers/causality.js +2 -2
  39. package/dist/handlers/cloud.js +9 -9
  40. package/dist/handlers/companion.js +4 -4
  41. package/dist/handlers/entity.js +6 -6
  42. package/dist/handlers/events.js +4 -4
  43. package/dist/handlers/failure.js +15 -12
  44. package/dist/handlers/goals.js +2 -2
  45. package/dist/handlers/intelligence.js +81 -74
  46. package/dist/handlers/mcpclient.js +3 -3
  47. package/dist/handlers/presence.js +1 -1
  48. package/dist/handlers/prompts.js +2 -2
  49. package/dist/handlers/quest.js +11 -4
  50. package/dist/handlers/recall.js +10 -10
  51. package/dist/handlers/resources.js +1 -1
  52. package/dist/handlers/review.js +3 -3
  53. package/dist/handlers/run.js +3 -3
  54. package/dist/handlers/search.js +1 -1
  55. package/dist/handlers/session.js +27 -25
  56. package/dist/handlers/share.js +3 -3
  57. package/dist/handlers/skill.js +2 -2
  58. package/dist/handlers/symbols.js +5 -5
  59. package/dist/handlers/syncops.js +2 -2
  60. package/dist/harvest.js +5 -5
  61. package/dist/hours.js +1 -1
  62. package/dist/http-auth.js +2 -2
  63. package/dist/http-fast.js +17 -17
  64. package/dist/http-projects.js +4 -0
  65. package/dist/http-rate-limit.js +1 -0
  66. package/dist/index-health.js +52 -0
  67. package/dist/index.js +2 -2
  68. package/dist/indexer.js +4 -4
  69. package/dist/intelligence.js +36 -29
  70. package/dist/lane-provenance.js +39 -0
  71. package/dist/license.js +1 -1
  72. package/dist/lineage.js +15 -0
  73. package/dist/live-embedding-state.js +2 -2
  74. package/dist/maintenance.js +2 -2
  75. package/dist/mcp-integrity.js +2 -2
  76. package/dist/memory-artifacts.js +39 -37
  77. package/dist/metabolize.js +9 -7
  78. package/dist/metrics.js +1 -1
  79. package/dist/migrate-prompt.js +1 -1
  80. package/dist/migrations.js +236 -45
  81. package/dist/priority-embed.js +1 -1
  82. package/dist/providers/embedding-provider.js +1 -1
  83. package/dist/readonly-gate.js +1 -1
  84. package/dist/redact-backfill.js +1 -0
  85. package/dist/rehydration.js +30 -20
  86. package/dist/reindex.js +1 -1
  87. package/dist/render-target.js +6 -6
  88. package/dist/repl-guard.js +1 -1
  89. package/dist/replication-daemon-entrypoint.js +1 -1
  90. package/dist/replication-daemon.js +1 -1
  91. package/dist/retention.js +1 -0
  92. package/dist/reverse-bridge.js +6 -6
  93. package/dist/review-sample.js +31 -0
  94. package/dist/security.js +1 -1
  95. package/dist/self-maintenance.js +4 -4
  96. package/dist/served-records.js +8 -2
  97. package/dist/setup-wizard.js +1 -1
  98. package/dist/setup.js +1 -1
  99. package/dist/skill-author.js +2 -2
  100. package/dist/store-open.js +1 -0
  101. package/dist/store-readonly.js +1 -0
  102. package/dist/sub-agent.js +2 -2
  103. package/dist/tool-manifest-v2.json +1 -1
  104. package/dist/tool-manifest.json +1 -1
  105. package/dist/truth-freshness.js +8 -0
  106. package/dist/ui-dashboard.js +4 -4
  107. package/dist/vault.js +1 -1
  108. package/dist/vector-init.js +1 -1
  109. package/dist/vectors.js +4 -3
  110. package/dist/visibility.js +12 -12
  111. package/dist/write-guard.js +1 -0
  112. package/dist/wyrm-cli.js +108 -92
  113. package/dist/wyrm-guard.js +13 -12
  114. package/dist/wyrm-loop.js +3 -3
  115. package/dist/wyrm-manifest.json +1 -1
  116. package/dist/wyrm-memo.js +5 -5
  117. package/package.json +1 -1
  118. package/scripts/hooks/wyrm-session-capture.mjs +3 -0
@@ -1,11 +1,11 @@
1
- import{TOOL_ANNOTATIONS as m}from"../tool-annotations.js";import{renderResult as f,withGlyph as s}from"../render.js";import{reverseBridgeSigFromTags as u,recordReverseBridgeRejection as v}from"../reverse-bridge.js";import{asString as y,requireBool as g,requireInt as _}from"../validate.js";const I=[{name:"wyrm_review",description:"Use to process the review queue - approve or reject the pending memory candidates waiting from extraction, harvest, imports, capture conflicts, or session distillation. Approve activates the artifact for recall; reject permanently deletes it (unrecoverable).",inputSchema:{type:"object",properties:{artifactId:{type:"number"},approved:{type:"boolean",description:"true = activate, false = hard delete"},notes:{type:"string"}},required:["artifactId","approved"]},outputSchema:{type:"object",properties:{artifact_id:{type:"integer"},approved:{type:"boolean"},deleted:{type:"boolean"},problem:{type:"string"},notes:{type:["string","null"]}},required:["artifact_id","approved","deleted","problem","notes"]},annotations:m.wyrm_review,aliases:[],handler:(c,{memory:a,raw:n})=>{const o=c,t=_("artifactId",o.artifactId,{min:1}),i=g("approved",o.approved),d=y("notes",o.notes),r=a.get(t);if(!r)return{content:[{type:"text",text:`Artifact #${t} not found.`}],isError:!0};if(i)a.approve(t);else{const e=u(r.tags);e&&v(n(),r.project_id,e),n().prepare("DELETE FROM memory_artifacts WHERE id = ?").run(t)}const l={artifact_id:t,approved:i,deleted:!i,problem:r.problem.slice(0,80),notes:d??null};return f(l,(e,p)=>e.approved?s(p.ok,`**Approved** -- Artifact #${e.artifact_id} is now active.`)+`
1
+ import{TOOL_ANNOTATIONS as f}from"../tool-annotations.js";import{renderResult as m,withGlyph as p}from"../render.js";import{reverseBridgeSigFromTags as v,recordReverseBridgeRejection as u}from"../reverse-bridge.js";import{asString as y,requireBool as g,requireInt as w}from"../validate.js";const j=[{name:"wyrm_review",description:"Use to process the review queue - approve or reject the pending memory candidates waiting from extraction, harvest, imports, capture conflicts, or session distillation. Approve activates the artifact for recall; reject permanently deletes it (unrecoverable).",inputSchema:{type:"object",properties:{artifactId:{type:"number"},approved:{type:"boolean",description:"true = activate, false = hard delete"},notes:{type:"string"}},required:["artifactId","approved"]},outputSchema:{type:"object",properties:{artifact_id:{type:"integer"},approved:{type:"boolean"},deleted:{type:"boolean"},problem:{type:"string"},notes:{type:["string","null"]}},required:["artifact_id","approved","deleted","problem","notes"]},annotations:f.wyrm_review,aliases:[],handler:(s,{memory:o,raw:c})=>{const i=s,t=w("artifactId",i.artifactId,{min:1}),n=g("approved",i.approved),d=y("notes",i.notes),r=o.get(t);if(!r)return{content:[{type:"text",text:`Artifact #${t} not found.`}],isError:!0};if(n)o.approve(t);else{const e=v(r.tags);e&&u(c(),r.project_id,e),o.reject(t)}const l={artifact_id:t,approved:n,deleted:!n,problem:r.problem.slice(0,80),notes:d??null};return m(l,(e,a)=>e.approved?p(a.ok,`**Approved** -- Artifact #${e.artifact_id} is now active.`)+`
2
2
 
3
3
  "${e.problem}"
4
4
 
5
5
  It will appear in future \`wyrm_recall\` and \`wyrm_context_build\` results.${e.notes?`
6
6
 
7
- Notes: ${e.notes}`:""}`:s(p.fail,`**Rejected** -- Artifact #${e.artifact_id} permanently deleted.`)+`
7
+ Notes: ${e.notes}`:""}`:p(a.fail,`**Rejected** -- Artifact #${e.artifact_id} permanently deleted.`)+`
8
8
 
9
9
  "${e.problem}"${e.notes?`
10
10
 
11
- Notes: ${e.notes}`:""}`)}}];export{I as reviewToolSpecs};
11
+ Notes: ${e.notes}`:""}`)}}];export{j as reviewToolSpecs};
@@ -3,10 +3,10 @@ import{TOOL_ANNOTATIONS as z}from"../tool-annotations.js";import{renderResult as
3
3
  ON CONFLICT(run_id, agent_id) DO UPDATE SET role =
4
4
  CASE WHEN run_agents.role = 'orchestrator' THEN run_agents.role
5
5
  ELSE COALESCE(excluded.role, run_agents.role) END
6
- `).run(n,s,r)}function S(e,n,s){return s?e.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ? AND role = 'orchestrator'").get(n,s)!==void 0:!1}function re(e,n,s){return e.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ?").get(n,s)!==void 0}function ne(e){return e.agent_id!==null&&e.source!=="client"&&e.source!=="legacy"}const C=(e,n)=>{e.prepare("UPDATE runs SET updated_at = datetime('now') WHERE run_id = ?").run(n)},ie=e=>({content:[{type:"text",text:`Run not found: ${e}`}],isError:!0}),le=[{name:"wyrm_run",description:"Use to manage a fleet run lifecycle from the orchestrator: action=start mints a ULID run_id and registers the orchestrator, join registers each subagent (agent_id, role), status reports agents, run-quarantined failure counts, and quest claims, debrief fans each agent's learnings through the LOCAL extractor (never a cloud LLM) into a run-scoped review queue for wyrm_review, and end writes the run summary, promotes or expires quarantined failures, and releases claims.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["start","join","status","debrief","end"]},run_id:{type:"string",description:"start mints if omitted"},agent_id:{type:"string",description:"default: ambient envelope"},role:{type:"string",description:"join"},orchestrator:{type:"string",description:"start"},parent_run_id:{type:"string",description:"start: nested fleets"},project_path:{type:"string",description:"debrief/end"},learnings:{type:"array",items:{type:"object"},description:"debrief: [{agent_id?, text}]"},status:{type:"string",enum:["completed","failed","abandoned"],description:"end (default completed)"}},required:["action"]},outputSchema:{type:"object",properties:{action:{type:"string"},run_id:{type:"string"},status:{type:"string"},orchestrator:{type:["string","null"]},agents:{type:"array",items:{type:"object",properties:{agent_id:{type:"string"},role:{type:["string","null"]}},required:["agent_id","role"]}},quarantined:{type:"integer"},resolved:{type:"integer"},claims:{type:"integer"},method:{type:"string"},queued:{type:"integer"},skipped:{type:"integer"},promoted:{type:"integer"},expired:{type:"integer"},claims_released:{type:"integer"},summary_artifact_id:{type:["integer","null"]}},required:["action","run_id","status"]},annotations:z.wyrm_run,aliases:[],handler:async(e,n)=>{const s=W("action",e.action,H);if(!s)throw new E("action",`must be one of: ${H.join(", ")}`);const r=n.raw(),_=B();if(s==="start"){const i=h("run_id",e.run_id),a=i??X(),d=w("orchestrator",e.orchestrator,{maxLen:200})??_.agent_id??null,c=h("agent_id",e.agent_id)??_.agent_id,o=h("parent_run_id",e.parent_run_id),p=O(r,a);if(p){if(!i||p.status!=="running")return{content:[{type:"text",text:`Run ${a} already exists (status: ${p.status}).`}],isError:!0};if(c){if(r.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND role = 'orchestrator' LIMIT 1").get(a)!==void 0&&!S(r,a,c))return{content:[{type:"text",text:`Run ${a} already has an orchestrator \u2014 re-start with the same agent_id, or join as a member instead.`}],isError:!0};q(r,a,c,"orchestrator")}C(r,a)}else{if(o&&!O(r,o))return{content:[{type:"text",text:`Parent run not found: ${o}`}],isError:!0};ee(r,a,o,d),c&&q(r,a,c,"orchestrator")}return R({action:"start",run_id:a,status:"running",parent_run_id:o,orchestrator:d,agents:c?[{agent_id:c,role:"orchestrator"}]:[]},(l,u)=>x(u.brand,`Run ${l.run_id} started${l.orchestrator?` by ${l.orchestrator}`:""}.`)+`
6
+ `).run(n,s,r)}function S(e,n,s){return s?e.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ? AND role = 'orchestrator'").get(n,s)!==void 0:!1}function re(e,n,s){return e.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ?").get(n,s)!==void 0}function ne(e){return e.agent_id!==null&&e.source!=="client"&&e.source!=="writer"&&e.source!=="legacy"}const C=(e,n)=>{e.prepare("UPDATE runs SET updated_at = datetime('now') WHERE run_id = ?").run(n)},ie=e=>({content:[{type:"text",text:`Run not found: ${e}`}],isError:!0}),le=[{name:"wyrm_run",description:"Use to manage a fleet run lifecycle from the orchestrator: action=start mints a ULID run_id and registers the orchestrator, join registers each subagent (agent_id, role), status reports agents, run-quarantined failure counts, and quest claims, debrief fans each agent's learnings through the LOCAL extractor (never a cloud LLM) into a run-scoped review queue for wyrm_review, and end writes the run summary, promotes or expires quarantined failures, and releases claims.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["start","join","status","debrief","end"]},run_id:{type:"string",description:"start mints if omitted"},agent_id:{type:"string",description:"default: ambient envelope"},role:{type:"string",description:"join"},orchestrator:{type:"string",description:"start"},parent_run_id:{type:"string",description:"start: nested fleets"},project_path:{type:"string",description:"debrief/end"},learnings:{type:"array",items:{type:"object"},description:"debrief: [{agent_id?, text}]"},status:{type:"string",enum:["completed","failed","abandoned"],description:"end (default completed)"}},required:["action"]},outputSchema:{type:"object",properties:{action:{type:"string"},run_id:{type:"string"},status:{type:"string"},orchestrator:{type:["string","null"]},agents:{type:"array",items:{type:"object",properties:{agent_id:{type:"string"},role:{type:["string","null"]}},required:["agent_id","role"]}},quarantined:{type:"integer"},resolved:{type:"integer"},claims:{type:"integer"},method:{type:"string"},queued:{type:"integer"},skipped:{type:"integer"},promoted:{type:"integer"},expired:{type:"integer"},claims_released:{type:"integer"},summary_artifact_id:{type:["integer","null"]}},required:["action","run_id","status"]},annotations:z.wyrm_run,aliases:[],handler:async(e,n)=>{const s=W("action",e.action,H);if(!s)throw new E("action",`must be one of: ${H.join(", ")}`);const r=n.raw(),_=B();if(s==="start"){const i=h("run_id",e.run_id),a=i??X(),d=w("orchestrator",e.orchestrator,{maxLen:200})??_.agent_id??null,c=h("agent_id",e.agent_id)??_.agent_id,o=h("parent_run_id",e.parent_run_id),p=O(r,a);if(p){if(!i||p.status!=="running")return{content:[{type:"text",text:`Run ${a} already exists (status: ${p.status}).`}],isError:!0};if(c){if(r.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND role = 'orchestrator' LIMIT 1").get(a)!==void 0&&!S(r,a,c))return{content:[{type:"text",text:`Run ${a} already has an orchestrator; re-start with the same agent_id, or join as a member instead.`}],isError:!0};q(r,a,c,"orchestrator")}C(r,a)}else{if(o&&!O(r,o))return{content:[{type:"text",text:`Parent run not found: ${o}`}],isError:!0};ee(r,a,o,d),c&&q(r,a,c,"orchestrator")}return R({action:"start",run_id:a,status:"running",parent_run_id:o,orchestrator:d,agents:c?[{agent_id:c,role:"orchestrator"}]:[]},(l,u)=>x(u.brand,`Run ${l.run_id} started${l.orchestrator?` by ${l.orchestrator}`:""}.`)+`
7
7
  Subagents join with: wyrm_run({ action: 'join', run_id: '${l.run_id}', agent_id, role })
8
8
  Export WYRM_RUN_ID=${l.run_id} so every write in the fleet carries run attribution.
9
- Prime ONCE per role: wyrm_session_prime({ run_id: '${l.run_id}', role, for_spawn: true }) -> embed the returned brief in each same-role agent's spawn prefix (don't re-prime per agent).`)}const t=h("run_id",e.run_id);if(!t)throw new E("run_id",`is required for action=${s}`);const g=O(r,t);if(!g)return ie(t);if(s==="join"){if(g.status!=="running")return{content:[{type:"text",text:`Run ${t} already ended (status: ${g.status}) \u2014 cannot join.`}],isError:!0};const i=h("agent_id",e.agent_id),a=i??_.agent_id;if(!a)throw new E("agent_id","no agent identity; pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)");const d=w("role",e.role,{maxLen:200})??null;if(i&&ne(_)&&i!==_.agent_id&&!S(r,t,_.agent_id))return{content:[{type:"text",text:`Joining run ${t} as '${i}' while attributed as '${_.agent_id}' is orchestrator-only \u2014 join as yourself, or have the orchestrator register the agent.`}],isError:!0};if(d==="orchestrator"&&!S(r,t,a)&&!S(r,t,_.agent_id))return{content:[{type:"text",text:`Role 'orchestrator' on run ${t} is reserved \u2014 it is registered at action=start. Join with a fleet role instead.`}],isError:!0};q(r,t,a,d),C(r,t);const c=v(r,t),o={action:"join",run_id:t,status:g.status,agents:c.map(p=>({agent_id:p.agent_id,role:p.role}))};return R(o,(p,m)=>x(m.brand,`${a} joined run ${p.run_id}${d?` as ${d}`:""} (${p.agents.length} agent(s) registered).`))}if(s==="status"){const i=v(r,t),a=r.prepare(`
9
+ Prime ONCE per role: wyrm_session_prime({ run_id: '${l.run_id}', role, for_spawn: true }) -> embed the returned brief in each same-role agent's spawn prefix (don't re-prime per agent).`)}const t=h("run_id",e.run_id);if(!t)throw new E("run_id",`is required for action=${s}`);const g=O(r,t);if(!g)return ie(t);if(s==="join"){if(g.status!=="running")return{content:[{type:"text",text:`Run ${t} already ended (status: ${g.status}); cannot join.`}],isError:!0};const i=h("agent_id",e.agent_id),a=i??_.agent_id;if(!a)throw new E("agent_id","no agent identity; pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)");const d=w("role",e.role,{maxLen:200})??null;if(i&&ne(_)&&i!==_.agent_id&&!S(r,t,_.agent_id))return{content:[{type:"text",text:`Joining run ${t} as '${i}' while attributed as '${_.agent_id}' is orchestrator-only; join as yourself, or have the orchestrator register the agent.`}],isError:!0};if(d==="orchestrator"&&!S(r,t,a)&&!S(r,t,_.agent_id))return{content:[{type:"text",text:`Role 'orchestrator' on run ${t} is reserved: it is registered at action=start. Join with a fleet role instead.`}],isError:!0};q(r,t,a,d),C(r,t);const c=v(r,t),o={action:"join",run_id:t,status:g.status,agents:c.map(p=>({agent_id:p.agent_id,role:p.role}))};return R(o,(p,m)=>x(m.brand,`${a} joined run ${p.run_id}${d?` as ${d}`:""} (${p.agents.length} agent(s) registered).`))}if(s==="status"){const i=v(r,t),a=r.prepare(`
10
10
  SELECT
11
11
  SUM(CASE WHEN quarantine_scope = 'run' AND resolved = 0 THEN 1 ELSE 0 END) AS quarantined,
12
12
  SUM(CASE WHEN resolved = 1 THEN 1 ELSE 0 END) AS resolved
@@ -18,7 +18,7 @@ Agents (${o.agents.length}):${m?`
18
18
  ${m}`:" none"}
19
19
  Failures: ${o.quarantined} run-quarantined unresolved, ${o.resolved} resolved
20
20
  Quest claims held: ${o.claims}`})}if(s==="debrief"){const i=w("project_path",e.project_path,{maxLen:500});if(!i)throw new E("project_path","is required for action=debrief (the project the candidates file under)");const a=n.store.getProject(i);if(!a)return{content:[{type:"text",text:`Project not found: ${i}`}],isError:!0};const d=e.learnings;if(!Array.isArray(d)||d.length===0)throw new E("learnings","is required for action=debrief: an array of { agent_id?, text }");if(d.length>64)throw new E("learnings","too many entries (max 64 per debrief call)");for(const u of d){const f=u&&typeof u=="object"?u.text:void 0;typeof f=="string"&&w("learnings[].text",f,{maxLen:32e3})}let c=0,o=0,p=0;const m=new Set;for(const u of d){const f=u&&typeof u=="object"?u.text:void 0;if(typeof f!="string"||f.trim().length<20){o++;continue}const F=f,P=h("learnings[].agent_id",u.agent_id)??_.agent_id,{candidates:M,method:U}=await J(F);m.add(U),p+=M.length;for(const k of M){const y=Q(k),Y=y.tags[y.tags.length-1];if(r.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1").get(a.id,"%"+K(Y)+"%")){o++;continue}G({agent_id:P,run_id:t,source:"param"},()=>n.memory.add(a.id,{kind:y.kind,problem:y.problem,tags:[...y.tags,`run:${t}`],confidence:y.confidence,needsReview:1,createdBy:"run-debrief"})),c++}}C(r,t),c>0&&n.cache.invalidate();const l={action:"debrief",run_id:t,status:g.status,method:m.size===1?[...m][0]:m.size===0?"none":"mixed",submitted:d.length,candidates:p,queued:c,skipped:o};return R(l,(u,f)=>x(f.brand,`Run ${u.run_id} debrief (${u.method}): ${u.submitted} learning(s) -> ${u.queued} candidate(s) queued for review, ${u.skipped} skipped.`)+`
21
- Approve/reject with wyrm_review (run-scoped tag run:${u.run_id}).`)}if(g.status!=="running")return{content:[{type:"text",text:`Run ${t} already ended (status: ${g.status}).`}],isError:!0};const T=h("agent_id",e.agent_id)??_.agent_id;if(!T)throw new E("agent_id","no agent identity: action=end is member-only; pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)");if(!re(r,t,T))return{content:[{type:"text",text:`'${T}' is not a registered member of run ${t} \u2014 only run members may end it (action=join first).`}],isError:!0};const $=W("status",e.status,Z,"completed"),A=w("project_path",e.project_path,{maxLen:500}),j=A?n.store.getProject(A):void 0;if(A&&!j)return{content:[{type:"text",text:`Project not found: ${A}`}],isError:!0};const I=r.prepare(`
21
+ Approve/reject with wyrm_review (run-scoped tag run:${u.run_id}).`)}if(g.status!=="running")return{content:[{type:"text",text:`Run ${t} already ended (status: ${g.status}).`}],isError:!0};const T=h("agent_id",e.agent_id)??_.agent_id;if(!T)throw new E("agent_id","no agent identity: action=end is member-only; pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)");if(!re(r,t,T))return{content:[{type:"text",text:`'${T}' is not a registered member of run ${t}; only run members may end it (action=join first).`}],isError:!0};const $=W("status",e.status,Z,"completed"),A=w("project_path",e.project_path,{maxLen:500}),j=A?n.store.getProject(A):void 0;if(A&&!j)return{content:[{type:"text",text:`Project not found: ${A}`}],isError:!0};const I=r.prepare(`
22
22
  SELECT id FROM failure_patterns
23
23
  WHERE run_id = ? AND quarantine_scope = 'run' AND resolved = 0
24
24
  `).all(t).map(i=>i.id);te(r,t,$),n.failures.sweepRunQuarantine();let N=0,L=0;if(I.length>0){const i=r.prepare("SELECT quarantine_scope, resolved FROM failure_patterns WHERE id = ?");for(const a of I){const d=i.get(a);d&&(d.quarantine_scope!=="run"?N++:d.resolved===1&&L++)}}const D=n.presence.releaseRunClaims(t);try{r.prepare("DELETE FROM run_briefs WHERE run_id = ?").run(t)}catch{}let b=null;if(j){const i=v(r,t);b=n.memory.add(j.id,{kind:"reasoning_trace",problem:`Fleet run ${t} ended ${$}: ${i.length} agent(s), ${N} quarantined failure(s) promoted, ${L} expired, ${D} quest claim(s) released.`,outcome:$==="completed"?"positive":$==="failed"?"negative":"neutral",tags:["run-summary",`run:${t}`],confidence:1,createdBy:"wyrm_run"}).id,r.prepare("UPDATE runs SET debrief_artifact_id = ? WHERE run_id = ?").run(b,t)}return n.cache.invalidate(),R({action:"end",run_id:t,status:$,promoted:N,expired:L,claims_released:D,summary_artifact_id:b},(i,a)=>x(a.brand,`Run ${i.run_id} ended (${i.status}): ${i.promoted} failure(s) promoted, ${i.expired} expired, ${i.claims_released} claim(s) released.`)+(i.summary_artifact_id!==null?`
@@ -11,5 +11,5 @@ import{TOOL_ANNOTATIONS as D}from"../tool-annotations.js";import{renderResult as
11
11
  `;for(const r of e.entities)s+=`${t.bullet} **${r.name}** _(${r.type})_ -- ${r.relationships} relationship${r.relationships!==1?"s":""}
12
12
  `;s+=`
13
13
  `}return e.sections.length===0&&e.skills.length===0&&e.entities.length===0&&(s+=`_No results found._
14
- `),s},{summary:(e,t)=>{const s=e.sections.reduce((r,a)=>r+a.count,0)+e.skills.length+e.entities.length;return v(t.brand,`**Search Results for "${e.query}"** _(mode: ${e.mode})_ -- ${s} result${s!==1?"s":""}`)}});return w.set(L("wyrm_search",JSON.stringify(b)),I,2e4),I}},{name:"wyrm_constellation",description:"Cross-project memory query \u2014 search every registered Wyrm project at once for matching truths, artifacts, quests, or design references. Returns candidates grouped by project; the calling AI does semantic ranking on its side. Use this when the operator asks 'have I solved this before?', 'where else have I used X?', or any portfolio-spanning question. Honours per-row cross_project_visibility ('within' = invisible across projects, 'org' = shared, 'public' = anywhere).",inputSchema:{type:"object",properties:{query:{type:"string",description:"Search keywords. Sanitised; safe FTS5 subset only."},kinds:{type:"array",items:{type:"string",enum:["truth","artifact","quest","decision","reference"]},description:"Restrict to specific kinds. Default: all kinds."},visibility:{type:"array",items:{type:"string",enum:["within","org","public"]},description:"Which visibility levels to include. Default: ['org','public'] \u2014 strict cross-project gate."},per_project_limit:{type:"number",description:"Max hits per project (default 5, max 50)."},caller_project_path:{type:"string",description:"Caller's current project path \u2014 excluded from results unless 'within' is in visibility."}},required:["query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},projects:{type:"array",items:{type:"object",properties:{project:{type:"string"},hits:{type:"array",items:{type:"object",properties:{kind:{type:"string"},id:{type:"integer"},title:{type:"string"},visibility:{type:"string"},snippet:{type:["string","null"]}},required:["kind","id","title","visibility","snippet"]}}},required:["project","hits"]}}},required:["query","count","projects"]},annotations:D.wyrm_constellation,aliases:[],handler:(b,{store:p,raw:k})=>{const{query:_,kinds:w,visibility:S,per_project_limit:x,caller_project_path:u}=b;let h;u&&(h=p.getProject(u)?.id);const y=K(k(),{query:_,kinds:w,visibility:S,perProjectLimit:x,callerProjectId:h}),f=new Map;for(const n of y){const o=f.get(n.projectName)??[];o.push(n),f.set(n.projectName,o)}const T={query:_,count:y.length,projects:Array.from(f.entries()).map(([n,o])=>({project:n,hits:o.map(c=>({kind:c.kind,id:c.id,title:c.title,visibility:c.visibility,snippet:c.snippet??null}))}))};return C(T,(n,o)=>{if(n.count===0)return v(o.brand,`No constellation matches for *${n.query}* across registered projects.`)+"\n\n_Set `cross_project_visibility` to `'org'` or `'public'` on the items you want surfaced here. Default is `'within'` -- within-project only._";const c=[v(o.brand,`**Constellation** -- ${n.count} match(es) for *${n.query}* across ${n.projects.length} project(s)`)];for(const l of n.projects){c.push(""),c.push(`### ${l.project}`);for(const i of l.hits)c.push(`${o.bullet} \`${i.kind}:${i.id}\` **${i.title}** (${i.visibility})`),i.snippet&&c.push(` ${i.snippet}`)}return c.join(`
14
+ `),s},{summary:(e,t)=>{const s=e.sections.reduce((r,a)=>r+a.count,0)+e.skills.length+e.entities.length;return v(t.brand,`**Search Results for "${e.query}"** _(mode: ${e.mode})_ -- ${s} result${s!==1?"s":""}`)}});return w.set(L("wyrm_search",JSON.stringify(b)),I,2e4),I}},{name:"wyrm_constellation",description:"Cross-project memory query: search every registered Wyrm project at once for matching truths, artifacts, quests, or design references. Returns candidates grouped by project; the calling AI does semantic ranking on its side. Use this when the operator asks 'have I solved this before?', 'where else have I used X?', or any portfolio-spanning question. Honours per-row cross_project_visibility ('within' = invisible across projects, 'org' = shared, 'public' = anywhere).",inputSchema:{type:"object",properties:{query:{type:"string",description:"Search keywords. Sanitised; safe FTS5 subset only."},kinds:{type:"array",items:{type:"string",enum:["truth","artifact","quest","decision","reference"]},description:"Restrict to specific kinds. Default: all kinds."},visibility:{type:"array",items:{type:"string",enum:["within","org","public"]},description:"Which visibility levels to include. Default: ['org','public'] (strict cross-project gate)."},per_project_limit:{type:"number",description:"Max hits per project (default 5, max 50)."},caller_project_path:{type:"string",description:"Caller's current project path; excluded from results unless 'within' is in visibility."}},required:["query"]},outputSchema:{type:"object",properties:{query:{type:"string"},count:{type:"integer"},projects:{type:"array",items:{type:"object",properties:{project:{type:"string"},hits:{type:"array",items:{type:"object",properties:{kind:{type:"string"},id:{type:"integer"},title:{type:"string"},visibility:{type:"string"},snippet:{type:["string","null"]}},required:["kind","id","title","visibility","snippet"]}}},required:["project","hits"]}}},required:["query","count","projects"]},annotations:D.wyrm_constellation,aliases:[],handler:(b,{store:p,raw:k})=>{const{query:_,kinds:w,visibility:S,per_project_limit:x,caller_project_path:u}=b;let h;u&&(h=p.getProject(u)?.id);const y=K(k(),{query:_,kinds:w,visibility:S,perProjectLimit:x,callerProjectId:h}),f=new Map;for(const n of y){const o=f.get(n.projectName)??[];o.push(n),f.set(n.projectName,o)}const T={query:_,count:y.length,projects:Array.from(f.entries()).map(([n,o])=>({project:n,hits:o.map(c=>({kind:c.kind,id:c.id,title:c.title,visibility:c.visibility,snippet:c.snippet??null}))}))};return C(T,(n,o)=>{if(n.count===0)return v(o.brand,`No constellation matches for *${n.query}* across registered projects.`)+"\n\n_Set `cross_project_visibility` to `'org'` or `'public'` on the items you want surfaced here. Default is `'within'` -- within-project only._";const c=[v(o.brand,`**Constellation** -- ${n.count} match(es) for *${n.query}* across ${n.projects.length} project(s)`)];for(const l of n.projects){c.push(""),c.push(`### ${l.project}`);for(const i of l.hits)c.push(`${o.bullet} \`${i.kind}:${i.id}\` **${i.title}** (${i.visibility})`),i.snippet&&c.push(` ${i.snippet}`)}return c.join(`
15
15
  `)})}}];export{V as searchToolSpecs};
@@ -1,10 +1,11 @@
1
- import{basename as ue}from"path";import{createHash as pe}from"crypto";import{blockText as X}from"./types.js";import{TOOL_ANNOTATIONS as C}from"../tool-annotations.js";import{renderResult as P,withGlyph as N,cacheKeyFor as L}from"../render.js";import{computeStaleness as le}from"../intelligence.js";import{ValidationError as me,asInt as _e,asString as F}from"../validate.js";import{sanitizeActorId as fe,getActor as ye}from"./boundary.js";import{makeEstimator as he}from"../token-budget.js";import{refreshThread as Z}from"../thread-assemble.js";import{autoEnrollProject as ge,resolveProjectForPath as ee}from"../project-resolve.js";import{resolvePolicy as te,policyHash as se}from"../recall-policy.js";function ne(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread \u2014 what we were working on and thinking about)_
1
+ import{basename as ue}from"path";import{createHash as pe}from"crypto";import{blockText as X}from"./types.js";import{TOOL_ANNOTATIONS as C}from"../tool-annotations.js";import{renderResult as P,withGlyph as N,cacheKeyFor as A}from"../render.js";import{computeStaleness as le}from"../intelligence.js";import{briefTag as me,checkTruthPaths as fe,truthFreshness as he}from"../truth-freshness.js";import{ValidationError as ye,asInt as _e,asString as F}from"../validate.js";import{sanitizeActorId as ge,getActor as je}from"./boundary.js";import{makeEstimator as be}from"../token-budget.js";import{refreshThread as Z}from"../thread-assemble.js";import{autoEnrollProject as Se,resolveProjectForPath as ee}from"../project-resolve.js";import{resolvePolicy as te,policyHash as se}from"../recall-policy.js";import{reviewSampleCounts as ve}from"../review-sample.js";function ne(e){return!e||!e.thread.trim()?null:`### \u{1F9F5} Where We Left Off _(the living thread: what we were working on and thinking about)_
2
2
  `+`_updated ${e.updated_at}${e.updated_by?" \xB7 "+e.updated_by:""} \xB7 ${e.turn_count} turn(s)_`+`
3
3
 
4
- `+e.thread.slice(-1800)}const je={tech_stack:"\u{1F6E0}\uFE0F Tech Stack",architecture:"\u{1F3D7}\uFE0F Architecture",constraint:"\u{1F6A7} Constraints",decision:"\u{1F4CB} Decisions",contact:"\u{1F464} Contacts",other:"\u{1F4CC} Other Facts"};function oe(e,t){let s=`### \u{1F4DA} Project Ground Truths _(treat as authoritative \u2014 do not infer or contradict)_
5
- `;const o=new Map;for(const r of e)o.has(r.category)||o.set(r.category,[]),o.get(r.category).push(r);for(const[r,i]of o){s+=`**${je[r]??r}:**
6
- `;for(const n of i){let u="";if(t){const d=le(n);d!==null&&d>.7&&(u="[\u26A0\uFE0F STALE] ")}s+=`- ${u}**${n.key}:** ${n.value}${n.rationale?` _(${n.rationale})_`:""}
7
- `}}return s}const re=`
4
+ `+e.thread.slice(-1800)}const we={tech_stack:"\u{1F6E0}\uFE0F Tech Stack",architecture:"\u{1F3D7}\uFE0F Architecture",constraint:"\u{1F6A7} Constraints",decision:"\u{1F4CB} Decisions",contact:"\u{1F464} Contacts",other:"\u{1F4CC} Other Facts"};function oe(e,t){const s=`### \u{1F4DA} Project Ground Truths _(treat as authoritative; do not infer or contradict)_
5
+ `,n=new Map;for(const c of e)n.has(c.category)||n.set(c.category,[]),n.get(c.category).push(c);const d=Date.now();let i=!1,o="";for(const[c,p]of n){o+=`**${we[c]??c}:**
6
+ `;for(const r of p){if(!t){o+=`- **${r.key}:** ${r.value}${r.rationale?` _(${r.rationale})_`:""}
7
+ `;continue}const l=le(r),a=he(r,d),b=a.stale||l!==null&&l>.7,h=fe(`${r.value} ${r.rationale??""}`).dead.length>0;(b||h)&&(i=!0),o+=`- **${r.key}** ${me(a,{stale:b,deadPath:h})}: ${r.value}${r.rationale?` _(${r.rationale})_`:""}
8
+ `}}return s+(i?"_STALE = past its TTL, path missing = a named path is gone: re-verify those before relying on them (`wyrm truth verify <id>`, or re-set the same value)._\n":"")+o}const re=`
8
9
  SELECT * FROM quests
9
10
  WHERE project_id = ? AND status = 'pending'
10
11
  ORDER BY
@@ -12,15 +13,15 @@ import{basename as ue}from"path";import{createHash as pe}from"crypto";import{blo
12
13
  id
13
14
  LIMIT 5
14
15
  `;async function ie(){try{const{resolveEmbeddingState:e,tierForResolution:t}=await import("../providers/embedding-provider.js");return t(e())}catch{return{n:1,label:"keyword"}}}function ce(e){const t={critical:"\u{1F534}",high:"\u{1F7E0}",medium:"\u{1F7E1}",low:"\u{1F7E2}"};let s=`### \u{1F3AF} Active Quests
15
- `;for(const o of e)s+=`- ${t[o.priority]??"\u2022"} #${o.id}: ${o.title}
16
- `;return s}function D(e,t,s,o,r){if(s)return e.getProjectById(s);if(o)return t.prepare("SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1").get(`%${o}%`);if(r){const d=e.getProject(r);if(d)return d;const{id:p}=ee(t,r);return p!=null?e.getProjectById(p):void 0}const{id:i}=ee(t,process.cwd());if(i!=null)return e.getProjectById(i);const n=t.prepare(`
16
+ `;for(const n of e)s+=`- ${t[n.priority]??"\u2022"} #${n.id}: ${n.title}
17
+ `;return s}function D(e,t,s,n,d){if(s)return e.getProjectById(s);if(n)return t.prepare("SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1").get(`%${n}%`);if(d){const c=e.getProject(d);if(c)return c;const{id:p}=ee(t,d);return p!=null?e.getProjectById(p):void 0}const{id:i}=ee(t,process.cwd());if(i!=null)return e.getProjectById(i);const o=t.prepare(`
17
18
  SELECT p.* FROM projects p
18
19
  JOIN sessions s ON s.project_id = p.id
19
20
  ORDER BY s.created_at DESC LIMIT 1
20
- `).get();if(n)return n;const u=ge(e);return u?e.getProjectById(u.id):void 0}function M(e){const t=e.getAllProjects(20);return t.length===0?{content:[{type:"text",text:"No projects found. Register one first (wyrm_project action=scan)."}],isError:!0}:{content:[{type:"text",text:`Project not found. Available projects:
21
- ${t.map(o=>`- ${o.name} (ID: ${o.id})`).join(`
21
+ `).get();if(o)return o;const f=Se(e);return f?e.getProjectById(f.id):void 0}function M(e){const t=e.getAllProjects(20);return t.length===0?{content:[{type:"text",text:"No projects found. Register one first (wyrm_project action=scan)."}],isError:!0}:{content:[{type:"text",text:`Project not found. Available projects:
22
+ ${t.map(n=>`- ${n.name} (ID: ${n.id})`).join(`
22
23
  `)}`}],isError:!0}}const ae=(e,t)=>{const s=e.session_id!==null?`
23
- Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.project_name}** | ${e.truth_count} truth${e.truth_count!==1?"s":""} - ${e.quest_count} quest${e.quest_count!==1?"s":""} - ${e.memory_count} memor${e.memory_count!==1?"ies":"y"}${s}`)},be=(e,t)=>{const s=typeof e.tier_n=="number"&&e.tier_label?`tier ${e.tier_n}/3 (${e.tier_label})`:"tier n/a (cached brief)";return N(t.brand,`Wyrm ${t.bullet} ${s} ${t.bullet} ${e.memory_count} memor${e.memory_count!==1?"ies":"y"} on file ${t.bullet} auto-capture: hooks on Claude Code, model-driven elsewhere`)},B=(e,t)=>{const s=ae(e,t),o=e.sections.length>0?e.sections.map(r=>r.text).join(`
24
+ Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.project_name}** | ${e.truth_count} truth${e.truth_count!==1?"s":""} - ${e.quest_count} quest${e.quest_count!==1?"s":""} - ${e.memory_count} memor${e.memory_count!==1?"ies":"y"}${s}`)},$e=(e,t)=>{const s=typeof e.tier_n=="number"&&e.tier_label?`tier ${e.tier_n}/3 (${e.tier_label})`:"tier n/a (cached brief)";return N(t.brand,`Wyrm ${t.bullet} ${s} ${t.bullet} ${e.memory_count} memor${e.memory_count!==1?"ies":"y"} on file ${t.bullet} auto-capture: hooks on Claude Code, model-driven elsewhere`)},B=(e,t)=>{const s=ae(e,t),n=e.sections.length>0?e.sections.map(d=>d.text).join(`
24
25
 
25
26
  ---
26
27
 
@@ -28,19 +29,20 @@ Session logged: #${e.session_id}`:"";return N(t.brand,`Wyrm primed for **${e.pro
28
29
 
29
30
  `+s:s+`
30
31
 
31
- _No memory stored yet -- start working and Wyrm will learn._`;return be(e,t)+`
32
+ _No memory stored yet -- start working and Wyrm will learn._`;return $e(e,t)+`
32
33
 
33
- `+o},W={summary:ae};async function H(e,t,s){try{const{logSavings:o}=await import("../statusline.js"),r=X(s.content[0]).length,i=Math.round(r/4),n=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);o(e,"wyrm_session_prime","recovered_context",i,n?.id)}catch{}}const Se={continuity:{path:"continuity",tool:"wyrm_session_prime"},truths:{path:"truths",tool:"wyrm_truth_get"},scaffold:{path:"scaffolds",tool:"wyrm_context_build"},memory:{path:"memory",tool:"wyrm_recall"},quests:{path:"quests",tool:"wyrm_all_quests"}},ve=48,we=1200;async function Ee(e,{store:t,raw:s,memory:o,truths:r,scaffolds:i,cache:n}){const{project_id:u,project_name:d,task:p,log_session:l}=e,m=fe(e.run_id);if(m===null)throw new me("run_id",'must be <=64 printable-ASCII chars (no ";")');const c=F("role",e.role,{maxLen:200})??"",w=_e("token_budget",e.token_budget,{min:1})??we,y=s(),E=te(y,m),T=E.mode==="full"&&E.mutedIds.length===0&&E.challengedTruthIds.length===0?c:`${c}::${se(E)}`,R=()=>y.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(m,T),x=F("projectPath",e.projectPath),b=u||d||x?D(t,y,u,d,x):void 0;if((u||d||x)&&!b)return M(t);let S=R();if(S&&b){const a=JSON.parse(S.body_json);if(a.project_id!==b.id)return{content:[{type:"text",text:`Fleet brief for (run ${m}, role '${c}') is pinned to project '${a.project_name}' (ID: ${a.project_id}) \u2014 refusing to serve it for requested project '${b.name}' (ID: ${b.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!S){const a=b??D(t,y,u,d,x);if(!a)return M(t);const h=[],I=ne(t.getContinuity(a.id));I&&h.push({kind:"continuity",text:I});const _=p??(c||void 0),f=r.getCurrent(a.id),v=o.buildContextBrief(a.id,_??"general",{maxItems:10,minConfidence:.3,runId:m,groundTruths:f}),Q=v.groundTruths??f;if(Q.length>0&&h.push({kind:"truths",text:oe(Q,!1).slice(0,1200)}),_){const $=i.findBest(_,a.id,.3);$&&h.push({kind:"scaffold",text:i.formatForContext($,1500)})}v.text&&h.push({kind:"memory",text:v.text.slice(0,2e3)});const O=y.prepare(re).all(a.id);O.length>0&&h.push({kind:"quests",text:ce(O).slice(0,800)});const G=he(),q=[];let A=ve;for(const $ of h){const Y=G.count($.text)+2;if(A+Y<=w)q.push($),A+=Y;else{const K=Se[$.kind]??{path:$.kind,tool:"wyrm_search"},z=`[beyond token_budget] wyrm://project/${a.id}/${K.path} -- fetch via ${K.tool}`;q.push({kind:$.kind,text:z}),A+=G.count(z)+2}}let J=null;l&&(J=t.createSession(a.id,{objectives:p??"Session started via wyrm_session_prime",summary:""}).id);const V=await ie(),de={project_id:a.id,project_name:a.name,truth_count:r.getStats(a.id).current,quest_count:O.length,memory_count:o.getStats(a.id).total,session_id:J,sections:q,run_id:m,role:c,token_budget:w,tier_n:V.n,tier_label:V.label};y.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(m,T,JSON.stringify(de),w),S=R()}const j=JSON.parse(S.body_json),k=P(j,B,W);if(e.for_spawn===!0){const a=P(j,B,{...W,exemptStructured:!0}),h=X(a.content[0]),I=pe("sha256").update(h,"utf8").digest("hex"),_={project_id:j.project_id,project_name:j.project_name,session_id:j.session_id??null,sections:j.sections,for_spawn:!0,run_id:m,role:c,brief:h,brief_hash:I,brief_chars:h.length,cache_key:{table:"run_briefs",run_id:m,role:T}};await H(y,j.project_id,a);const f={content:[{type:"text",text:h}],structuredContent:_};return n.set(L("wyrm_session_prime",JSON.stringify(e)),f,3e4),f}return await H(y,j.project_id,k),n.set(L("wyrm_session_prime",JSON.stringify(e)),k,3e4),k}const Le=[{name:"wyrm_session_start",description:"Start or continue a session for a project",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path"},objectives:{type:"string",description:"Session objectives"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},date:{type:"string"},objectives:{type:["string","null"]}},required:["session_id","project","date","objectives"]},annotations:C.wyrm_session_start,aliases:[],handler:(e,{store:t,raw:s})=>{const{projectPath:o,objectives:r}=e;let i=t.getProject(o);i||(i=t.registerProject(ue(o),o));let n=t.getTodaySession(i.id);n?r&&(n=t.updateSession(n.id,{objectives:n.objectives?`${n.objectives}
34
- ${r}`:r})):n=t.createSession(i.id,{objectives:r||""}),t.archiveOldSessions(i.id,10),Z(s(),i.id);const u={session_id:n.id,project:i.name,date:n.date,objectives:n.objectives||null};return P(u,(d,p)=>N(p.brand,`Session ${d.session_id} for ${d.project}`)+`
35
- **Date:** ${d.date}
36
- **Objectives:** ${d.objectives??"None set"}`)}},{name:"wyrm_session_update",description:"Update the current session with completed work, issues, or notes",inputSchema:{type:"object",properties:{projectPath:{type:"string"},completed:{type:"string",description:"What was completed"},issues:{type:"string"},commits:{type:"string",description:"Git commits made"},notes:{type:"string"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},updated:{type:"array",items:{type:"string",enum:["completed","issues","commits","notes"]}}},required:["session_id","project","updated"]},annotations:C.wyrm_session_update,aliases:[],handler:(e,{store:t,indexing:s,raw:o})=>{const{projectPath:r,completed:i,issues:n,commits:u,notes:d}=e,p=t.getProject(r);if(!p)return{content:[{type:"text",text:"Project not found"}],isError:!0};let l=t.getTodaySession(p.id);l||(l=t.createSession(p.id,{}));const m={};i&&(m.completed=l.completed?`${l.completed}
37
- ${i}`:i),n&&(m.issues=l.issues?`${l.issues}
38
- ${n}`:n),u&&(m.commits=l.commits?`${l.commits}
39
- ${u}`:u),d&&(m.notes=l.notes?`${l.notes}
40
- ${d}`:d),l=t.updateSession(l.id,m);const c=[i,n,d].filter(Boolean).join(" \xB7 ");c&&t.updateContinuity(p.id,c,"session_update"),s()?.enqueue("session",l.id,p.id),Z(o(),p.id);const w={session_id:l.id,project:p.name,updated:Object.keys(m)};return P(w,(y,E)=>N(E.brand,`Session updated for ${y.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:C.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:o,include_failures:r,max_truth_chars:i}=e,n=t.rehydrate(s,{include_artifacts:o,include_failures:r,max_truth_chars:i});return n?P({...n},u=>u.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},projectPath:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:C.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:o,truths:r,scaffolds:i,cache:n})=>{const{project_id:u,project_name:d,task:p,log_session:l}=e;if(e.run_id!==void 0&&e.run_id!==null)return Ee(e,{store:t,raw:s,memory:o,truths:r,scaffolds:i,cache:n});const m=F("projectPath",e.projectPath),c=D(t,s(),u,d,m);if(!c)return M(t);const w=ye().run_id,y=te(s(),w),E=y.mode==="full"&&y.mutedIds.length===0&&y.challengedTruthIds.length===0,g=[],T=ne(t.getContinuity(c.id));T&&g.push({kind:"continuity",text:T});const R=r.getCurrent(c.id),x=o.buildContextBrief(c.id,p??"general",{maxItems:10,minConfidence:.3,runId:w,groundTruths:R}),b=x.groundTruths??R;if(b.length>0&&g.push({kind:"truths",text:oe(b,!0).slice(0,1200)}),p){const _=i.findBest(p,c.id,.3);_&&g.push({kind:"scaffold",text:i.formatForContext(_,1500)})}x.text&&g.push({kind:"memory",text:x.text.slice(0,2e3)});const S=s().prepare(re).all(c.id);S.length>0&&g.push({kind:"quests",text:ce(S).slice(0,800)});try{const _=s().prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE project_id = ? AND needs_review = 1").get(c.id).n;_>0&&g.push({kind:"review_queue",text:`### \u{1F4E5} Review Queue
41
- ${_} captured memor${_===1?"y awaits":"ies await"} review (wyrm_review) \u2014 queued memory is invisible to recall until approved.`})}catch{}try{const{resolveEmbeddingState:_}=await import("../providers/embedding-provider.js"),f=_();let v=-1;try{v=s().prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{v=0}f.resolved==="none"?g.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
42
- No embedding provider is active (${f.reason||"no provider"}) - recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10). Fix: ${f.fix}`}):f.resolved==="local"?g.push({kind:"vector_health",text:`### [!] Semantic Recall is NON-SEMANTIC
43
- Provider 'local' (hash-384) is a test-grade fallback, not a real embedder - recall quality is at the FTS floor. Fix: ${f.fix}`}):v===0&&g.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
44
- Provider '${f.resolved}' (${f.model}) is configured but nothing is indexed - recall is running FTS5-only until the index is built. Fix: wyrm index rebuild`})}catch{}try{const{vaultAdvisory:_}=await import("../vault.js"),f=_();f.length>0&&g.push({kind:"vault",text:`### \u{1F510} Credential Vault
45
- `+f.map(v=>`- ${v}`).join(`
46
- `)})}catch{}let j=null;l&&(j=t.createSession(c.id,{objectives:p??"Session started via wyrm_session_prime",summary:""}).id);const k=await ie(),U={project_id:c.id,project_name:c.name,truth_count:r.getStats(c.id).current,quest_count:S.length,memory_count:o.getStats(c.id).total,session_id:j,sections:g,tier_n:k.n,tier_label:k.label},a=P(U,B,W);await H(s(),c.id,a);const h=L("wyrm_session_prime",JSON.stringify(e)),I=E?h:`${h}::${se(y)}`;return n.set(I,a,3e4),a}}];export{Le as sessionToolSpecs};
34
+ `+n},W={summary:ae};async function H(e,t,s){try{const{logSavings:n}=await import("../statusline.js"),d=X(s.content[0]).length,i=Math.round(d/4),o=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);n(e,"wyrm_session_prime","recovered_context",i,o?.id)}catch{}}const Ee={continuity:{path:"continuity",tool:"wyrm_session_prime"},truths:{path:"truths",tool:"wyrm_truth_get"},scaffold:{path:"scaffolds",tool:"wyrm_context_build"},memory:{path:"memory",tool:"wyrm_recall"},quests:{path:"quests",tool:"wyrm_all_quests"}},Te=48,xe=1200;async function ke(e,{store:t,raw:s,memory:n,truths:d,scaffolds:i,cache:o}){const{project_id:f,project_name:c,task:p,log_session:r}=e,l=ge(e.run_id);if(l===null)throw new ye("run_id",'must be <=64 printable-ASCII chars (no ";")');const a=F("role",e.role,{maxLen:200})??"",b=_e("token_budget",e.token_budget,{min:1})??xe,h=s(),$=te(h,l),x=$.mode==="full"&&$.mutedIds.length===0&&$.challengedTruthIds.length===0?a:`${a}::${se($)}`,R=()=>h.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(l,x),E=F("projectPath",e.projectPath),v=f||c||E?D(t,h,f,c,E):void 0;if((f||c||E)&&!v)return M(t);let w=R();if(w&&v){const u=JSON.parse(w.body_json);if(u.project_id!==v.id)return{content:[{type:"text",text:`Fleet brief for (run ${l}, role '${a}') is pinned to project '${u.project_name}' (ID: ${u.project_id}); refusing to serve it for requested project '${v.name}' (ID: ${v.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!w){const u=v??D(t,h,f,c,E);if(!u)return M(t);const _=[],I=ne(t.getContinuity(u.id));I&&_.push({kind:"continuity",text:I});const y=p??(a||void 0),m=d.getCurrent(u.id),g=n.buildContextBrief(u.id,y??"general",{maxItems:10,minConfidence:.3,runId:l,groundTruths:m}),Q=g.groundTruths??m;if(Q.length>0&&_.push({kind:"truths",text:oe(Q,!1).slice(0,1200)}),y){const T=i.findBest(y,u.id,.3);T&&_.push({kind:"scaffold",text:i.formatForContext(T,1500)})}g.text&&_.push({kind:"memory",text:g.text.slice(0,2e3)});const O=h.prepare(re).all(u.id);O.length>0&&_.push({kind:"quests",text:ce(O).slice(0,800)});const G=be(),q=[];let L=Te;for(const T of _){const Y=G.count(T.text)+2;if(L+Y<=b)q.push(T),L+=Y;else{const K=Ee[T.kind]??{path:T.kind,tool:"wyrm_search"},z=`[beyond token_budget] wyrm://project/${u.id}/${K.path} -- fetch via ${K.tool}`;q.push({kind:T.kind,text:z}),L+=G.count(z)+2}}let J=null;r&&(J=t.createSession(u.id,{objectives:p??"Session started via wyrm_session_prime",summary:""}).id);const V=await ie(),de={project_id:u.id,project_name:u.name,truth_count:d.getStats(u.id).current,quest_count:O.length,memory_count:n.getStats(u.id).total,session_id:J,sections:q,run_id:l,role:a,token_budget:b,tier_n:V.n,tier_label:V.label};h.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(l,x,JSON.stringify(de),b),w=R()}const S=JSON.parse(w.body_json),k=P(S,B,W);if(e.for_spawn===!0){const u=P(S,B,{...W,exemptStructured:!0}),_=X(u.content[0]),I=pe("sha256").update(_,"utf8").digest("hex"),y={project_id:S.project_id,project_name:S.project_name,session_id:S.session_id??null,sections:S.sections,for_spawn:!0,run_id:l,role:a,brief:_,brief_hash:I,brief_chars:_.length,cache_key:{table:"run_briefs",run_id:l,role:x}};await H(h,S.project_id,u);const m={content:[{type:"text",text:_}],structuredContent:y};return o.set(A("wyrm_session_prime",JSON.stringify(e)),m,3e4),m}return await H(h,S.project_id,k),o.set(A("wyrm_session_prime",JSON.stringify(e)),k,3e4),k}const He=[{name:"wyrm_session_start",description:"Start or continue a session for a project",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project path"},objectives:{type:"string",description:"Session objectives"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},date:{type:"string"},objectives:{type:["string","null"]}},required:["session_id","project","date","objectives"]},annotations:C.wyrm_session_start,aliases:[],handler:(e,{store:t,raw:s})=>{const{projectPath:n,objectives:d}=e;let i=t.getProject(n);i||(i=t.registerProject(ue(n),n));let o=t.getTodaySession(i.id);o?d&&(o=t.updateSession(o.id,{objectives:o.objectives?`${o.objectives}
35
+ ${d}`:d})):o=t.createSession(i.id,{objectives:d||""}),t.archiveOldSessions(i.id,10),Z(s(),i.id);const f={session_id:o.id,project:i.name,date:o.date,objectives:o.objectives||null};return P(f,(c,p)=>N(p.brand,`Session ${c.session_id} for ${c.project}`)+`
36
+ **Date:** ${c.date}
37
+ **Objectives:** ${c.objectives??"None set"}`)}},{name:"wyrm_session_update",description:"Update the current session with completed work, issues, or notes",inputSchema:{type:"object",properties:{projectPath:{type:"string"},completed:{type:"string",description:"What was completed"},issues:{type:"string"},commits:{type:"string",description:"Git commits made"},notes:{type:"string"}},required:["projectPath"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project:{type:"string"},updated:{type:"array",items:{type:"string",enum:["completed","issues","commits","notes"]}}},required:["session_id","project","updated"]},annotations:C.wyrm_session_update,aliases:[],handler:(e,{store:t,indexing:s,raw:n})=>{const{projectPath:d,completed:i,issues:o,commits:f,notes:c}=e,p=t.getProject(d);if(!p)return{content:[{type:"text",text:"Project not found"}],isError:!0};let r=t.getTodaySession(p.id);r||(r=t.createSession(p.id,{}));const l={};i&&(l.completed=r.completed?`${r.completed}
38
+ ${i}`:i),o&&(l.issues=r.issues?`${r.issues}
39
+ ${o}`:o),f&&(l.commits=r.commits?`${r.commits}
40
+ ${f}`:f),c&&(l.notes=r.notes?`${r.notes}
41
+ ${c}`:c),r=t.updateSession(r.id,l);const a=[i,o,c].filter(Boolean).join(" \xB7 ");a&&t.updateContinuity(p.id,a,"session_update"),s()?.enqueue("session",r.id,p.id),Z(n(),p.id);const b={session_id:r.id,project:p.name,updated:Object.keys(l)};return P(b,(h,$)=>N($.brand,`Session updated for ${h.project}`))}},{name:"wyrm_session_rehydrate",description:"Lossless session rehydration. Given a past session ID, returns a complete briefing markdown a fresh AI agent can ingest to inherit prior state: objectives, completed work, notes, summary, current ground truths, open quests, validated patterns (artifacts), unresolved failure patterns. Cross-session continuity no other AI memory tool offers.",inputSchema:{type:"object",properties:{session_id:{type:"number",description:"ID of the session to rehydrate"},include_artifacts:{type:"boolean",description:"Include memory artifacts (default true)"},include_failures:{type:"boolean",description:"Include unresolved failure patterns (default true)"},max_truth_chars:{type:"number",description:"Cap on chars used for ground truths (default 2000)"}},required:["session_id"]},outputSchema:{type:"object",properties:{session_id:{type:"integer"},project_name:{type:"string"},project_path:{type:"string"},briefing_markdown:{type:"string"},context_chars:{type:"integer"},attached:{type:"object",properties:{quests:{type:"integer"},truths:{type:"integer"},artifacts:{type:"integer"},failures:{type:"integer"}},required:["quests","truths","artifacts","failures"]}},required:["session_id","project_name","project_path","briefing_markdown","context_chars","attached"]},annotations:C.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:s,include_artifacts:n,include_failures:d,max_truth_chars:i}=e,o=t.rehydrate(s,{include_artifacts:n,include_failures:d,max_truth_chars:i});return o?P({...o},f=>f.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${s} not found.`}],isError:!0}}},{name:"wyrm_session_prime",description:"Use first at session start - load everything you need to know before starting work, and catch me up on where we left off last time: ground truths, memory brief, reasoning scaffold, and active quests in one call. First call every agent makes in a run.",inputSchema:{type:"object",properties:{project_id:{type:"number"},project_name:{type:"string"},projectPath:{type:"string"},task:{type:"string",description:"Drives scaffold + memory selection"},log_session:{type:"boolean",description:"Also log a session row"},run_id:{type:"string",description:"Fleet: brief cached per (run_id, role)"},role:{type:"string"},token_budget:{type:"number"},for_spawn:{type:"boolean"}}},outputSchema:{type:"object",properties:{project_id:{type:"integer"},project_name:{type:"string"},session_id:{type:["integer","null"]},sections:{type:"array",items:{type:"object",properties:{kind:{type:"string"},text:{type:"string"}},required:["kind","text"]}}},required:["project_id","project_name","session_id","sections"]},annotations:C.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:s,memory:n,truths:d,scaffolds:i,cache:o})=>{const{project_id:f,project_name:c,task:p,log_session:r}=e;if(e.run_id!==void 0&&e.run_id!==null)return ke(e,{store:t,raw:s,memory:n,truths:d,scaffolds:i,cache:o});const l=F("projectPath",e.projectPath),a=D(t,s(),f,c,l);if(!a)return M(t);const b=je().run_id,h=te(s(),b),$=h.mode==="full"&&h.mutedIds.length===0&&h.challengedTruthIds.length===0,j=[],x=ne(t.getContinuity(a.id));x&&j.push({kind:"continuity",text:x});const R=d.getCurrent(a.id),E=n.buildContextBrief(a.id,p??"general",{maxItems:10,minConfidence:.3,runId:b,groundTruths:R}),v=E.groundTruths??R;if(v.length>0&&j.push({kind:"truths",text:oe(v,!0).slice(0,1200)}),p){const y=i.findBest(p,a.id,.3);y&&j.push({kind:"scaffold",text:i.formatForContext(y,1500)})}E.text&&j.push({kind:"memory",text:E.text.slice(0,2e3)});const w=s().prepare(re).all(a.id);w.length>0&&j.push({kind:"quests",text:ce(w).slice(0,800)});try{const y=s().prepare("SELECT COUNT(*) AS n FROM memory_artifacts WHERE project_id = ? AND needs_review = 1").get(a.id).n,m=ve(s(),a.id).unreviewed,g=[];y>0&&g.push(`${y} captured memor${y===1?"y awaits":"ies await"} review (wyrm_review). Queued memory is invisible to recall until approved.`),m>0&&g.push(`${m} auto-approved memor${m===1?"y was":"ies were"} sampled for a spot check (wyrm review sample). They stay in recall either way.`),g.length>0&&j.push({kind:"review_queue",text:`### \u{1F4E5} Review Queue
42
+ ${g.join(`
43
+ `)}`})}catch{}try{const{resolveEmbeddingState:y}=await import("../providers/embedding-provider.js"),m=y();let g=-1;try{g=s().prepare("SELECT COUNT(*) AS n FROM vectors").get().n}catch{g=0}m.resolved==="none"?j.push({kind:"vector_health",text:`### [!] Semantic Recall is OFF
44
+ No embedding provider is active (${m.reason||"no provider"}) - recall is FTS5 keyword-only (~59.9% vs 72.2% hybrid recall@10). Fix: ${m.fix}`}):m.resolved==="local"?j.push({kind:"vector_health",text:`### [!] Semantic Recall is NON-SEMANTIC
45
+ Provider 'local' (hash-384) is a test-grade fallback, not a real embedder - recall quality is at the FTS floor. Fix: ${m.fix}`}):g===0&&j.push({kind:"vector_health",text:`### [!] Vector Index is EMPTY
46
+ Provider '${m.resolved}' (${m.model}) is configured but nothing is indexed - recall is running FTS5-only until the index is built. Fix: wyrm index rebuild`})}catch{}try{const{vaultAdvisory:y}=await import("../vault.js"),m=y();m.length>0&&j.push({kind:"vault",text:`### \u{1F510} Credential Vault
47
+ `+m.map(g=>`- ${g}`).join(`
48
+ `)})}catch{}let S=null;r&&(S=t.createSession(a.id,{objectives:p??"Session started via wyrm_session_prime",summary:""}).id);const k=await ie(),U={project_id:a.id,project_name:a.name,truth_count:d.getStats(a.id).current,quest_count:w.length,memory_count:n.getStats(a.id).total,session_id:S,sections:j,tier_n:k.n,tier_label:k.label},u=P(U,B,W);await H(s(),a.id,u);const _=A("wyrm_session_prime",JSON.stringify(e)),I=$?_:`${_}::${se(h)}`;return o.set(I,u,3e4),u}}];export{He as sessionToolSpecs};
@@ -1,8 +1,8 @@
1
- import{TOOL_ANNOTATIONS as p}from"../tool-annotations.js";import{ValidationError as h,asEnum as g,requireInt as _}from"../validate.js";const m=["session","quest","truth","artifact","edge"];function y(r){const s=g("kind",r.kind,m);if(s===void 0)throw new h("kind",`is required (one of: ${m.join(", ")})`);return{kind:s,id:_("id",r.id,{min:1})}}const w=[{name:"wyrm_ask",description:"Wyrm answers a natural-language question using its own data as context. Assembles relevant ground truths, sessions, artifacts, failures, symbols, then generates an answer. BACKEND POLICY (9.0.0): Ollama (local, auto-detected on localhost:11434) is the only backend used by default, because the context sent to a backend is the operator's own memory. Cloud backends \u2014 Claude (ANTHROPIC_API_KEY), NVIDIA NIM (NVIDIA_API_KEY), OpenAI (OPENAI_API_KEY) \u2014 are OPT-IN: they run only when the operator sets WYRM_ASK_ALLOW_CLOUD=1 or the caller names one via model_override. Having an API key set is NOT consent to send memory off the machine. If no permitted backend is reachable, returns the assembled raw context (degraded mode \u2014 explicit, not silent) with an error naming the remedy.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Question in natural language"},projectPath:{type:"string",description:"Scope context to one project (optional)"},max_context_chars:{type:"number",description:"Soft cap on assembled context (default 12000, max 50000)"},model_override:{type:"string",description:"Force a backend/model: 'ollama:llama3.2', 'anthropic:claude-opus-4-8' (alias 'claude'), 'nim:nvidia/nemotron-3-ultra-550b' (alias 'nemotron'), or 'openai:gpt-4o-mini'"}},required:["query"]},annotations:p.wyrm_ask,aliases:[],handler:async(r,s)=>{const{db:c,subAgent:o}=s,{query:t,projectPath:e,max_context_chars:a,model_override:i}=r,u=e?c.getProject(e):null,n=await o.ask({query:t,project_id:u?.id??null,max_context_chars:a,model_override:i});return{content:[{type:"text",text:(n.degraded?`\u{F115D} Wyrm (${n.model}, ${n.context_chars} ctx chars, ${n.latency_ms}ms) \u2014 DEGRADED MODE: ${n.error}
1
+ import{TOOL_ANNOTATIONS as p}from"../tool-annotations.js";import{ValidationError as h,asEnum as g,requireInt as _}from"../validate.js";const m=["session","quest","truth","artifact","edge"];function y(r){const a=g("kind",r.kind,m);if(a===void 0)throw new h("kind",`is required (one of: ${m.join(", ")})`);return{kind:a,id:_("id",r.id,{min:1})}}const w=[{name:"wyrm_ask",description:"Wyrm answers a natural-language question using its own data as context. Assembles relevant ground truths, sessions, artifacts, failures, symbols, then generates an answer. BACKEND POLICY (9.0.0): Ollama (local, auto-detected on localhost:11434) is the only backend used by default, because the context sent to a backend is the operator's own memory. Cloud backends (Claude via ANTHROPIC_API_KEY, NVIDIA NIM via NVIDIA_API_KEY, OpenAI via OPENAI_API_KEY) are OPT-IN: they run only when the operator sets WYRM_ASK_ALLOW_CLOUD=1 or the caller names one via model_override. Having an API key set is NOT consent to send memory off the machine. If no permitted backend is reachable, returns the assembled raw context (degraded mode: explicit, not silent) with an error naming the remedy.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Question in natural language"},projectPath:{type:"string",description:"Scope context to one project (optional)"},max_context_chars:{type:"number",description:"Soft cap on assembled context (default 12000, max 50000)"},model_override:{type:"string",description:"Force a backend/model: 'ollama:llama3.2', 'anthropic:claude-opus-4-8' (alias 'claude'), 'nim:nvidia/nemotron-3-ultra-550b' (alias 'nemotron'), or 'openai:gpt-4o-mini'"}},required:["query"]},annotations:p.wyrm_ask,aliases:[],handler:async(r,a)=>{const{db:c,subAgent:o}=a,{query:t,projectPath:e,max_context_chars:s,model_override:i}=r,u=e?c.getProject(e):null,n=await o.ask({query:t,project_id:u?.id??null,max_context_chars:s,model_override:i});return{content:[{type:"text",text:(n.degraded?`\u{F115D} Wyrm (${n.model}, ${n.context_chars} ctx chars, ${n.latency_ms}ms). DEGRADED MODE: ${n.error}
2
2
 
3
3
  `:`\u{F115D} Wyrm (${n.model}, ${n.context_chars} ctx chars, ${n.latency_ms}ms${n.tokens_in?`, ${n.tokens_in}+${n.tokens_out} tok`:""}):
4
4
 
5
- `)+n.answer}]}}},{name:"wyrm_share",description:"Use to share a finding with the rest of the team - marks a row (session/quest/truth/artifact/edge) is_shared for the next sync push; revoke with wyrm_replication action=unshare.",inputSchema:{type:"object",properties:{kind:{type:"string",enum:["session","quest","truth","artifact","edge"]},id:{type:"number"}},required:["kind","id"]},annotations:p.wyrm_share,aliases:[],handler:async(r,s)=>{const{federation:c}=s,{kind:o,id:t}=y(r),e=c.shareOutcome(o,t);if(e.ok)return{content:[{type:"text",text:`\u{F115D} ${o}#${t} marked shared.`}]};if(e.reason==="private_grove"){const a=e.projectId!=null?String(e.projectId):"<project>";return{content:[{type:"text",text:`\u{F115D} ${o}#${t} was not shared: it belongs to a private grove (project ${a}), and private groves never leave this machine. To share it, change the grove's lane first: \`wyrm grove policy ${a} cloud\` (your own cloud) or \`wyrm grove policy ${a} team\` (federates to a team Wyrm).`}],isError:!0}}return{content:[{type:"text",text:`\u{F115D} ${o}#${t} not found.`}],isError:!0}}},{name:"wyrm_unshare",description:"Revoke team visibility for a row. Local row stays; future syncs won't include it.",inputSchema:{type:"object",properties:{kind:{type:"string",enum:["session","quest","truth","artifact","edge"]},id:{type:"number"}},required:["kind","id"]},annotations:p.wyrm_unshare,aliases:[],handler:async(r,s)=>{const{federation:c}=s,{kind:o,id:t}=y(r);return c.unshare(o,t)?{content:[{type:"text",text:`\u{F115D} ${o}#${t} unshared.`}]}:{content:[{type:"text",text:`\u{F115D} ${o}#${t} not found.`}],isError:!0}}},{name:"wyrm_sync_conflicts",description:"List unresolved sync conflicts \u2014 rows where a remote push landed on top of a newer local edit. Each conflict has been auto-converted into a quest (tag 'sync-conflict') so the operator sees it on next session.",inputSchema:{type:"object",properties:{limit:{type:"number",description:"Max conflicts (default 50)"}}},annotations:p.wyrm_sync_conflicts,aliases:[],handler:async(r,s)=>{const{federation:c,sync:o}=s,{limit:t}=r,e=c.unresolvedConflicts(Math.min(Math.max(1,t??50),500));if(e.length===0)return{content:[{type:"text",text:"\u{F115D} No unresolved sync conflicts."}]};const a=e.map(i=>`\u2022 #${i.id} ${i.local_kind}#${i.local_id} from ${i.remote_origin??"?"} at ${i.detected_at}`+(i.quest_id?` \u2192 quest #${i.quest_id}`:"")).join(`
5
+ `)+n.answer}]}}},{name:"wyrm_share",description:"Use to share a finding with the rest of the team - marks a row (session/quest/truth/artifact/edge) is_shared for the next sync push; revoke with wyrm_replication action=unshare.",inputSchema:{type:"object",properties:{kind:{type:"string",enum:["session","quest","truth","artifact","edge"]},id:{type:"number"}},required:["kind","id"]},annotations:p.wyrm_share,aliases:[],handler:async(r,a)=>{const{federation:c}=a,{kind:o,id:t}=y(r),e=c.shareOutcome(o,t);if(e.ok)return{content:[{type:"text",text:`\u{F115D} ${o}#${t} marked shared.`}]};if(e.reason==="private_grove"){const s=e.projectId!=null?String(e.projectId):"<project>";return{content:[{type:"text",text:`\u{F115D} ${o}#${t} was not shared: it belongs to a private grove (project ${s}), and private groves never leave this machine. To share it, change the grove's lane first: \`wyrm grove policy ${s} cloud\` (your own cloud) or \`wyrm grove policy ${s} team\` (federates to a team Wyrm).`}],isError:!0}}return{content:[{type:"text",text:`\u{F115D} ${o}#${t} not found.`}],isError:!0}}},{name:"wyrm_unshare",description:"Revoke team visibility for a row. Local row stays; future syncs won't include it.",inputSchema:{type:"object",properties:{kind:{type:"string",enum:["session","quest","truth","artifact","edge"]},id:{type:"number"}},required:["kind","id"]},annotations:p.wyrm_unshare,aliases:[],handler:async(r,a)=>{const{federation:c}=a,{kind:o,id:t}=y(r);return c.unshare(o,t)?{content:[{type:"text",text:`\u{F115D} ${o}#${t} unshared.`}]}:{content:[{type:"text",text:`\u{F115D} ${o}#${t} not found.`}],isError:!0}}},{name:"wyrm_sync_conflicts",description:"List unresolved sync conflicts: rows where a remote push landed on top of a newer local edit. Each conflict has been auto-converted into a quest (tag 'sync-conflict') so the operator sees it on next session.",inputSchema:{type:"object",properties:{limit:{type:"number",description:"Max conflicts (default 50)"}}},annotations:p.wyrm_sync_conflicts,aliases:[],handler:async(r,a)=>{const{federation:c,sync:o}=a,{limit:t}=r,e=c.unresolvedConflicts(Math.min(Math.max(1,t??50),500));if(e.length===0)return{content:[{type:"text",text:"\u{F115D} No unresolved sync conflicts."}]};const s=e.map(i=>`\u2022 #${i.id} ${i.local_kind}#${i.local_id} from ${i.remote_origin??"?"} at ${i.detected_at}`+(i.quest_id?` \u2192 quest #${i.quest_id}`:"")).join(`
6
6
  `);return{content:[{type:"text",text:`\u{F115D} ${e.length} unresolved conflict(s):
7
- ${a}`}]}}},{name:"wyrm_sync_resolve",description:"Resolve a sync conflict. Mode: 'kept_local' (drop remote), 'kept_remote' (overwrite local with remote payload), 'merged' (apply a hand-merged payload supplied by the caller).",inputSchema:{type:"object",properties:{conflict_id:{type:"number"},mode:{type:"string",enum:["kept_local","kept_remote","merged"]},merged_payload:{type:"object",description:"Required when mode='merged' \u2014 full row payload to write"}},required:["conflict_id","mode"]},annotations:p.wyrm_sync_resolve,aliases:[],handler:async(r,s)=>{const{federation:c}=s,{conflict_id:o,mode:t,merged_payload:e}=r;return t==="merged"&&!e?{content:[{type:"text",text:"merged_payload required for mode=merged"}],isError:!0}:{content:[{type:"text",text:c.resolveConflict(o,t,e)?`\u{F115D} Conflict #${o} resolved (${t}).`:`\u{F115D} Conflict #${o} not found or already resolved.`}]}}},{name:"wyrm_act",description:"Use to pursue a goal autonomously - the OODA loop: observe Wyrm context, orient, decide one whitelisted tool call, act, loop. Pass goal_id for an existing goal, or query+title to create and pursue ad-hoc.",inputSchema:{type:"object",properties:{goal_id:{type:"number"},query:{type:"string",description:"Ad-hoc"},title:{type:"string",description:"Ad-hoc goal"},projectPath:{type:"string"},max_steps:{type:"number",description:"Default 5, cap 50"},model_override:{type:"string",description:"ollama:model"}}},annotations:p.wyrm_act,aliases:[],handler:async(r,s)=>{const{agentLoop:c,db:o,goals:t}=s,e=r;let a=e.goal_id,i=!1;if(!a){if(!e.query)return{content:[{type:"text",text:"Either goal_id or query required"}],isError:!0};const l=e.projectPath?o.getProject(e.projectPath):null;a=t.set({project_id:l?.id??null,title:(e.title??e.query).slice(0,140),description:e.query,priority:"medium",max_iterations:Math.min(50,e.max_steps??5)+2}).id,i=!0}const u=await c.pursue(a,{max_steps:e.max_steps,model_override:e.model_override});if(u.length===0)return{content:[{type:"text",text:`Goal #${a} not active or already at max_iterations.`}],isError:!0};const n=u[u.length-1],d=[];d.push(`\u{F115D} Agent ran ${u.length} iteration(s) on goal #${a}${i?" (ad-hoc)":""}.`),d.push(`Final outcome: **${n.outcome}** (model: ${n.model}${n.degraded?", DEGRADED":""}, ${n.latency_ms}ms)`),d.push("");for(const l of u)d.push(`### Iter #${l.iteration_num} \u2014 ${l.outcome}`),l.decided.thought&&d.push(`> ${l.decided.thought}`),d.push(`Action: \`${l.decided.action}\``+(l.decided.args?` args=${JSON.stringify(l.decided.args).slice(0,200)}`:"")),d.push(`Result: ${l.action_result.slice(0,400)}`),d.push("");return{content:[{type:"text",text:d.join(`
7
+ ${s}`}]}}},{name:"wyrm_sync_resolve",description:"Resolve a sync conflict. Mode: 'kept_local' (drop remote), 'kept_remote' (overwrite local with remote payload), 'merged' (apply a hand-merged payload supplied by the caller).",inputSchema:{type:"object",properties:{conflict_id:{type:"number"},mode:{type:"string",enum:["kept_local","kept_remote","merged"]},merged_payload:{type:"object",description:"Required when mode='merged': full row payload to write"}},required:["conflict_id","mode"]},annotations:p.wyrm_sync_resolve,aliases:[],handler:async(r,a)=>{const{federation:c}=a,{conflict_id:o,mode:t,merged_payload:e}=r;return t==="merged"&&!e?{content:[{type:"text",text:"merged_payload required for mode=merged"}],isError:!0}:{content:[{type:"text",text:c.resolveConflict(o,t,e)?`\u{F115D} Conflict #${o} resolved (${t}).`:`\u{F115D} Conflict #${o} not found or already resolved.`}]}}},{name:"wyrm_act",description:"Use to pursue a goal autonomously - the OODA loop: observe Wyrm context, orient, decide one whitelisted tool call, act, loop. Pass goal_id for an existing goal, or query+title to create and pursue ad-hoc.",inputSchema:{type:"object",properties:{goal_id:{type:"number"},query:{type:"string",description:"Ad-hoc"},title:{type:"string",description:"Ad-hoc goal"},projectPath:{type:"string"},max_steps:{type:"number",description:"Default 5, cap 50"},model_override:{type:"string",description:"ollama:model"}}},annotations:p.wyrm_act,aliases:[],handler:async(r,a)=>{const{agentLoop:c,db:o,goals:t}=a,e=r;let s=e.goal_id,i=!1;if(!s){if(!e.query)return{content:[{type:"text",text:"Either goal_id or query required"}],isError:!0};const l=e.projectPath?o.getProject(e.projectPath):null;s=t.set({project_id:l?.id??null,title:(e.title??e.query).slice(0,140),description:e.query,priority:"medium",max_iterations:Math.min(50,e.max_steps??5)+2}).id,i=!0}const u=await c.pursue(s,{max_steps:e.max_steps,model_override:e.model_override});if(u.length===0)return{content:[{type:"text",text:`Goal #${s} not active or already at max_iterations.`}],isError:!0};const n=u[u.length-1],d=[];d.push(`\u{F115D} Agent ran ${u.length} iteration(s) on goal #${s}${i?" (ad-hoc)":""}.`),d.push(`Final outcome: **${n.outcome}** (model: ${n.model}${n.degraded?", DEGRADED":""}, ${n.latency_ms}ms)`),d.push("");for(const l of u)d.push(`### Iter #${l.iteration_num}: ${l.outcome}`),l.decided.thought&&d.push(`> ${l.decided.thought}`),d.push(`Action: \`${l.decided.action}\``+(l.decided.args?` args=${JSON.stringify(l.decided.args).slice(0,200)}`:"")),d.push(`Result: ${l.action_result.slice(0,400)}`),d.push("");return{content:[{type:"text",text:d.join(`
8
8
  `)}]}}}];export{w as shareToolSpecs};
@@ -1,4 +1,4 @@
1
- import{TOOL_ANNOTATIONS as v}from"../tool-annotations.js";import{currentReadCacheKey as S}from"./boundary.js";import{cache as h}from"../performance.js";import{sanitizeFtsQuery as O}from"../security.js";import{deploySkill as j,validateAtomicSkill as C}from"../skill-author.js";const R=[{name:"wyrm_skill_create",description:"Author + deploy + register an ATOMIC skill in one clean step. Validates atomicity (one focused capability, discoverable 12\u2013280 char description, proper frontmatter), writes <skillsDir>/<slug>/SKILL.md, and registers it so it's instantly findable via wyrm_skill_search. Refuses to clobber an existing skill unless force=true. Use this to create skills as they're needed instead of leaving them un-deployed.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (slugified to kebab-case)"},description:{type:"string",description:"One-line description, 12\u2013280 chars \u2014 used for discovery/recall ranking"},body:{type:"string",description:"The SKILL.md markdown body (the actual skill content; should be ONE focused capability)"},argumentHint:{type:"string",description:"Optional argument-hint frontmatter (for user-invocable skills)"},userInvocable:{type:"boolean",description:"Whether it's a user-invocable slash command"},category:{type:"string",description:"Optional category"},tags:{type:"array",items:{type:"string"},description:"Optional tags"},force:{type:"boolean",description:"Overwrite an existing skill of the same slug (default false)"},register:{type:"boolean",description:"Register in the skill registry for discovery (default true)"}},required:["name","description","body"]},annotations:v.wyrm_skill_create,aliases:[],handler:async(c,p)=>{const{db:d}=p,n=c||{},o=($=>{const x=$.filter(_=>!n[_]&&n[_]!==0&&n[_]!==!1);return x.length>0?{content:[{type:"text",text:`Missing required arguments: ${x.join(", ")}`}],isError:!0}:null})(["name","description","body"]);if(o)return o;const{name:t,description:s,body:g,argumentHint:m,userInvocable:a,category:y,tags:i,force:e,register:l}=c,k={name:t,description:s,body:g,argumentHint:m,userInvocable:a,category:y,tags:i},u=C(k);if(!u.ok)return{content:[{type:"text",text:`\u274C Skill rejected (not clean/atomic):
1
+ import{TOOL_ANNOTATIONS as v}from"../tool-annotations.js";import{currentReadCacheKey as S}from"./boundary.js";import{cache as h}from"../performance.js";import{sanitizeFtsQuery as O}from"../security.js";import{deploySkill as j,validateAtomicSkill as C}from"../skill-author.js";const R=[{name:"wyrm_skill_create",description:"Author + deploy + register an ATOMIC skill in one clean step. Validates atomicity (one focused capability, discoverable 12\u2013280 char description, proper frontmatter), writes <skillsDir>/<slug>/SKILL.md, and registers it so it's instantly findable via wyrm_skill_search. Refuses to clobber an existing skill unless force=true. Use this to create skills as they're needed instead of leaving them un-deployed.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Skill name (slugified to kebab-case)"},description:{type:"string",description:"One-line description, 12\u2013280 chars, used for discovery/recall ranking"},body:{type:"string",description:"The SKILL.md markdown body (the actual skill content; should be ONE focused capability)"},argumentHint:{type:"string",description:"Optional argument-hint frontmatter (for user-invocable skills)"},userInvocable:{type:"boolean",description:"Whether it's a user-invocable slash command"},category:{type:"string",description:"Optional category"},tags:{type:"array",items:{type:"string"},description:"Optional tags"},force:{type:"boolean",description:"Overwrite an existing skill of the same slug (default false)"},register:{type:"boolean",description:"Register in the skill registry for discovery (default true)"}},required:["name","description","body"]},annotations:v.wyrm_skill_create,aliases:[],handler:async(c,p)=>{const{db:d}=p,n=c||{},o=($=>{const x=$.filter(_=>!n[_]&&n[_]!==0&&n[_]!==!1);return x.length>0?{content:[{type:"text",text:`Missing required arguments: ${x.join(", ")}`}],isError:!0}:null})(["name","description","body"]);if(o)return o;const{name:t,description:s,body:g,argumentHint:m,userInvocable:a,category:y,tags:i,force:e,register:l}=c,k={name:t,description:s,body:g,argumentHint:m,userInvocable:a,category:y,tags:i},u=C(k);if(!u.ok)return{content:[{type:"text",text:`\u274C Skill rejected (not clean/atomic):
2
2
  - ${u.errors.join(`
3
3
  - `)}`}]};let f;try{f=j(k,{force:e})}catch($){return{content:[{type:"text",text:`\u274C Deploy failed: ${$ instanceof Error?$.message:String($)}`}]}}let b=!1;if(l!==!1)try{d.registerSkill(u.slug,s,f.path,y,"Ghost Protocol","1.0.0",(i||[]).join(",")),b=!0}catch{}const w=u.warnings.length?`
4
4
 
@@ -60,7 +60,7 @@ ${r?`Skill "${r}" is not registered (or has no graph).`:"No god-tier skills regi
60
60
 
61
61
  `;if(t==="tree"||t==="both")for(const e of s)i+=y(e,0);if(t==="edges"||t==="both"){i+=`
62
62
  **Edges (${g.length}):**
63
- `;for(const e of g)i+=`- ${e.from} \u2014${e.kind}\u2192 ${e.to}${e.kind==="governs"&&!e.resolved?" (unresolved)":""}
63
+ `;for(const e of g)i+=`- ${e.from} [${e.kind}]\u2192 ${e.to}${e.kind==="governs"&&!e.resolved?" (unresolved)":""}
64
64
  `}return i+=`
65
65
  \`\`\`json
66
66
  ${JSON.stringify({tree:s,edges:g},null,2)}
@@ -1,8 +1,8 @@
1
- import{TOOL_ANNOTATIONS as m}from"../tool-annotations.js";const u=[{name:"wyrm_symbol_index",description:"Scan a project for function/class/type/interface definitions across TS/JS/Python/Rust/Go/PHP/Ruby and index them. Idempotent \u2014 clears the project's symbols before re-indexing.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root to scan"}},required:["projectPath"]},annotations:m.wyrm_symbol_index,aliases:[],handler:async(s,r)=>{const{db:i,symbols:a}=r,{projectPath:e}=s,t=i.getProject(e);if(!t)return{content:[{type:"text",text:"Project not found"}],isError:!0};const o=a.indexProject(t.id,t.path);return{content:[{type:"text",text:`\u{F115D} Indexed ${o.symbols} symbol(s) across ${o.files} file(s) in ${t.name}.`}]}}},{name:"wyrm_symbol_search",description:"Look up a symbol across all (or one) project. Cursor's workspace-symbols stops at the repo boundary; this doesn't.",inputSchema:{type:"object",properties:{symbol:{type:"string",description:"Name to search for"},projectPath:{type:"string",description:"Scope to one project (optional)"},kind:{type:"string",enum:["function","class","type","interface","const","export"],description:"Filter by kind"},language:{type:"string",description:"Filter by language: ts/tsx/js/jsx/py/rs/go/php/rb"},exact:{type:"boolean",description:"Exact match (default: substring)"},limit:{type:"number",description:"Max results (default 50)"}},required:["symbol"]},annotations:m.wyrm_symbol_search,aliases:[],handler:async(s,r)=>{const{db:i,symbols:a}=r,{symbol:e,projectPath:t,kind:o,language:c,exact:l,limit:n}=s,y=t?i.getProject(t):null,d=a.search(e,{projectId:y?.id,kind:o,language:c,exact:l,limit:Math.min(Math.max(1,n??50),500)});if(d.length===0)return{content:[{type:"text",text:`\u{F115D} No symbols match "${e}".`}]};const b=d.map(p=>`\u2022 [${p.language}] ${p.kind} ${p.symbol} \u2014 ${p.file_path}:${p.line}
1
+ import{TOOL_ANNOTATIONS as m}from"../tool-annotations.js";const u=[{name:"wyrm_symbol_index",description:"Scan a project for function/class/type/interface definitions across TS/JS/Python/Rust/Go/PHP/Ruby and index them. Idempotent: clears the project's symbols before re-indexing.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Project root to scan"}},required:["projectPath"]},annotations:m.wyrm_symbol_index,aliases:[],handler:async(s,r)=>{const{db:a,symbols:i}=r,{projectPath:e}=s,t=a.getProject(e);if(!t)return{content:[{type:"text",text:"Project not found"}],isError:!0};const o=i.indexProject(t.id,t.path);return{content:[{type:"text",text:`\u{F115D} Indexed ${o.symbols} symbol(s) across ${o.files} file(s) in ${t.name}.`}]}}},{name:"wyrm_symbol_search",description:"Look up a symbol across all (or one) project. Cursor's workspace-symbols stops at the repo boundary; this doesn't.",inputSchema:{type:"object",properties:{symbol:{type:"string",description:"Name to search for"},projectPath:{type:"string",description:"Scope to one project (optional)"},kind:{type:"string",enum:["function","class","type","interface","const","export"],description:"Filter by kind"},language:{type:"string",description:"Filter by language: ts/tsx/js/jsx/py/rs/go/php/rb"},exact:{type:"boolean",description:"Exact match (default: substring)"},limit:{type:"number",description:"Max results (default 50)"}},required:["symbol"]},annotations:m.wyrm_symbol_search,aliases:[],handler:async(s,r)=>{const{db:a,symbols:i}=r,{symbol:e,projectPath:t,kind:o,language:c,exact:l,limit:n}=s,y=t?a.getProject(t):null,d=i.search(e,{projectId:y?.id,kind:o,language:c,exact:l,limit:Math.min(Math.max(1,n??50),500)});if(d.length===0)return{content:[{type:"text",text:`\u{F115D} No symbols match "${e}".`}]};const b=d.map(p=>`\u2022 [${p.language}] ${p.kind} ${p.symbol} at ${p.file_path}:${p.line}
2
2
  ${p.signature??""}`).join(`
3
3
  `);return{content:[{type:"text",text:`\u{F115D} ${d.length} match(es):
4
- ${b}`}]}}},{name:"wyrm_symbol_callers",description:"Best-effort caller graph \u2014 files in the project (or workspace) whose indexed signature mentions this symbol. Approximate; for a true caller graph use LSP.",inputSchema:{type:"object",properties:{symbol:{type:"string"},projectPath:{type:"string",description:"Scope to one project (optional)"}},required:["symbol"]},annotations:m.wyrm_symbol_callers,aliases:[],handler:async(s,r)=>{const{db:i,symbols:a}=r,{symbol:e,projectPath:t}=s,o=t?i.getProject(t):null,c=a.callers(e,o?.id);if(c.length===0)return{content:[{type:"text",text:`\u{F115D} No indexed callers for "${e}".`}]};const l=c.slice(0,50).map(n=>`\u2022 ${n.file_path}:${n.line} \u2014 ${n.signature?.slice(0,100)??""}`).join(`
4
+ ${b}`}]}}},{name:"wyrm_symbol_callers",description:"Best-effort caller graph: files in the project (or workspace) whose indexed signature mentions this symbol. Approximate; for a true caller graph use LSP.",inputSchema:{type:"object",properties:{symbol:{type:"string"},projectPath:{type:"string",description:"Scope to one project (optional)"}},required:["symbol"]},annotations:m.wyrm_symbol_callers,aliases:[],handler:async(s,r)=>{const{db:a,symbols:i}=r,{symbol:e,projectPath:t}=s,o=t?a.getProject(t):null,c=i.callers(e,o?.id);if(c.length===0)return{content:[{type:"text",text:`\u{F115D} No indexed callers for "${e}".`}]};const l=c.slice(0,50).map(n=>`\u2022 ${n.file_path}:${n.line}: ${n.signature?.slice(0,100)??""}`).join(`
5
5
  `);return{content:[{type:"text",text:`\u{F115D} ${c.length} caller candidate(s) (showing first 50):
6
- ${l}`}]}}},{name:"wyrm_symbol_stats",description:"Symbol-index stats \u2014 total count, per-language and per-kind breakdown. Project-scoped if projectPath given, else workspace-wide.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Scope to one project (optional)"}}},annotations:m.wyrm_symbol_stats,aliases:[],handler:async(s,r)=>{const{db:i,symbols:a}=r,{projectPath:e}=s,t=e?i.getProject(e):null,o=a.stats(t?.id),c=Object.entries(o.by_language).map(([n,y])=>`${n}: ${y}`).join(", "),l=Object.entries(o.by_kind).map(([n,y])=>`${n}: ${y}`).join(", ");return{content:[{type:"text",text:`\u{F115D} Symbols indexed: ${o.total}
7
- Languages: ${c||"\u2014"}
8
- Kinds: ${l||"\u2014"}`}]}}}];export{u as symbolsToolSpecs};
6
+ ${l}`}]}}},{name:"wyrm_symbol_stats",description:"Symbol-index stats: total count, per-language and per-kind breakdown. Project-scoped if projectPath given, else workspace-wide.",inputSchema:{type:"object",properties:{projectPath:{type:"string",description:"Scope to one project (optional)"}}},annotations:m.wyrm_symbol_stats,aliases:[],handler:async(s,r)=>{const{db:a,symbols:i}=r,{projectPath:e}=s,t=e?a.getProject(e):null,o=i.stats(t?.id),c=Object.entries(o.by_language).map(([n,y])=>`${n}: ${y}`).join(", "),l=Object.entries(o.by_kind).map(([n,y])=>`${n}: ${y}`).join(", ");return{content:[{type:"text",text:`\u{F115D} Symbols indexed: ${o.total}
7
+ Languages: ${c||"none"}
8
+ Kinds: ${l||"none"}`}]}}}];export{u as symbolsToolSpecs};
@@ -1,4 +1,4 @@
1
- import{TOOL_ANNOTATIONS as I}from"../tool-annotations.js";import{logger as O}from"../logger.js";import{asInt as U}from"../validate.js";import{createCipheriv as H,createDecipheriv as q,pbkdf2Sync as C,randomBytes as j}from"crypto";import{chmodSync as J,copyFileSync as M,existsSync as w,readFileSync as k,unlinkSync as p,writeFileSync as N}from"fs";import{homedir as T}from"os";import{join as D}from"path";import{resolveUserPath as Y,userPathRoots as W,SecurityError as B}from"../security.js";const rt=[{name:"wyrm_prune",description:"Prune low-confidence, stale memory artifacts. Dry-run by default \u2014 use dry_run:false with confirm_ids to actually delete. Never deletes artifacts in the review queue.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID (optional, all projects if omitted)"},min_confidence:{type:"number",description:"Prune artifacts below this confidence (default: 0.3)"},older_than_days:{type:"number",description:"Last accessed more than N days ago (default: 90)"},types:{type:"array",items:{type:"string"},description:"Filter by artifact_type (optional)"},dry_run:{type:"boolean",description:"If true (default), return candidates without deleting. Set false to delete."},confirm_ids:{type:"array",items:{type:"number"},description:"Required when dry_run:false \u2014 only IDs in this list will be deleted"}}},annotations:I.wyrm_prune,aliases:[],handler:async(u,h)=>{const{db:d}=h,R=u||{},{project_id:_,min_confidence:S=.3,older_than_days:s=90,types:o,dry_run:g=!0,confirm_ids:e}=u,t=d.getDatabase(),n=U("project_id",_),m=n!==void 0?"AND project_id = ?":"",l=n!==void 0?[n]:[],f=o&&o.length>0?`AND kind IN (${o.map(()=>"?").join(",")})`:"",x=o??[],c=t.prepare(`
1
+ import{TOOL_ANNOTATIONS as I}from"../tool-annotations.js";import{logger as O}from"../logger.js";import{asInt as U}from"../validate.js";import{createCipheriv as H,createDecipheriv as q,pbkdf2Sync as C,randomBytes as j}from"crypto";import{chmodSync as J,copyFileSync as M,existsSync as w,readFileSync as k,unlinkSync as p,writeFileSync as N}from"fs";import{homedir as T}from"os";import{join as D}from"path";import{resolveUserPath as Y,userPathRoots as W,SecurityError as B}from"../security.js";const rt=[{name:"wyrm_prune",description:"Prune low-confidence, stale memory artifacts. Dry-run by default; use dry_run:false with confirm_ids to actually delete. Never deletes artifacts in the review queue.",inputSchema:{type:"object",properties:{project_id:{type:"number",description:"Project ID (optional, all projects if omitted)"},min_confidence:{type:"number",description:"Prune artifacts below this confidence (default: 0.3)"},older_than_days:{type:"number",description:"Last accessed more than N days ago (default: 90)"},types:{type:"array",items:{type:"string"},description:"Filter by artifact_type (optional)"},dry_run:{type:"boolean",description:"If true (default), return candidates without deleting. Set false to delete."},confirm_ids:{type:"array",items:{type:"number"},description:"Required when dry_run:false; only IDs in this list will be deleted"}}},annotations:I.wyrm_prune,aliases:[],handler:async(u,h)=>{const{db:d}=h,R=u||{},{project_id:_,min_confidence:S=.3,older_than_days:s=90,types:o,dry_run:g=!0,confirm_ids:e}=u,t=d.getDatabase(),n=U("project_id",_),m=n!==void 0?"AND project_id = ?":"",l=n!==void 0?[n]:[],f=o&&o.length>0?`AND kind IN (${o.map(()=>"?").join(",")})`:"",x=o??[],c=t.prepare(`
2
2
  SELECT id, project_id, kind, problem, confidence, last_accessed_at, access_count
3
3
  FROM memory_artifacts
4
4
  WHERE confidence < ?
@@ -9,4 +9,4 @@ import{TOOL_ANNOTATIONS as I}from"../tool-annotations.js";import{logger as O}fro
9
9
  ${f}
10
10
  ORDER BY confidence ASC, last_accessed_at ASC
11
11
  LIMIT 500
12
- `).all(S,`-${s} days`,...l,...x);if(g)return{content:[{type:"text",text:JSON.stringify({dry_run:!0,count:c.length,candidates:c.map(r=>({id:r.id,kind:r.kind,problem:r.problem.slice(0,100),confidence:r.confidence,last_accessed_at:r.last_accessed_at}))})}]};if(!e||e.length===0)return{content:[{type:"text",text:"\u{F115D} **Prune**: dry_run:false requires confirm_ids \u2014 provide the IDs from a dry-run to confirm deletion."}],isError:!0};const b=new Set(c.map(r=>r.id)),a=e.filter(r=>b.has(r));if(a.length===0)return{content:[{type:"text",text:"\u{F115D} **Prune**: No matching IDs to delete (confirm_ids not in candidate set)."}]};const y=a.map(()=>"?").join(","),i=t.prepare(`DELETE FROM memory_artifacts WHERE id IN (${y}) AND needs_review = 0`).run(...a);return{content:[{type:"text",text:JSON.stringify({deleted:i.changes,ids:a})}]}}},{name:"wyrm_sync_export",description:"Export an AES-256-GCM encrypted snapshot of the Wyrm database for cross-device sync. Passphrase from WYRM_SYNC_PASSPHRASE env var.",inputSchema:{type:"object",properties:{output_path:{type:"string",description:"File path to write the encrypted snapshot"},description:{type:"string",description:"Optional description of this export"}},required:["output_path"]},annotations:I.wyrm_sync_export,aliases:[],handler:async(u,h)=>{const{db:d,server:R}=h,{output_path:_,description:S}=u;let s;try{s=Y(_,W(),"wyrm_sync_export.output_path")}catch(n){if(n instanceof B)return{content:[{type:"text",text:`\u{F115D} **Sync Export** refused: ${n.message}`}],isError:!0};throw n}const o=process.env.WYRM_SYNC_PASSPHRASE;if(!o)return{content:[{type:"text",text:"\u{F115D} **Sync Export**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted export."}],isError:!0};const g=d.getDatabasePath(),e=D(T(),".wyrm"),t=D(e,"wyrm_sync_export_temp.db");try{const n=d.getDatabase();w(t)&&p(t),n.prepare("VACUUM INTO ?").run(t);const m=k(t),l=j(32),f=j(16),x=C(o,l,6e5,32,"sha256"),c=H("aes-256-gcm",x,f),b=Buffer.concat([c.update(m),c.final()]),a=c.getAuthTag(),y=Buffer.from("WYRM"),i=Buffer.alloc(1);i.writeUInt8(1,0);const r=Buffer.concat([y,i,l,f,a,b]);N(s,r);try{J(s,384)}catch{}try{p(t)}catch{}const P=(r.length/(1024*1024)).toFixed(2);return{content:[{type:"text",text:JSON.stringify({success:!0,path:s,size_mb:parseFloat(P),exported_at:new Date().toISOString(),description:S??null})}]}}catch(n){try{w(t)&&p(t)}catch{}return O.error("Sync export failed",{error:n.message}),{content:[{type:"text",text:"\u{F115D} **Sync Export** failed. Check server logs for details."}],isError:!0}}}},{name:"wyrm_sync_import",description:"Import an encrypted Wyrm snapshot. Use restore_mode:'preview' to inspect without touching the DB; 'restore' to replace current DB (backs up first).",inputSchema:{type:"object",properties:{input_path:{type:"string",description:"Path to the encrypted snapshot file"},restore_mode:{type:"string",enum:["preview","restore"],description:"preview = show stats only; restore = replace DB (backs up first)"}},required:["input_path","restore_mode"]},annotations:I.wyrm_sync_import,aliases:[],handler:async(u,h)=>{const{db:d,server:R}=h,{input_path:_,restore_mode:S}=u;let s;try{s=Y(_,W(),"wyrm_sync_import.input_path")}catch(t){if(t instanceof B)return{content:[{type:"text",text:`\u{F115D} **Sync Import** refused: ${t.message}`}],isError:!0};throw t}const o=process.env.WYRM_SYNC_PASSPHRASE;if(!o)return{content:[{type:"text",text:"\u{F115D} **Sync Import**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted import."}],isError:!0};const g=D(T(),".wyrm"),e=D(g,"wyrm_sync_import_temp.db");try{const t=k(s);if(t.subarray(0,4).toString("ascii")!=="WYRM")return{content:[{type:"text",text:"\u{F115D} Invalid Wyrm snapshot file (bad magic bytes)."}],isError:!0};const m=t.readUInt8(4);if(m!==1)return{content:[{type:"text",text:`\u{F115D} Unsupported snapshot version: ${m}`}],isError:!0};const l=t.subarray(5,37),f=t.subarray(37,53),x=t.subarray(53,69),c=t.subarray(69),b=C(o,l,6e5,32,"sha256"),a=q("aes-256-gcm",b,f);a.setAuthTag(x);let y;try{y=Buffer.concat([a.update(c),a.final()])}catch{return{content:[{type:"text",text:"\u{F115D} Decryption failed \u2014 wrong passphrase or corrupted snapshot."}],isError:!0}}if(S==="preview"){w(e)&&p(e),N(e,y);let E={};try{const L=(await import("better-sqlite3")).default,A=new L(e,{readonly:!0}),$=["projects","sessions","ground_truths","memory_artifacts","quests"];for(const v of $)try{const F=A.prepare(`SELECT COUNT(*) as n FROM ${v}`).get();E[v]=F.n}catch{E[v]=0}A.close()}catch{}try{p(e)}catch{}return{content:[{type:"text",text:JSON.stringify({preview:!0,stats:E})}]}}const i=d.getDatabasePath(),r=new Date().toISOString().replace(/[:.]/g,"-"),P=`${i}.backup.${r}`;M(i,P),w(e)&&p(e),N(e,y),d.getDatabase().close(),M(e,i);try{p(e)}catch{}return{content:[{type:"text",text:JSON.stringify({success:!0,restored_from:s,backup_at:P})}]}}catch(t){try{w(e)&&p(e)}catch{}return O.error("Sync import failed",{error:t.message}),{content:[{type:"text",text:"\u{F115D} **Sync Import** failed. Check server logs for details."}],isError:!0}}}}];export{rt as syncopsToolSpecs};
12
+ `).all(S,`-${s} days`,...l,...x);if(g)return{content:[{type:"text",text:JSON.stringify({dry_run:!0,count:c.length,candidates:c.map(r=>({id:r.id,kind:r.kind,problem:r.problem.slice(0,100),confidence:r.confidence,last_accessed_at:r.last_accessed_at}))})}]};if(!e||e.length===0)return{content:[{type:"text",text:"\u{F115D} **Prune**: dry_run:false requires confirm_ids; provide the IDs from a dry-run to confirm deletion."}],isError:!0};const b=new Set(c.map(r=>r.id)),a=e.filter(r=>b.has(r));if(a.length===0)return{content:[{type:"text",text:"\u{F115D} **Prune**: No matching IDs to delete (confirm_ids not in candidate set)."}]};const y=a.map(()=>"?").join(","),i=t.prepare(`DELETE FROM memory_artifacts WHERE id IN (${y}) AND needs_review = 0`).run(...a);return{content:[{type:"text",text:JSON.stringify({deleted:i.changes,ids:a})}]}}},{name:"wyrm_sync_export",description:"Export an AES-256-GCM encrypted snapshot of the Wyrm database for cross-device sync. Passphrase from WYRM_SYNC_PASSPHRASE env var.",inputSchema:{type:"object",properties:{output_path:{type:"string",description:"File path to write the encrypted snapshot"},description:{type:"string",description:"Optional description of this export"}},required:["output_path"]},annotations:I.wyrm_sync_export,aliases:[],handler:async(u,h)=>{const{db:d,server:R}=h,{output_path:_,description:S}=u;let s;try{s=Y(_,W(),"wyrm_sync_export.output_path")}catch(n){if(n instanceof B)return{content:[{type:"text",text:`\u{F115D} **Sync Export** refused: ${n.message}`}],isError:!0};throw n}const o=process.env.WYRM_SYNC_PASSPHRASE;if(!o)return{content:[{type:"text",text:"\u{F115D} **Sync Export**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted export."}],isError:!0};const g=d.getDatabasePath(),e=D(T(),".wyrm"),t=D(e,"wyrm_sync_export_temp.db");try{const n=d.getDatabase();w(t)&&p(t),n.prepare("VACUUM INTO ?").run(t);const m=k(t),l=j(32),f=j(16),x=C(o,l,6e5,32,"sha256"),c=H("aes-256-gcm",x,f),b=Buffer.concat([c.update(m),c.final()]),a=c.getAuthTag(),y=Buffer.from("WYRM"),i=Buffer.alloc(1);i.writeUInt8(1,0);const r=Buffer.concat([y,i,l,f,a,b]);N(s,r);try{J(s,384)}catch{}try{p(t)}catch{}const P=(r.length/(1024*1024)).toFixed(2);return{content:[{type:"text",text:JSON.stringify({success:!0,path:s,size_mb:parseFloat(P),exported_at:new Date().toISOString(),description:S??null})}]}}catch(n){try{w(t)&&p(t)}catch{}return O.error("Sync export failed",{error:n.message}),{content:[{type:"text",text:"\u{F115D} **Sync Export** failed. Check server logs for details."}],isError:!0}}}},{name:"wyrm_sync_import",description:"Import an encrypted Wyrm snapshot. Use restore_mode:'preview' to inspect without touching the DB; 'restore' to replace current DB (backs up first).",inputSchema:{type:"object",properties:{input_path:{type:"string",description:"Path to the encrypted snapshot file"},restore_mode:{type:"string",enum:["preview","restore"],description:"preview = show stats only; restore = replace DB (backs up first)"}},required:["input_path","restore_mode"]},annotations:I.wyrm_sync_import,aliases:[],handler:async(u,h)=>{const{db:d,server:R}=h,{input_path:_,restore_mode:S}=u;let s;try{s=Y(_,W(),"wyrm_sync_import.input_path")}catch(t){if(t instanceof B)return{content:[{type:"text",text:`\u{F115D} **Sync Import** refused: ${t.message}`}],isError:!0};throw t}const o=process.env.WYRM_SYNC_PASSPHRASE;if(!o)return{content:[{type:"text",text:"\u{F115D} **Sync Import**: Set WYRM_SYNC_PASSPHRASE env var to enable encrypted import."}],isError:!0};const g=D(T(),".wyrm"),e=D(g,"wyrm_sync_import_temp.db");try{const t=k(s);if(t.subarray(0,4).toString("ascii")!=="WYRM")return{content:[{type:"text",text:"\u{F115D} Invalid Wyrm snapshot file (bad magic bytes)."}],isError:!0};const m=t.readUInt8(4);if(m!==1)return{content:[{type:"text",text:`\u{F115D} Unsupported snapshot version: ${m}`}],isError:!0};const l=t.subarray(5,37),f=t.subarray(37,53),x=t.subarray(53,69),c=t.subarray(69),b=C(o,l,6e5,32,"sha256"),a=q("aes-256-gcm",b,f);a.setAuthTag(x);let y;try{y=Buffer.concat([a.update(c),a.final()])}catch{return{content:[{type:"text",text:"\u{F115D} Decryption failed: wrong passphrase or corrupted snapshot."}],isError:!0}}if(S==="preview"){w(e)&&p(e),N(e,y);let E={};try{const L=(await import("better-sqlite3")).default,A=new L(e,{readonly:!0}),$=["projects","sessions","ground_truths","memory_artifacts","quests"];for(const v of $)try{const F=A.prepare(`SELECT COUNT(*) as n FROM ${v}`).get();E[v]=F.n}catch{E[v]=0}A.close()}catch{}try{p(e)}catch{}return{content:[{type:"text",text:JSON.stringify({preview:!0,stats:E})}]}}const i=d.getDatabasePath(),r=new Date().toISOString().replace(/[:.]/g,"-"),P=`${i}.backup.${r}`;M(i,P),w(e)&&p(e),N(e,y),d.getDatabase().close(),M(e,i);try{p(e)}catch{}return{content:[{type:"text",text:JSON.stringify({success:!0,restored_from:s,backup_at:P})}]}}catch(t){try{w(e)&&p(e)}catch{}return O.error("Sync import failed",{error:t.message}),{content:[{type:"text",text:"\u{F115D} **Sync Import** failed. Check server logs for details."}],isError:!0}}}}];export{rt as syncopsToolSpecs};
package/dist/harvest.js CHANGED
@@ -1,5 +1,5 @@
1
- import{execFileSync as m}from"child_process";import{readFileSync as h,existsSync as g}from"fs";import{join as u}from"path";import{createHash as x}from"crypto";const $=["README.md","CLAUDE.md","AGENTS.md","ARCHITECTURE.md"],k=40,p=o=>x("sha1").update(o).digest("hex").slice(0,8);function y(o){const n=[];for(const c of $){const e=u(o,c);if(!g(e))continue;let s;try{s=h(e,"utf-8")}catch{continue}for(const t of s.split(/\n(?=#{1,4}\s)/)){const i=t.match(/^#{1,4}\s+(.+)/);if(!i)continue;const d=/[\x00-\x1f\x7f]/g,r=i[1].replace(/[#*`]/g,"").replace(d,"").trim(),f=t.slice(i[0].length).split(`
2
- `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")&&!a.startsWith("|")&&!a.startsWith("```")).slice(0,3).join(" ").replace(d,"").replace(/\s+/g," ");if(!r||f.length<24)continue;const l=`${r}: ${f}`.slice(0,600);if(n.push({kind:"lesson",text:l,source:`doc:${c}`,sig:`doc:${c}:${p(l)}`,tags:["harvest","doc",c],confidence:.6}),n.length>=k)return n}}return n}const S=/^(merge|wip|typo|fixup|squash|amend|\.+|chore: lint|format)\b/i;function C(o,n=30){if(!g(u(o,".git")))return[];let c;try{c=m("git",["-C",o,"log",`-n${Math.max(1,Math.min(n,200))}`,"--no-merges","--pretty=%h%x09%s"],{encoding:"utf-8",timeout:5e3})}catch{return[]}const e=[];for(const s of c.split(`
3
- `)){const t=s.trim();if(!t)continue;const i=t.indexOf(" ");if(i<0)continue;const d=t.slice(0,i),r=t.slice(i+1).trim();r.length<8||S.test(r)||e.push({kind:"pattern",text:r.slice(0,300),source:"git",sig:`git:${d}`,tags:["harvest","git",`sha:${d}`],confidence:.45})}return e}function v(o){const n=[],c=u(o,"package.json");if(g(c))try{const e=JSON.parse(h(c,"utf-8")),s=Object.keys({...e.dependencies,...e.devDependencies}).slice(0,16),t=Object.keys(e.scripts??{}).slice(0,10);if(s.length||t.length){const i=`Stack (package.json): ${s.join(", ")||"\u2014"}${t.length?` \xB7 scripts: ${t.join(", ")}`:""}`.slice(0,600);n.push({kind:"lesson",text:i,source:"code:package.json",sig:`code:pkg:${p(i)}`,tags:["harvest","code","stack"],confidence:.6})}}catch{}for(const e of["Cargo.toml","composer.json","requirements.txt","pyproject.toml","go.mod","wrangler.toml"]){const s=u(o,e);if(g(s))try{const t=h(s,"utf-8").split(`
4
- `).map(i=>i.trim()).filter(Boolean).slice(0,6).join(" ").replace(/\s+/g," ").slice(0,400);t&&n.push({kind:"lesson",text:`Stack (${e}): ${t}`,source:`code:${e}`,sig:`code:${e}:${p(t)}`,tags:["harvest","code","stack"],confidence:.55})}catch{}}if(g(u(o,".git")))try{const e=m("git",["-C",o,"grep","-nIE","\\b(TODO|FIXME|HACK|XXX)\\b","--","*.ts","*.tsx","*.js","*.py","*.rs","*.go","*.php","*.rb"],{encoding:"utf-8",timeout:6e3,maxBuffer:4194304}),s=/[\x00-\x1f\x7f]/g;let t=0;for(const i of e.split(`
5
- `)){if(t>=25)break;const d=i.trim();if(!d)continue;const r=d.match(/^([^:]+):(\d+):(.*)$/);if(!r)continue;const f=r[3].trim().replace(s,"").slice(0,200);if(f.length<8)continue;const l=`${r[1]}:${r[2]} \u2014 ${f}`;n.push({kind:"anti_pattern",text:l,source:"code:todo",sig:`code:todo:${r[1]}:${r[2]}:${p(f)}`,tags:["harvest","code","todo"],confidence:.4}),t++}}catch{}return n}function b(o,n,c={}){const e=y(n.path),s=C(n.path,c.gitLimit??30),t=c.includeCode?v(n.path):[],i=[...e,...s,...t];let d=0,r=0;const f=[],l=new Set;for(const a of i){if(l.has(a.sig)||o.existsBySig(n.id,a.sig)){r++,c.dryRun||o.onSkip?.(n.id,a);continue}l.add(a.sig),c.dryRun||o.addCandidate(n.id,a),d++,f.length<5&&f.push(`[${a.source}] ${a.text.slice(0,80)}`)}return{project:n.name,docFacts:e.length,commits:s.length,codeSignals:t.length,added:d,skipped:r,sample:f}}function j(o,n,c={}){const e=n.map(s=>b(o,s,c));return{reports:e,totalAdded:e.reduce((s,t)=>s+t.added,0),totalSkipped:e.reduce((s,t)=>s+t.skipped,0)}}export{v as harvestCodeSignals,y as harvestDocFacts,C as harvestGitCommits,b as harvestProject,j as harvestProjects};
1
+ import{execFileSync as m}from"child_process";import{readFileSync as h,existsSync as g}from"fs";import{join as u}from"path";import{createHash as x}from"crypto";import{runAsWriter as $}from"./handlers/boundary.js";const k=["README.md","CLAUDE.md","AGENTS.md","ARCHITECTURE.md"],y=40,p=o=>x("sha1").update(o).digest("hex").slice(0,8);function S(o){const n=[];for(const i of k){const e=u(o,i);if(!g(e))continue;let s;try{s=h(e,"utf-8")}catch{continue}for(const t of s.split(/\n(?=#{1,4}\s)/)){const c=t.match(/^#{1,4}\s+(.+)/);if(!c)continue;const d=/[\x00-\x1f\x7f]/g,r=c[1].replace(/[#*`]/g,"").replace(d,"").trim(),f=t.slice(c[0].length).split(`
2
+ `).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")&&!a.startsWith("|")&&!a.startsWith("```")).slice(0,3).join(" ").replace(d,"").replace(/\s+/g," ");if(!r||f.length<24)continue;const l=`${r}: ${f}`.slice(0,600);if(n.push({kind:"lesson",text:l,source:`doc:${i}`,sig:`doc:${i}:${p(l)}`,tags:["harvest","doc",i],confidence:.6}),n.length>=y)return n}}return n}const C=/^(merge|wip|typo|fixup|squash|amend|\.+|chore: lint|format)\b/i;function v(o,n=30){if(!g(u(o,".git")))return[];let i;try{i=m("git",["-C",o,"log",`-n${Math.max(1,Math.min(n,200))}`,"--no-merges","--pretty=%h%x09%s"],{encoding:"utf-8",timeout:5e3})}catch{return[]}const e=[];for(const s of i.split(`
3
+ `)){const t=s.trim();if(!t)continue;const c=t.indexOf(" ");if(c<0)continue;const d=t.slice(0,c),r=t.slice(c+1).trim();r.length<8||C.test(r)||e.push({kind:"pattern",text:r.slice(0,300),source:"git",sig:`git:${d}`,tags:["harvest","git",`sha:${d}`],confidence:.45})}return e}function b(o){const n=[],i=u(o,"package.json");if(g(i))try{const e=JSON.parse(h(i,"utf-8")),s=Object.keys({...e.dependencies,...e.devDependencies}).slice(0,16),t=Object.keys(e.scripts??{}).slice(0,10);if(s.length||t.length){const c=`Stack (package.json): ${s.join(", ")||"\u2014"}${t.length?` \xB7 scripts: ${t.join(", ")}`:""}`.slice(0,600);n.push({kind:"lesson",text:c,source:"code:package.json",sig:`code:pkg:${p(c)}`,tags:["harvest","code","stack"],confidence:.6})}}catch{}for(const e of["Cargo.toml","composer.json","requirements.txt","pyproject.toml","go.mod","wrangler.toml"]){const s=u(o,e);if(g(s))try{const t=h(s,"utf-8").split(`
4
+ `).map(c=>c.trim()).filter(Boolean).slice(0,6).join(" ").replace(/\s+/g," ").slice(0,400);t&&n.push({kind:"lesson",text:`Stack (${e}): ${t}`,source:`code:${e}`,sig:`code:${e}:${p(t)}`,tags:["harvest","code","stack"],confidence:.55})}catch{}}if(g(u(o,".git")))try{const e=m("git",["-C",o,"grep","-nIE","\\b(TODO|FIXME|HACK|XXX)\\b","--","*.ts","*.tsx","*.js","*.py","*.rs","*.go","*.php","*.rb"],{encoding:"utf-8",timeout:6e3,maxBuffer:4194304}),s=/[\x00-\x1f\x7f]/g;let t=0;for(const c of e.split(`
5
+ `)){if(t>=25)break;const d=c.trim();if(!d)continue;const r=d.match(/^([^:]+):(\d+):(.*)$/);if(!r)continue;const f=r[3].trim().replace(s,"").slice(0,200);if(f.length<8)continue;const l=`${r[1]}:${r[2]}: ${f}`;n.push({kind:"anti_pattern",text:l,source:"code:todo",sig:`code:todo:${r[1]}:${r[2]}:${p(f)}`,tags:["harvest","code","todo"],confidence:.4}),t++}}catch{}return n}function A(o,n,i={}){const e=S(n.path),s=v(n.path,i.gitLimit??30),t=i.includeCode?b(n.path):[],c=[...e,...s,...t];let d=0,r=0;const f=[],l=new Set;for(const a of c){if(l.has(a.sig)||o.existsBySig(n.id,a.sig)){r++,i.dryRun||o.onSkip?.(n.id,a);continue}l.add(a.sig),i.dryRun||$("harvest",()=>o.addCandidate(n.id,a)),d++,f.length<5&&f.push(`[${a.source}] ${a.text.slice(0,80)}`)}return{project:n.name,docFacts:e.length,commits:s.length,codeSignals:t.length,added:d,skipped:r,sample:f}}function F(o,n,i={}){const e=n.map(s=>A(o,s,i));return{reports:e,totalAdded:e.reduce((s,t)=>s+t.added,0),totalSkipped:e.reduce((s,t)=>s+t.skipped,0)}}export{b as harvestCodeSignals,S as harvestDocFacts,v as harvestGitCommits,A as harvestProject,F as harvestProjects};
package/dist/hours.js CHANGED
@@ -9,5 +9,5 @@ class j{db;constructor(e){this.db=e}report(e){const r=e.default_session_hours??1
9
9
  `).map(o=>` ${o}`).join(`
10
10
  `)),e.business_contact&&s.push(` ${e.business_contact}`),s.push("")),s.push(`**To:** ${e.client_name}`),e.client_address&&s.push(e.client_address.split(`
11
11
  `).map(o=>` ${o}`).join(`
12
- `)),s.push(""),s.push(`**Period:** ${r.range.start} \u2192 ${r.range.end}`),s.push(`**Invoice Date:** ${new Date().toISOString().slice(0,10)}`),s.push(`**Rate:** ${a} ${c.toFixed(2)}/hour`),s.push(""),s.push("## Line Items"),s.push(""),s.push("| Date | Project | Hours | Summary |"),s.push("|------|---------|-------|---------|");for(const o of r.entries){const t=(o.summary??"").replace(/\|/g,"\\|").replace(/\n/g," ").slice(0,80),n=o.is_estimated?" *(est.)*":"";s.push(`| ${o.date} | ${o.project_name} | ${o.hours.toFixed(2)}${n} | ${t} |`)}s.push(""),s.push("## Totals"),s.push(""),s.push("| Project | Sessions | Hours |"),s.push("|---------|----------|-------|");for(const o of r.by_project)s.push(`| ${o.project_name} | ${o.session_count} | ${o.hours.toFixed(2)} |`);return s.push(""),s.push(`**Total Hours:** ${r.total_hours.toFixed(2)}`),s.push(`**Subtotal:** ${a} ${i.toFixed(2)}`),s.push(`**Total Due:** ${a} ${u.toFixed(2)}`),s.push(""),r.estimated_sessions>0&&(s.push(`> \u2139\uFE0F ${r.estimated_sessions} session(s) had no recorded work content \u2014 hours estimated at ${e.default_session_hours??1}h default. Review marked rows.`),s.push("")),e.notes&&(s.push("## Notes"),s.push(""),s.push(e.notes),s.push("")),s.join(`
12
+ `)),s.push(""),s.push(`**Period:** ${r.range.start} \u2192 ${r.range.end}`),s.push(`**Invoice Date:** ${new Date().toISOString().slice(0,10)}`),s.push(`**Rate:** ${a} ${c.toFixed(2)}/hour`),s.push(""),s.push("## Line Items"),s.push(""),s.push("| Date | Project | Hours | Summary |"),s.push("|------|---------|-------|---------|");for(const o of r.entries){const t=(o.summary??"").replace(/\|/g,"\\|").replace(/\n/g," ").slice(0,80),n=o.is_estimated?" *(est.)*":"";s.push(`| ${o.date} | ${o.project_name} | ${o.hours.toFixed(2)}${n} | ${t} |`)}s.push(""),s.push("## Totals"),s.push(""),s.push("| Project | Sessions | Hours |"),s.push("|---------|----------|-------|");for(const o of r.by_project)s.push(`| ${o.project_name} | ${o.session_count} | ${o.hours.toFixed(2)} |`);return s.push(""),s.push(`**Total Hours:** ${r.total_hours.toFixed(2)}`),s.push(`**Subtotal:** ${a} ${i.toFixed(2)}`),s.push(`**Total Due:** ${a} ${u.toFixed(2)}`),s.push(""),r.estimated_sessions>0&&(s.push(`> \u2139\uFE0F ${r.estimated_sessions} session(s) had no recorded work content; hours estimated at ${e.default_session_hours??1}h default. Review marked rows.`),s.push("")),e.notes&&(s.push("## Notes"),s.push(""),s.push(e.notes),s.push("")),s.join(`
13
13
  `)}}export{j as HourLedger};
package/dist/http-auth.js CHANGED
@@ -1,3 +1,3 @@
1
- import{existsSync as O,readFileSync as R,writeFileSync as l,mkdirSync as I}from"fs";import{join as S}from"path";import{homedir as N}from"os";import{randomBytes as p,createHash as T,timingSafeEqual as M}from"crypto";import{WyrmLogger as P}from"./logger.js";const A=S(N(),".wyrm"),a=S(A,"http-config.json"),L={enabled:!0,requests:100,windowMs:6e4},x=["http://localhost:3333","http://127.0.0.1:3333"],u=new Map;let c=null;const i=new P;function y(e){return T("sha256").update(e).digest("hex")}function k(e,t){if(e.length!==t.length)return!1;let r=0;for(let o=0;o<e.length;o++)r|=e.charCodeAt(o)^t.charCodeAt(o);return r===0}function H(){if(O(A)||I(A,{recursive:!0,mode:448}),O(a))try{const r=JSON.parse(R(a,"utf-8"));return{apiKeyHash:r.apiKeyHash,allowedOrigins:r.allowedOrigins||x,rateLimit:{...L,...r.rateLimit},requireAuth:r.requireAuth??!0,devMode:process.env.WYRM_DEV==="true"}}catch(r){i.error("Failed to load HTTP config, regenerating",{error:r.message})}const e=p(32).toString("hex");i.info("Generated new API key for HTTP server"),console.log(`
1
+ import{existsSync as L,readFileSync as N,writeFileSync as v,mkdirSync as K}from"fs";import{join as k}from"path";import{homedir as b}from"os";import{randomBytes as m,createHash as W,timingSafeEqual as E}from"crypto";import{WyrmLogger as D}from"./logger.js";import{DEFAULT_RATE_LIMIT as F,WindowCounter as x,budgetKey as B,failureLimit as _,isLoopbackAddress as U,normalizeRateLimit as S,requestLimit as X}from"./http-rate-limit.js";import{canonicalOrigin as R,configuredOrigins as z,isCrossSiteRequest as Y,ownOrigins as G}from"./write-guard.js";const p=k(b(),".wyrm"),w=k(p,"http-config.json"),J=new x,M=new x;let u=null;const l=new D;function A(e){return W("sha256").update(e).digest("hex")}function V(e,t){if(e.length!==t.length)return!1;let n=0;for(let o=0;o<e.length;o++)n|=e.charCodeAt(o)^t.charCodeAt(o);return n===0}function $(){if(L(p)||K(p,{recursive:!0,mode:448}),L(w))try{const n=JSON.parse(N(w,"utf-8"));return{apiKeyHash:n.apiKeyHash,allowedOrigins:z(n.allowedOrigins),rateLimit:S(n.rateLimit),requireAuth:n.requireAuth??!0,devMode:process.env.WYRM_DEV==="true"}}catch(n){l.error("Failed to load HTTP config, regenerating",{error:n.message})}const e=m(32).toString("hex");l.info("Generated new API key for HTTP server"),console.log(`
2
2
  `+"\u2550".repeat(60)),console.log("\u{1F510} WYRM API KEY (save this securely, shown once):"),console.log(""),console.log(` ${e}`),console.log(""),console.log(" Use with: Authorization: Bearer <key>"),console.log("\u2550".repeat(60)+`
3
- `);const t={apiKeyHash:y(e),allowedOrigins:x,rateLimit:L,requireAuth:!0,devMode:process.env.WYRM_DEV==="true"};return l(a,JSON.stringify(t,null,2),{mode:384}),t}function s(){return c||(c=H()),c}function v(e){const t=s(),r=e.socket.remoteAddress??"",o=r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1",n=(e.headers.host??"").split(":")[0].toLowerCase(),d=n==="localhost"||n==="127.0.0.1"||n==="::1"||n==="[::1]";if(t.devMode&&o&&d)return!0;const h=e.headers["x-wyrm-nonce"];if(o&&d&&typeof h=="string"&&K(h))return!0;if(!t.requireAuth)return i.warn("Authentication disabled - API is open"),!0;const g=e.headers.authorization;if(!g||!g.startsWith("Bearer "))return!1;const m=g.slice(7);if(!m||m.length<32)return!1;const C=y(m);return k(C,t.apiKeyHash)}function D(e){const t=s();if(!t.rateLimit.enabled)return{allowed:!0,remaining:1/0,resetAt:0};const r=e.socket.remoteAddress||"unknown",o=Date.now();if(u.size>1e4)for(const[d,h]of u)h.resetAt<o&&u.delete(d);let n=u.get(r);return(!n||n.resetAt<o)&&(n={count:0,resetAt:o+t.rateLimit.windowMs},u.set(r,n)),n.count++,{allowed:n.count<=t.rateLimit.requests,remaining:Math.max(0,t.rateLimit.requests-n.count),resetAt:n.resetAt}}function E(e){const t=s(),r=e.headers.origin;return r&&t.allowedOrigins.includes(r)?r:t.allowedOrigins[0]}function w(e){return{"Access-Control-Allow-Origin":E(e),"Access-Control-Allow-Methods":"GET, POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Cache-Control":"no-store","Content-Security-Policy":"default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'","Referrer-Policy":"no-referrer","Permissions-Policy":"geolocation=(), microphone=(), camera=()","X-Robots-Tag":"noindex, nofollow"}}const b=["/health","/auth/status","/ui"];function U(e,t){if(e.method==="OPTIONS"){const n=w(e);return t.writeHead(204,n),t.end(),{error:!0}}const r=D(e);if(!r.allowed){const n=w(e);return t.writeHead(429,{...n,"X-RateLimit-Remaining":"0","X-RateLimit-Reset":String(r.resetAt),"Retry-After":String(Math.ceil((r.resetAt-Date.now())/1e3))}),t.end(JSON.stringify({error:"Rate limit exceeded"})),i.warn("Rate limit exceeded",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}const o=new URL(e.url||"/","http://localhost").pathname;if(b.includes(o))return{error:!1};if(!v(e)){const n=w(e);return t.writeHead(401,{...n,"WWW-Authenticate":'Bearer realm="Wyrm API"'}),t.end(JSON.stringify({error:"Unauthorized"})),i.warn("Authentication failed",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}return{error:!1}}function z(){const e=p(32).toString("hex"),t=s();return t.apiKeyHash=y(e),l(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("API key regenerated"),e}function G(e){const t=s();t.allowedOrigins=e,l(a,JSON.stringify(t,null,2),{mode:384}),c=t,i.info("CORS origins updated",{origins:e})}function X(e,t){const r=s();r.rateLimit={enabled:!0,requests:e,windowMs:t},l(a,JSON.stringify(r,null,2),{mode:384}),c=r,i.info("Rate limit updated",{requests:e,windowMs:t})}function Y(e){const t=s();t.requireAuth=e,l(a,JSON.stringify(t,null,2),{mode:384}),c=t,e||i.warn("Authentication disabled - API is now open")}function j(){const e=s();e.devMode=!0}let f=null;function V(){return f===null&&(f=p(32).toString("hex")),f}function K(e){if(f===null)return!1;const t=Buffer.from(e,"utf8"),r=Buffer.from(f,"utf8");return t.length!==r.length?!1:M(t,r)}function $(){const e=s();return{requireAuth:e.requireAuth,devMode:e.devMode,rateLimit:e.rateLimit,allowedOrigins:e.allowedOrigins}}export{U as authMiddleware,v as authenticate,D as checkRateLimit,j as enableDevMode,$ as getAuthStatus,E as getCorsOrigin,w as getSecurityHeaders,V as getUiNonce,z as regenerateApiKey,G as setAllowedOrigins,X as setRateLimit,Y as setRequireAuth};
3
+ `);const t={apiKeyHash:A(e),allowedOrigins:[],rateLimit:{...F},requireAuth:!0,devMode:process.env.WYRM_DEV==="true"};return f(t),t}function f(e){const{allowedOrigins:t,...n}=e,o=t.length>0?{...n,allowedOrigins:t}:n;v(w,JSON.stringify(o,null,2),{mode:384})}function a(){return u||(u=$()),u}function P(e){return{ownOrigins:G(e.socket.localPort,e.socket.localAddress),allowedOrigins:a().allowedOrigins}}function se(e){return T(e)!==null}function T(e){const t=a(),n=U(e.socket.remoteAddress??""),o=(e.headers.host??"").split(":")[0].toLowerCase(),r=o==="localhost"||o==="127.0.0.1"||o==="::1"||o==="[::1]";if(t.devMode&&n&&r)return{kind:"dev"};const i=e.headers["x-wyrm-nonce"];if(n&&r&&typeof i=="string"&&Z(i))return{kind:"ui"};if(!t.requireAuth)return l.warn("Authentication disabled - API is open"),{kind:"open"};const s=e.headers.authorization;if(!s||!s.startsWith("Bearer "))return null;const c=s.slice(7);if(!c||c.length<32)return null;const d=A(c);return V(d,t.apiKeyHash)?{kind:"token",tokenHash:d}:null}function H(e,t,n,o,r){t.writeHead(429,{...y(e),"X-RateLimit-Limit":String(n.limit),"X-RateLimit-Remaining":"0","X-RateLimit-Reset":String(n.resetAt),"Retry-After":String(Math.max(1,Math.ceil((n.resetAt-r)/1e3)))}),t.end(JSON.stringify({error:o})),l.warn(o,{ip:e.socket.remoteAddress,path:e.url})}function I(e,t,n,o,r){const i=a().rateLimit;if(!i.enabled)return!0;const s=e.socket.remoteAddress??"",c=J.hit(B(n,s)+r,X(i,s),i.windowMs,o);return c.allowed?!0:(H(e,t,c,"Rate limit exceeded",o),!1)}function j(e){const t=e.headers.origin,n=Array.isArray(t)?t[0]:t,o=n===void 0?null:R(n);if(o===null)return;const r=P(e);return[...r.ownOrigins,...r.allowedOrigins].some(i=>R(i)===o)?n:void 0}function y(e){const t=j(e);return{...t?{"Access-Control-Allow-Origin":t}:{},Vary:"Origin","Access-Control-Allow-Methods":"GET, POST, OPTIONS","Access-Control-Allow-Headers":"Content-Type, Authorization","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Cache-Control":"no-store","Content-Security-Policy":"default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'","Referrer-Policy":"no-referrer","Permissions-Policy":"geolocation=(), microphone=(), camera=()","X-Robots-Tag":"noindex, nofollow"}}const Q=["/health","/auth/status","/ui"];function ae(e,t){if(e.method==="OPTIONS"){const g=y(e);return t.writeHead(204,g),t.end(),{error:!0}}const n=Date.now(),o=e.socket.remoteAddress??"",r=Y(e.headers,P(e))?"|cross-site":"",i=new URL(e.url||"/","http://localhost").pathname;if(Q.includes(i))return I(e,t,null,n,r)?{error:!1}:{error:!0};const s=a().rateLimit,c=`failed-auth:${o||"unknown"}${r}`,d=_(s,o),O=M.peek(c,d,s.windowMs,n);if(!O.allowed)return H(e,t,O,"Too many failed authentication attempts",n),{error:!0};const C=T(e);if(!C){M.hit(c,d,s.windowMs,n);const g=y(e);return t.writeHead(401,{...g,"WWW-Authenticate":'Bearer realm="Wyrm API"'}),t.end(JSON.stringify({error:"Unauthorized"})),l.warn("Authentication failed",{ip:e.socket.remoteAddress,path:e.url}),{error:!0}}return I(e,t,C,n,r)?{error:!1}:{error:!0}}function ce(){const e=m(32).toString("hex"),t=a();return t.apiKeyHash=A(e),f(t),u=t,l.info("API key regenerated"),e}function le(e){const t=a();t.allowedOrigins=e,f(t),u=t,l.info("CORS origins updated",{origins:e})}function ue(e,t){const n=a();n.rateLimit=S({...n.rateLimit,enabled:!0,requests:e,windowMs:t}),f(n),u=n,l.info("Rate limit updated",{requests:e,windowMs:t})}function de(e){const t=a();t.requireAuth=e,f(t),u=t,e||l.warn("Authentication disabled - API is now open")}function fe(){const e=a();e.devMode=!0}let h=null;function he(){return h===null&&(h=m(32).toString("hex")),h}function Z(e){if(h===null)return!1;const t=Buffer.from(e,"utf8"),n=Buffer.from(h,"utf8");return t.length!==n.length?!1:E(t,n)}function ge(){const e=a();return{requireAuth:e.requireAuth,devMode:e.devMode,rateLimit:e.rateLimit,allowedOrigins:e.allowedOrigins}}export{ae as authMiddleware,se as authenticate,T as authenticateRequest,fe as enableDevMode,ge as getAuthStatus,j as getCorsOrigin,y as getSecurityHeaders,he as getUiNonce,ce as regenerateApiKey,le as setAllowedOrigins,ue as setRateLimit,de as setRequireAuth,P as siteContextFor};