wyrm-mcp 7.3.2 → 7.3.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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,40 +1,652 @@
1
- import{basename as X}from"path";import{createHash as Z}from"crypto";import{blockText as G}from"./types.js";import{TOOL_ANNOTATIONS as T}from"../tool-annotations.js";import{renderResult as x,withGlyph as O,cacheKeyFor as N}from"../render.js";import{computeStaleness as ee}from"../intelligence.js";import{ValidationError as te,asInt as se,asString as ne}from"../validate.js";import{sanitizeActorId as oe}from"./boundary.js";import{makeEstimator as ie}from"../token-budget.js";function J(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)_
2
- `+`_updated ${e.updated_at}${e.updated_by?" \xB7 "+e.updated_by:""} \xB7 ${e.turn_count} turn(s)_`+`
3
-
4
- `+e.thread.slice(-1800)}const re={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 Q(e,t){let n=`### \u{1F4DA} Project Ground Truths _(treat as authoritative \u2014 do not infer or contradict)_
5
- `;const s=new Map;for(const o of e)s.has(o.category)||s.set(o.category,[]),s.get(o.category).push(o);for(const[o,i]of s){n+=`**${re[o]??o}:**
6
- `;for(const c of i){let p="";if(t){const u=ee(c);u!==null&&u>.7&&(p="[\u26A0\uFE0F STALE] ")}n+=`- ${p}**${c.key}:** ${c.value}${c.rationale?` _(${c.rationale})_`:""}
7
- `}}return n}const V=`
1
+ /**
2
+ * Session domain ToolSpec contract v2 (v7 F3 T026, hot-path extraction).
3
+ *
4
+ * wyrm_session_start / wyrm_session_update / wyrm_session_rehydrate /
5
+ * wyrm_session_prime, moved VERBATIM from the index.ts dispatch switch +
6
+ * buildAllTools(). The wyrm_session noun shim keeps routing action=start|
7
+ * update|rehydrate onto these names (registry checked before switch — zero
8
+ * shim changes), and wyrm_session_prime stays the first-class survivor.
9
+ *
10
+ * session_prime: the five 6.x sections (truths / scaffold / memory brief /
11
+ * quests / vault advisory) are now the canonical structured body
12
+ * (`sections[]` + the count fields the summary derives from); the text
13
+ * channel is the same joined brief. Section CONTENT is institutional 6.x
14
+ * prose built by the subsystems (incl. their emoji headers) — that is body
15
+ * DATA, not renderer decoration, so it is byte-identical to 6.x; only the
16
+ * summary line's brand glyph moved behind WYRM_FANCY (T019 policy).
17
+ *
18
+ * v7 F3 (T028) — FLEET MODE (spec FR-5, §7 criterion 8): when `run_id` is
19
+ * given, the FIRST prime of a (run_id, role) compiles a byte-stable
20
+ * role-sliced brief under a token budget (default 1,200; approx-4cpt, the
21
+ * token-budget.ts convention) and CAS-caches it in the run_briefs table
22
+ * (migration 23: INSERT OR IGNORE, then EVERY caller reads the row back), so
23
+ * 12 simultaneous primes — across processes — return the byte-identical
24
+ * cached prefix. Nondeterminism sources are pinned at compile time:
25
+ * - NO staleness prefixes (computeStaleness reads Date.now());
26
+ * - NO vault advisory section (vaultAdvisory reads host vault state);
27
+ * - quest ordering carries an explicit `id` tiebreak;
28
+ * - truth ordering is getCurrent's ORDER BY category, key (already stable).
29
+ * Sections beyond the budget are STUBBED as plain-text `wyrm://` references —
30
+ * the T034 resource layer will turn those into real resource links (the seam
31
+ * is the STUB_REFS map below); 7.0 deliberately ships them as text only.
32
+ *
33
+ * FEATHERWEIGHT opt #2 — DISTRIBUTE-ONCE (`for_spawn:true`, advances T038 in
34
+ * spirit: the role brief is RENDERED ONCE for OUT-OF-BAND delivery instead of
35
+ * being re-sent to every agent over the wire). The orchestrator primes one
36
+ * (run_id, role) with for_spawn:true and gets the brief back as clean
37
+ * embeddable markdown (`brief`, byte-EQUAL to the agent-side prime's
38
+ * content[0].text — same CAS compile, same primeTemplate render, just packaged
39
+ * for embedding) + a `brief_hash` + the (run_id, role) cache key. It embeds
40
+ * that brief in the spawn-prompt PREFIX of every same-role agent (where they
41
+ * genuinely share a cacheable prefix), so those agents never call prime — the
42
+ * cacheable share of the fleet's prime cost becomes NEVER-SENT bytes. A
43
+ * forgetful orchestrator that skips for_spawn degrades to today's per-agent
44
+ * prime against the SAME CAS row (the byte-stable fallback) — never below the
45
+ * S3 floor. for_spawn is purely additive: same compile, alternate envelope.
46
+ *
47
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
48
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
49
+ */
50
+ import { basename } from 'path';
51
+ import { createHash } from 'crypto';
52
+ import { blockText } from './types.js';
53
+ import { TOOL_ANNOTATIONS } from '../tool-annotations.js';
54
+ import { renderResult, withGlyph, cacheKeyFor } from '../render.js';
55
+ import { computeStaleness } from '../intelligence.js';
56
+ import { ValidationError, asInt, asString } from '../validate.js';
57
+ import { sanitizeActorId } from './boundary.js';
58
+ import { makeEstimator } from '../token-budget.js';
59
+ /**
60
+ * The Continuity section — the living thread ("where we left off / what we were working on and
61
+ * thinking about"), restored FIRST so any fresh chat or surface (phone, Hermes, Ember, Gemini)
62
+ * re-opens warm. Returns null when there's no thread yet.
63
+ */
64
+ function buildContinuitySectionText(c) {
65
+ if (!c || !c.thread.trim())
66
+ return null;
67
+ const meta = `_updated ${c.updated_at}${c.updated_by ? ' · ' + c.updated_by : ''} · ${c.turn_count} turn(s)_`;
68
+ return ('### 🧵 Where We Left Off _(the living thread — what we were working on and thinking about)_\n' +
69
+ meta +
70
+ '\n\n' +
71
+ c.thread.slice(-1800));
72
+ }
73
+ const CATEGORY_LABELS_SP = {
74
+ tech_stack: '🛠️ Tech Stack', architecture: '🏗️ Architecture',
75
+ constraint: '🚧 Constraints', decision: '📋 Decisions',
76
+ contact: '👤 Contacts', other: '📌 Other Facts',
77
+ };
78
+ /** Section 1 — ground truths. `includeStaleness:false` is the T028 fleet pin:
79
+ * computeStaleness() reads Date.now(), which would make the compiled brief
80
+ * time-dependent. Output is byte-identical to 6.x when true. */
81
+ function buildTruthsSectionText(currentTruths, includeStaleness) {
82
+ let spTruthsText = '### 📚 Project Ground Truths _(treat as authoritative — do not infer or contradict)_\n';
83
+ const spByCategory = new Map();
84
+ for (const t of currentTruths) {
85
+ if (!spByCategory.has(t.category))
86
+ spByCategory.set(t.category, []);
87
+ spByCategory.get(t.category).push(t);
88
+ }
89
+ for (const [cat, items] of spByCategory) {
90
+ spTruthsText += `**${CATEGORY_LABELS_SP[cat] ?? cat}:**\n`;
91
+ for (const t of items) {
92
+ let stalePrefix = '';
93
+ if (includeStaleness) {
94
+ const staleness = computeStaleness(t);
95
+ if (staleness !== null && staleness > 0.7)
96
+ stalePrefix = '[⚠️ STALE] ';
97
+ }
98
+ spTruthsText += `- ${stalePrefix}**${t.key}:** ${t.value}${t.rationale ? ` _(${t.rationale})_` : ''}\n`;
99
+ }
100
+ }
101
+ return spTruthsText;
102
+ }
103
+ // T028: explicit `id` tiebreak appended to the 6.x priority ordering — same
104
+ // priority buckets, but ties are now deterministic (a fleet-brief pin that is
105
+ // strictly-stable for the default path too; no consumer locks quest order).
106
+ const ACTIVE_QUESTS_SQL = `
8
107
  SELECT * FROM quests
9
108
  WHERE project_id = ? AND status = 'pending'
10
109
  ORDER BY
11
110
  CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END,
12
111
  id
13
112
  LIMIT 5
14
- `;function Y(e){const t={critical:"\u{1F534}",high:"\u{1F7E0}",medium:"\u{1F7E1}",low:"\u{1F7E2}"};let n=`### \u{1F3AF} Active Quests
15
- `;for(const s of e)n+=`- ${t[s.priority]??"\u2022"} #${s.id}: ${s.title}
16
- `;return n}function q(e,t,n,s){return n?e.getProjectById(n):s?t.prepare("SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1").get(`%${s}%`):t.prepare(`
113
+ `;
114
+ /** Section 4 — active quests (byte-identical to the 6.x block). */
115
+ function buildQuestsSectionText(activeQuests) {
116
+ const priorityEmoji = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
117
+ let questsBlock = '### 🎯 Active Quests\n';
118
+ for (const q of activeQuests) {
119
+ questsBlock += `- ${priorityEmoji[q.priority] ?? '•'} #${q.id}: ${q.title}\n`;
120
+ }
121
+ return questsBlock;
122
+ }
123
+ /** The 6.x three-branch project resolution (by_id / by_name / latest-active),
124
+ * shared verbatim by the default and fleet paths. */
125
+ function resolvePrimeProject(store, db, spId, spName) {
126
+ if (spId)
127
+ return store.getProjectById(spId);
128
+ if (spName) {
129
+ return db.prepare('SELECT * FROM projects WHERE LOWER(name) LIKE LOWER(?) LIMIT 1').get(`%${spName}%`);
130
+ }
131
+ return db.prepare(`
17
132
  SELECT p.* FROM projects p
18
133
  JOIN sessions s ON s.project_id = p.id
19
134
  ORDER BY s.created_at DESC LIMIT 1
20
- `).get()}function P(e){const t=e.getAllProjects(20);return t.length===0?{content:[{type:"text",text:"No projects found. Run `wyrm_scan_projects` first."}],isError:!0}:{content:[{type:"text",text:`Project not found. Available projects:
21
- ${t.map(s=>`- ${s.name} (ID: ${s.id})`).join(`
22
- `)}`}],isError:!0}}const K=(e,t)=>{const n=e.session_id!==null?`
23
- Session logged: #${e.session_id}`:"";return O(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"}${n}`)},L=(e,t)=>{const n=K(e,t);return e.sections.length>0?e.sections.map(s=>s.text).join(`
24
-
25
- ---
26
-
27
- `)+`
28
-
29
- `+n:n+`
30
-
31
- _No memory stored yet -- start working and Wyrm will learn._`},A={summary:K};async function D(e,t,n){try{const{logSavings:s}=await import("../statusline.js"),o=G(n.content[0]).length,i=Math.round(o/4),c=e.prepare("SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(t);s(e,"wyrm_session_prime","recovered_context",i,c?.id)}catch{}}const ce={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"}},ae=48,de=1200;async function pe(e,{store:t,raw:n,memory:s,truths:o,scaffolds:i,cache:c}){const{project_id:p,project_name:u,task:a,log_session:f}=e,r=oe(e.run_id);if(r===null)throw new te("run_id",'must be <=64 printable-ASCII chars (no ";")');const m=ne("role",e.role,{maxLen:200})??"",j=se("token_budget",e.token_budget,{min:1})??de,_=n(),$=()=>_.prepare("SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?").get(r,m),y=p||u?q(t,_,p,u):void 0;if((p||u)&&!y)return P(t);let g=$();if(g&&y){const d=JSON.parse(g.body_json);if(d.project_id!==y.id)return{content:[{type:"text",text:`Fleet brief for (run ${r}, role '${m}') is pinned to project '${d.project_name}' (ID: ${d.project_id}) \u2014 refusing to serve it for requested project '${y.name}' (ID: ${y.id}). Use a distinct role for a second project, or prime without run_id.`}],isError:!0}}if(!g){const d=y??q(t,_,p,u);if(!d)return P(t);const l=[],k=J(t.getContinuity(d.id));k&&l.push({kind:"continuity",text:k});const v=o.getCurrent(d.id);v.length>0&&l.push({kind:"truths",text:Q(v,!1).slice(0,1200)});const w=a??(m||void 0);if(w){const S=i.findBest(w,d.id,.3);S&&l.push({kind:"scaffold",text:i.formatForContext(S,1500)})}const B=s.buildContextBrief(d.id,w??"general",{maxItems:10,minConfidence:.3});B.text&&l.push({kind:"memory",text:B.text.slice(0,2e3)});const I=_.prepare(V).all(d.id);I.length>0&&l.push({kind:"quests",text:Y(I).slice(0,800)});const M=ie(),R=[];let C=ae;for(const S of l){const W=M.count(S.text)+2;if(C+W<=j)R.push(S),C+=W;else{const H=ce[S.kind]??{path:S.kind,tool:"wyrm_search"},U=`[beyond token_budget] wyrm://project/${d.id}/${H.path} -- fetch via ${H.tool}`;R.push({kind:S.kind,text:U}),C+=M.count(U)+2}}let F=null;f&&(F=t.createSession(d.id,{objectives:a??"Session started via wyrm_session_prime",summary:""}).id);const z={project_id:d.id,project_name:d.name,truth_count:o.getStats(d.id).current,quest_count:I.length,memory_count:s.getStats(d.id).total,session_id:F,sections:R,run_id:r,role:m,token_budget:j};_.prepare("INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)").run(r,m,JSON.stringify(z),j),g=$()}const h=JSON.parse(g.body_json),b=x(h,L,A);if(e.for_spawn===!0){const d=x(h,L,{...A,exemptStructured:!0}),l=G(d.content[0]),k=Z("sha256").update(l,"utf8").digest("hex"),v={project_id:h.project_id,project_name:h.project_name,session_id:h.session_id??null,sections:h.sections,for_spawn:!0,run_id:r,role:m,brief:l,brief_hash:k,brief_chars:l.length,cache_key:{table:"run_briefs",run_id:r,role:m}};await D(_,h.project_id,d);const w={content:[{type:"text",text:l}],structuredContent:v};return c.set(N("wyrm_session_prime",JSON.stringify(e)),w,3e4),w}return await D(_,h.project_id,b),c.set(N("wyrm_session_prime",JSON.stringify(e)),b,3e4),b}const Se=[{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:T.wyrm_session_start,aliases:[],handler:(e,{store:t})=>{const{projectPath:n,objectives:s}=e;let o=t.getProject(n);o||(o=t.registerProject(X(n),n));let i=t.getTodaySession(o.id);i?s&&(i=t.updateSession(i.id,{objectives:i.objectives?`${i.objectives}
32
- ${s}`:s})):i=t.createSession(o.id,{objectives:s||""}),t.archiveOldSessions(o.id,10);const c={session_id:i.id,project:o.name,date:i.date,objectives:i.objectives||null};return x(c,(p,u)=>O(u.brand,`Session ${p.session_id} for ${p.project}`)+`
33
- **Date:** ${p.date}
34
- **Objectives:** ${p.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:T.wyrm_session_update,aliases:[],handler:(e,{store:t,indexing:n})=>{const{projectPath:s,completed:o,issues:i,commits:c,notes:p}=e,u=t.getProject(s);if(!u)return{content:[{type:"text",text:"Project not found"}],isError:!0};let a=t.getTodaySession(u.id);a||(a=t.createSession(u.id,{}));const f={};o&&(f.completed=a.completed?`${a.completed}
35
- ${o}`:o),i&&(f.issues=a.issues?`${a.issues}
36
- ${i}`:i),c&&(f.commits=a.commits?`${a.commits}
37
- ${c}`:c),p&&(f.notes=a.notes?`${a.notes}
38
- ${p}`:p),a=t.updateSession(a.id,f);const r=[o,i,p].filter(Boolean).join(" \xB7 ");r&&t.updateContinuity(u.id,r,"session_update"),n()?.enqueue("session",a.id,u.id);const m={session_id:a.id,project:u.name,updated:Object.keys(f)};return x(m,(j,_)=>O(_.brand,`Session updated for ${j.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:T.wyrm_session_rehydrate,aliases:[],handler:(e,{rehydration:t})=>{const{session_id:n,include_artifacts:s,include_failures:o,max_truth_chars:i}=e,c=t.rehydrate(n,{include_artifacts:s,include_failures:o,max_truth_chars:i});return c?x({...c},p=>p.briefing_markdown,{exemptStructured:!0}):{content:[{type:"text",text:`Session #${n} 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"},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:T.wyrm_session_prime,aliases:[],handler:async(e,{store:t,raw:n,memory:s,truths:o,scaffolds:i,cache:c})=>{const{project_id:p,project_name:u,task:a,log_session:f}=e;if(e.run_id!==void 0&&e.run_id!==null)return pe(e,{store:t,raw:n,memory:s,truths:o,scaffolds:i,cache:c});const r=q(t,n(),p,u);if(!r)return P(t);const m=[],j=J(t.getContinuity(r.id));j&&m.push({kind:"continuity",text:j});const _=o.getCurrent(r.id);if(_.length>0&&m.push({kind:"truths",text:Q(_,!0).slice(0,1200)}),a){const E=i.findBest(a,r.id,.3);E&&m.push({kind:"scaffold",text:i.formatForContext(E,1500)})}const $=s.buildContextBrief(r.id,a??"general",{maxItems:10,minConfidence:.3});$.text&&m.push({kind:"memory",text:$.text.slice(0,2e3)});const y=n().prepare(V).all(r.id);y.length>0&&m.push({kind:"quests",text:Y(y).slice(0,800)});try{const{vaultAdvisory:E}=await import("../vault.js"),d=E();d.length>0&&m.push({kind:"vault",text:`### \u{1F510} Credential Vault
39
- `+d.map(l=>`- ${l}`).join(`
40
- `)})}catch{}let g=null;f&&(g=t.createSession(r.id,{objectives:a??"Session started via wyrm_session_prime",summary:""}).id);const h={project_id:r.id,project_name:r.name,truth_count:o.getStats(r.id).current,quest_count:y.length,memory_count:s.getStats(r.id).total,session_id:g,sections:m},b=x(h,L,A);return await D(n(),r.id,b),c.set(N("wyrm_session_prime",JSON.stringify(e)),b,3e4),b}}];export{Se as sessionToolSpecs};
135
+ `).get();
136
+ }
137
+ /** The 6.x not-found advisory (isError per the T026 goals precedent). */
138
+ function primeProjectNotFound(store) {
139
+ const allProjects = store.getAllProjects(20);
140
+ if (allProjects.length === 0) {
141
+ return { content: [{ type: "text", text: `No projects found. Run \`wyrm_scan_projects\` first.` }], isError: true };
142
+ }
143
+ const candidates = allProjects.map(p => `- ${p.name} (ID: ${p.id})`).join('\n');
144
+ return { content: [{ type: "text", text: `Project not found. Available projects:\n${candidates}` }], isError: true };
145
+ }
146
+ /** The session_prime summary line the one-line roll-up the WYRM_CHANNEL=
147
+ * structured seam keeps on the text channel; the section bodies (the actual
148
+ * knowledge) ride structuredContent (`body.sections`) unchanged. */
149
+ const primeSummary = (b, g) => {
150
+ const sessionNote = b.session_id !== null ? `\nSession logged: #${b.session_id}` : '';
151
+ return withGlyph(g.brand, `Wyrm primed for **${b.project_name}** | ${b.truth_count} truth${b.truth_count !== 1 ? 's' : ''} - ` +
152
+ `${b.quest_count} quest${b.quest_count !== 1 ? 's' : ''} - ` +
153
+ `${b.memory_count} memor${b.memory_count !== 1 ? 'ies' : 'y'}${sessionNote}`);
154
+ };
155
+ /** The session_prime text template pure (body, glyphs), shared by both
156
+ * paths so cached fleet bodies re-render to the exact same text channel. */
157
+ const primeTemplate = (b, g) => {
158
+ const summaryLine = primeSummary(b, g);
159
+ return b.sections.length > 0
160
+ ? b.sections.map((s) => s.text).join('\n\n---\n\n') + '\n\n' + summaryLine
161
+ : summaryLine + '\n\n_No memory stored yet -- start working and Wyrm will learn._';
162
+ };
163
+ /** Shared render-options for both session_prime emit paths. */
164
+ const PRIME_RENDER_OPTS = { summary: primeSummary };
165
+ /** Spec 018 recovered-context savings accounting (both paths, best-effort). */
166
+ async function logPrimeSavings(db, projectId, response) {
167
+ try {
168
+ const { logSavings } = await import('../statusline.js');
169
+ const recoveredChars = blockText(response.content[0]).length;
170
+ // ~4 chars/token is a conservative English estimate.
171
+ const recoveredTokens = Math.round(recoveredChars / 4);
172
+ // Use last active session for this project if available.
173
+ const lastSession = db.prepare(`SELECT id FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1`).get(projectId);
174
+ logSavings(db, 'wyrm_session_prime', 'recovered_context', recoveredTokens, lastSession?.id);
175
+ }
176
+ catch { /* best-effort */ }
177
+ }
178
+ // ── T028 fleet mode ─────────────────────────────────────────────────────────
179
+ /** Over-budget sections collapse to a plain-text `wyrm://` reference naming
180
+ * the follow-on tool. THE T034 SEAM: when the resource layer lands, these
181
+ * become real `resourceLink` returns — 7.0 ships them as text on purpose. */
182
+ const STUB_REFS = {
183
+ continuity: { path: 'continuity', tool: 'wyrm_session_prime' },
184
+ truths: { path: 'truths', tool: 'wyrm_truth_get' },
185
+ scaffold: { path: 'scaffolds', tool: 'wyrm_context_build' },
186
+ memory: { path: 'memory', tool: 'wyrm_recall' },
187
+ quests: { path: 'quests', tool: 'wyrm_all_quests' },
188
+ };
189
+ /** Tokens reserved for the summary line + section separators before any
190
+ * section is admitted (measured worst-case summary ≈40 tokens at 4cpt). */
191
+ const PRIME_SUMMARY_RESERVE = 48;
192
+ const DEFAULT_PRIME_BUDGET = 1200;
193
+ /**
194
+ * Fleet-mode session_prime (T028). Compile-once, CAS-insert, read-back-winner:
195
+ * byte-equality across N concurrent primes of one (run_id, role) holds even
196
+ * across processes, because every caller renders the row that WON the
197
+ * INSERT OR IGNORE — a loser's own compile is discarded, never returned.
198
+ * First-caller-wins applies to the args too (task/project/log_session/
199
+ * token_budget shape only the compiled brief); later callers with different
200
+ * args still receive the cached prefix — that IS the stability contract.
201
+ */
202
+ async function fleetPrime(args, { store, raw, memory, truths, scaffolds, cache }) {
203
+ const { project_id: spId, project_name: spName, task: spTask, log_session: spLog } = args;
204
+ const runId = sanitizeActorId(args.run_id);
205
+ if (runId === null)
206
+ throw new ValidationError('run_id', 'must be <=64 printable-ASCII chars (no ";")');
207
+ // role is half the cache key — '' is the role-less slice.
208
+ const role = asString('role', args.role, { maxLen: 200 }) ?? '';
209
+ const budget = asInt('token_budget', args.token_budget, { min: 1 }) ?? DEFAULT_PRIME_BUDGET;
210
+ const db = raw();
211
+ const readBrief = () => db.prepare('SELECT body_json FROM run_briefs WHERE run_id = ? AND role = ?').get(runId, role);
212
+ // Security pass #1 (confirmed finding): the cache key is (run_id, role) —
213
+ // a cache hit previously skipped project resolution ENTIRELY, so a prime
214
+ // naming a DIFFERENT project under the same key silently received the
215
+ // first project's ground truths, memory, and quests. An EXPLICITLY
216
+ // requested project is now resolved BEFORE the cache read and checked
217
+ // against the project_id the winning compile baked into body_json; a
218
+ // mismatch is a visible error, never a cross-project leak. Project-LESS
219
+ // primes still take the cached row untouched — first-caller-wins applies
220
+ // to the args (incl. the project), which IS the byte-stability contract,
221
+ // and resolving the latest-active-session branch on a hit would itself
222
+ // be a mid-run nondeterminism source.
223
+ const requested = (spId || spName) ? resolvePrimeProject(store, db, spId, spName) : undefined;
224
+ if ((spId || spName) && !requested)
225
+ return primeProjectNotFound(store);
226
+ let row = readBrief();
227
+ if (row && requested) {
228
+ const cached = JSON.parse(row.body_json);
229
+ if (cached.project_id !== requested.id) {
230
+ return {
231
+ content: [{
232
+ type: 'text',
233
+ text: `Fleet brief for (run ${runId}, role '${role}') is pinned to project '${cached.project_name}' (ID: ${cached.project_id}) — refusing to serve it for requested project '${requested.name}' (ID: ${requested.id}). Use a distinct role for a second project, or prime without run_id.`,
234
+ }],
235
+ isError: true,
236
+ };
237
+ }
238
+ }
239
+ if (!row) {
240
+ const project = requested ?? resolvePrimeProject(store, db, spId, spName);
241
+ if (!project)
242
+ return primeProjectNotFound(store); // never cached — a later prime may resolve
243
+ // Same section compilation as the default path, nondeterminism pinned
244
+ // (no staleness prefixes, no vault advisory — see the module header).
245
+ const compiled = [];
246
+ // Section 0 — Continuity: the living thread, restored FIRST (where we left off).
247
+ const contText = buildContinuitySectionText(store.getContinuity(project.id));
248
+ if (contText)
249
+ compiled.push({ kind: 'continuity', text: contText });
250
+ const currentTruths = truths.getCurrent(project.id);
251
+ if (currentTruths.length > 0) {
252
+ compiled.push({ kind: 'truths', text: buildTruthsSectionText(currentTruths, false).slice(0, 1200) });
253
+ }
254
+ // The ROLE SLICE: role drives scaffold + memory selection when the
255
+ // orchestrator didn't pass an explicit task.
256
+ const fleetTask = spTask ?? (role || undefined);
257
+ if (fleetTask) {
258
+ const scaffoldMatch = scaffolds.findBest(fleetTask, project.id, 0.3);
259
+ if (scaffoldMatch)
260
+ compiled.push({ kind: 'scaffold', text: scaffolds.formatForContext(scaffoldMatch, 1500) });
261
+ }
262
+ const spBrief = memory.buildContextBrief(project.id, fleetTask ?? 'general', { maxItems: 10, minConfidence: 0.3 });
263
+ if (spBrief.text)
264
+ compiled.push({ kind: 'memory', text: spBrief.text.slice(0, 2000) });
265
+ const activeQuests = db.prepare(ACTIVE_QUESTS_SQL).all(project.id);
266
+ if (activeQuests.length > 0) {
267
+ compiled.push({ kind: 'quests', text: buildQuestsSectionText(activeQuests).slice(0, 800) });
268
+ }
269
+ // Token budget (approx-4cpt, the token-budget.ts convention): greedy
270
+ // in section order; an over-budget section becomes its wyrm:// stub.
271
+ // Stubs always emit (a pathological budget floors at stubs + summary —
272
+ // the brief never silently loses a whole section).
273
+ const est = makeEstimator();
274
+ const sections = [];
275
+ let used = PRIME_SUMMARY_RESERVE;
276
+ for (const s of compiled) {
277
+ const cost = est.count(s.text) + 2; // +2 ≈ the '\n\n---\n\n' separator
278
+ if (used + cost <= budget) {
279
+ sections.push(s);
280
+ used += cost;
281
+ }
282
+ else {
283
+ const ref = STUB_REFS[s.kind] ?? { path: s.kind, tool: 'wyrm_search' };
284
+ const stub = `[beyond token_budget] wyrm://project/${project.id}/${ref.path} -- fetch via ${ref.tool}`;
285
+ sections.push({ kind: s.kind, text: stub });
286
+ used += est.count(stub) + 2;
287
+ }
288
+ }
289
+ // log_session fires on the COMPILING call only — the logged session_id is
290
+ // baked into the cached body; cache hits never insert another row.
291
+ let sessionId = null;
292
+ if (spLog) {
293
+ sessionId = store.createSession(project.id, {
294
+ objectives: spTask ?? 'Session started via wyrm_session_prime',
295
+ summary: '',
296
+ }).id;
297
+ }
298
+ const body = {
299
+ project_id: project.id,
300
+ project_name: project.name,
301
+ truth_count: truths.getStats(project.id).current,
302
+ quest_count: activeQuests.length,
303
+ memory_count: memory.getStats(project.id).total,
304
+ session_id: sessionId,
305
+ sections,
306
+ run_id: runId,
307
+ role,
308
+ token_budget: budget,
309
+ };
310
+ db.prepare('INSERT OR IGNORE INTO run_briefs (run_id, role, body_json, token_budget) VALUES (?, ?, ?, ?)').run(runId, role, JSON.stringify(body), budget);
311
+ row = readBrief();
312
+ }
313
+ // Render the WINNER's body — text re-derives through the shared template,
314
+ // so the text channel is byte-identical for every caller too.
315
+ const winner = JSON.parse(row.body_json);
316
+ const response = renderResult(winner, primeTemplate, PRIME_RENDER_OPTS);
317
+ // FEATHERWEIGHT opt #2 — for_spawn: same CAS row, same compile, packaged for
318
+ // OUT-OF-BAND embedding (advances T038 in spirit). The brief is the payload
319
+ // the orchestrator embeds RAW into each same-role agent's spawn prefix —
320
+ // there is NO structuredContent channel in a spawn prompt, so the brief MUST
321
+ // carry the FULL prime (truths/scaffold/memory/quests), never the renderer's
322
+ // WYRM_CHANNEL=structured summary collapse. We therefore render a dedicated
323
+ // exemptStructured response for the brief (mirroring session_rehydrate, whose
324
+ // text IS its payload): under WYRM_CHANNEL=both this is byte-identical to the
325
+ // agent-side prime's content[0].text (the distribute-once == fallback
326
+ // guarantee); under WYRM_CHANNEL=structured the agent-side fallback collapses
327
+ // its text channel but still ships the full body on structuredContent, while
328
+ // the embedded brief — which has no such body to fall back on — stays full.
329
+ const forSpawn = args.for_spawn === true;
330
+ if (forSpawn) {
331
+ const briefResponse = renderResult(winner, primeTemplate, { ...PRIME_RENDER_OPTS, exemptStructured: true });
332
+ const brief = blockText(briefResponse.content[0]);
333
+ const briefHash = createHash('sha256').update(brief, 'utf8').digest('hex');
334
+ // The for_spawn envelope is a SUPERSET of the declared session_prime
335
+ // outputSchema (project_id/project_name/session_id/sections required) — the
336
+ // schema-required fields stay so a strict client validates it, and the
337
+ // distribute-once extras (brief / brief_hash / cache_key) ride alongside
338
+ // per the T022 lean-union rule (undeclared extras on structuredContent).
339
+ const spawnBody = {
340
+ project_id: winner.project_id,
341
+ project_name: winner.project_name,
342
+ session_id: winner.session_id ?? null,
343
+ sections: winner.sections,
344
+ for_spawn: true,
345
+ run_id: runId,
346
+ role,
347
+ brief,
348
+ brief_hash: briefHash,
349
+ brief_chars: brief.length,
350
+ cache_key: { table: 'run_briefs', run_id: runId, role },
351
+ };
352
+ await logPrimeSavings(db, winner.project_id, briefResponse);
353
+ // The for_spawn envelope is per-(args) like any read; publish under the
354
+ // same dispatcher key convention so a repeat for_spawn read is cache-hot.
355
+ const spawnResult = {
356
+ content: [{ type: 'text', text: brief }],
357
+ structuredContent: spawnBody,
358
+ };
359
+ cache.set(cacheKeyFor('wyrm_session_prime', JSON.stringify(args)), spawnResult, 30000);
360
+ return spawnResult;
361
+ }
362
+ await logPrimeSavings(db, winner.project_id, response);
363
+ // READ_ONLY_TOOLS cache publish — dispatcher key convention.
364
+ cache.set(cacheKeyFor('wyrm_session_prime', JSON.stringify(args)), response, 30000);
365
+ return response;
366
+ }
367
+ export const sessionToolSpecs = [
368
+ {
369
+ name: "wyrm_session_start",
370
+ description: "Start or continue a session for a project",
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ projectPath: { type: "string", description: "Project path" },
375
+ objectives: { type: "string", description: "Session objectives" },
376
+ },
377
+ required: ["projectPath"],
378
+ },
379
+ outputSchema: {
380
+ type: "object",
381
+ properties: {
382
+ session_id: { type: "integer" },
383
+ project: { type: "string" },
384
+ date: { type: "string" },
385
+ objectives: { type: ["string", "null"] },
386
+ },
387
+ required: ["session_id", "project", "date", "objectives"],
388
+ },
389
+ annotations: TOOL_ANNOTATIONS.wyrm_session_start,
390
+ aliases: [],
391
+ handler: (args, { store }) => {
392
+ const { projectPath, objectives } = args;
393
+ let project = store.getProject(projectPath);
394
+ if (!project) {
395
+ // Auto-register project
396
+ project = store.registerProject(basename(projectPath), projectPath);
397
+ }
398
+ let session = store.getTodaySession(project.id);
399
+ if (!session) {
400
+ session = store.createSession(project.id, { objectives: objectives || '' });
401
+ }
402
+ else if (objectives) {
403
+ session = store.updateSession(session.id, {
404
+ objectives: session.objectives ? `${session.objectives}\n${objectives}` : objectives,
405
+ });
406
+ }
407
+ // Archive old sessions
408
+ store.archiveOldSessions(project.id, 10);
409
+ const body = {
410
+ session_id: session.id,
411
+ project: project.name,
412
+ date: session.date,
413
+ objectives: session.objectives || null,
414
+ };
415
+ return renderResult(body, (b, g) => withGlyph(g.brand, `Session ${b.session_id} for ${b.project}`) + '\n' +
416
+ `**Date:** ${b.date}\n` +
417
+ `**Objectives:** ${b.objectives ?? 'None set'}`);
418
+ },
419
+ },
420
+ {
421
+ name: "wyrm_session_update",
422
+ description: "Update the current session with completed work, issues, or notes",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ projectPath: { type: "string" },
427
+ completed: { type: "string", description: "What was completed" },
428
+ issues: { type: "string" },
429
+ commits: { type: "string", description: "Git commits made" },
430
+ notes: { type: "string" },
431
+ },
432
+ required: ["projectPath"],
433
+ },
434
+ outputSchema: {
435
+ type: "object",
436
+ properties: {
437
+ session_id: { type: "integer" },
438
+ project: { type: "string" },
439
+ updated: { type: "array", items: { type: "string", enum: ["completed", "issues", "commits", "notes"] } },
440
+ },
441
+ required: ["session_id", "project", "updated"],
442
+ },
443
+ annotations: TOOL_ANNOTATIONS.wyrm_session_update,
444
+ aliases: [],
445
+ handler: (args, { store, indexing }) => {
446
+ const { projectPath, completed, issues, commits, notes } = args;
447
+ const project = store.getProject(projectPath);
448
+ if (!project) {
449
+ // T026: was a non-error "Project not found" text in 6.x — domain
450
+ // not-founds are isError per the goals precedent (schema-exempt).
451
+ return { content: [{ type: "text", text: "Project not found" }], isError: true };
452
+ }
453
+ let session = store.getTodaySession(project.id);
454
+ if (!session) {
455
+ session = store.createSession(project.id, {});
456
+ }
457
+ const updates = {};
458
+ if (completed)
459
+ updates.completed = session.completed ? `${session.completed}\n${completed}` : completed;
460
+ if (issues)
461
+ updates.issues = session.issues ? `${session.issues}\n${issues}` : issues;
462
+ if (commits)
463
+ updates.commits = session.commits ? `${session.commits}\n${commits}` : commits;
464
+ if (notes)
465
+ updates.notes = session.notes ? `${session.notes}\n${notes}` : notes;
466
+ session = store.updateSession(session.id, updates);
467
+ // Feed the living thread (Continuity Layer) — what we just worked on, so the next session
468
+ // or surface (phone, Hermes, Ember, Gemini) re-opens already knowing it.
469
+ const threadNote = [completed, issues, notes].filter(Boolean).join(' · ');
470
+ if (threadNote)
471
+ store.updateContinuity(project.id, threadNote, 'session_update');
472
+ indexing()?.enqueue('session', session.id, project.id);
473
+ const body = { session_id: session.id, project: project.name, updated: Object.keys(updates) };
474
+ return renderResult(body, (b, g) => withGlyph(g.brand, `Session updated for ${b.project}`));
475
+ },
476
+ },
477
+ {
478
+ name: "wyrm_session_rehydrate",
479
+ 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.",
480
+ inputSchema: {
481
+ type: "object",
482
+ properties: {
483
+ session_id: { type: "number", description: "ID of the session to rehydrate" },
484
+ include_artifacts: { type: "boolean", description: "Include memory artifacts (default true)" },
485
+ include_failures: { type: "boolean", description: "Include unresolved failure patterns (default true)" },
486
+ max_truth_chars: { type: "number", description: "Cap on chars used for ground truths (default 2000)" },
487
+ },
488
+ required: ["session_id"],
489
+ },
490
+ outputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ session_id: { type: "integer" },
494
+ project_name: { type: "string" },
495
+ project_path: { type: "string" },
496
+ briefing_markdown: { type: "string" },
497
+ context_chars: { type: "integer" },
498
+ attached: {
499
+ type: "object",
500
+ properties: {
501
+ quests: { type: "integer" },
502
+ truths: { type: "integer" },
503
+ artifacts: { type: "integer" },
504
+ failures: { type: "integer" },
505
+ },
506
+ required: ["quests", "truths", "artifacts", "failures"],
507
+ },
508
+ },
509
+ required: ["session_id", "project_name", "project_path", "briefing_markdown", "context_chars", "attached"],
510
+ },
511
+ annotations: TOOL_ANNOTATIONS.wyrm_session_rehydrate,
512
+ aliases: [],
513
+ handler: (args, { rehydration }) => {
514
+ const { session_id, include_artifacts, include_failures, max_truth_chars } = args;
515
+ const brief = rehydration.rehydrate(session_id, {
516
+ include_artifacts, include_failures, max_truth_chars,
517
+ });
518
+ if (!brief) {
519
+ return { content: [{ type: "text", text: `Session #${session_id} not found.` }], isError: true };
520
+ }
521
+ // The briefing markdown IS the text channel (6.x behavior, byte-
522
+ // identical); the body carries it plus the structured brief identity.
523
+ // EXEMPT from WYRM_CHANNEL=structured shrink: the text channel here is
524
+ // the payload (the body just wraps the same markdown), so collapsing it
525
+ // to a summary line would drop the knowledge the model reads.
526
+ return renderResult({ ...brief }, (b) => b.briefing_markdown, { exemptStructured: true });
527
+ },
528
+ },
529
+ {
530
+ name: "wyrm_session_prime",
531
+ 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.",
532
+ inputSchema: {
533
+ type: "object",
534
+ properties: {
535
+ project_id: { type: "number" },
536
+ project_name: { type: "string" },
537
+ task: { type: "string", description: "Drives scaffold + memory selection" },
538
+ log_session: { type: "boolean", description: "Also log a session row" },
539
+ // T028 fleet mode (descriptions omitted under the 8K default-surface
540
+ // pin — semantics in the tool description + run_id's frozen meaning).
541
+ run_id: { type: "string", description: "Fleet: brief cached per (run_id, role)" },
542
+ role: { type: "string" },
543
+ token_budget: { type: "number" },
544
+ for_spawn: { type: "boolean" },
545
+ },
546
+ },
547
+ // T028: the truth/quest/memory count properties were dropped from the
548
+ // DECLARED schema to fund the fleet-mode input params under the 8K pin
549
+ // (the T027 handoff earmarked them); the body still carries the counts —
550
+ // undeclared extras ride structuredContent (the T022 lean-union rule).
551
+ outputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ project_id: { type: "integer" },
555
+ project_name: { type: "string" },
556
+ session_id: { type: ["integer", "null"] },
557
+ sections: {
558
+ type: "array",
559
+ items: {
560
+ type: "object",
561
+ properties: {
562
+ kind: { type: "string" },
563
+ text: { type: "string" },
564
+ },
565
+ required: ["kind", "text"],
566
+ },
567
+ },
568
+ },
569
+ required: ["project_id", "project_name", "session_id", "sections"],
570
+ },
571
+ annotations: TOOL_ANNOTATIONS.wyrm_session_prime,
572
+ aliases: [],
573
+ handler: async (args, { store, raw, memory, truths, scaffolds, cache }) => {
574
+ const { project_id: spId, project_name: spName, task: spTask, log_session: spLog } = args;
575
+ // T028 fleet mode: run_id present → the byte-stable cached-brief path.
576
+ if (args.run_id !== undefined && args.run_id !== null) {
577
+ return fleetPrime(args, { store, raw, memory, truths, scaffolds, cache });
578
+ }
579
+ // Project resolution (shared 3-branch helper — T028 hoist, verbatim).
580
+ const spProject = resolvePrimeProject(store, raw(), spId, spName);
581
+ if (!spProject) {
582
+ // T026: not-found paths are isError (goals precedent; was non-error
583
+ // prose in 6.x — recorded deviation).
584
+ return primeProjectNotFound(store);
585
+ }
586
+ const sections = [];
587
+ // Section 0 — Continuity: the living thread, restored FIRST (where we left off).
588
+ const spContText = buildContinuitySectionText(store.getContinuity(spProject.id));
589
+ if (spContText)
590
+ sections.push({ kind: 'continuity', text: spContText });
591
+ // Section 1 — Ground Truths (up to 1200 chars), prefix stale truths
592
+ const spCurrentTruths = truths.getCurrent(spProject.id);
593
+ if (spCurrentTruths.length > 0) {
594
+ sections.push({ kind: 'truths', text: buildTruthsSectionText(spCurrentTruths, true).slice(0, 1200) });
595
+ }
596
+ // Section 2 — Reasoning Scaffold (up to 1500 chars)
597
+ if (spTask) {
598
+ const scaffoldMatchSP = scaffolds.findBest(spTask, spProject.id, 0.3);
599
+ if (scaffoldMatchSP) {
600
+ sections.push({ kind: 'scaffold', text: scaffolds.formatForContext(scaffoldMatchSP, 1500) });
601
+ }
602
+ }
603
+ // Section 3 — Memory Brief (up to 2000 chars)
604
+ const spBrief = memory.buildContextBrief(spProject.id, spTask ?? 'general', { maxItems: 10, minConfidence: 0.3 });
605
+ if (spBrief.text)
606
+ sections.push({ kind: 'memory', text: spBrief.text.slice(0, 2000) });
607
+ // Section 4 — Active Quests (up to 800 chars)
608
+ const spActiveQuests = raw().prepare(ACTIVE_QUESTS_SQL).all(spProject.id);
609
+ if (spActiveQuests.length > 0) {
610
+ sections.push({ kind: 'quests', text: buildQuestsSectionText(spActiveQuests).slice(0, 800) });
611
+ }
612
+ // Section 5 — Credential vault advisory (global; only when actionable, so it
613
+ // self-resolves once `wyrm vault setup` has been run). Lets Wyrm AUTOMATICALLY
614
+ // guide the connecting AI to set credential storage up properly.
615
+ try {
616
+ const { vaultAdvisory } = await import('../vault.js');
617
+ const vaultLines = vaultAdvisory();
618
+ if (vaultLines.length > 0) {
619
+ sections.push({ kind: 'vault', text: '### 🔐 Credential Vault\n' + vaultLines.map(l => `- ${l}`).join('\n') });
620
+ }
621
+ }
622
+ catch { /* vault optional */ }
623
+ // Optionally log session
624
+ let sessionId = null;
625
+ if (spLog) {
626
+ const session = store.createSession(spProject.id, {
627
+ objectives: spTask ?? 'Session started via wyrm_session_prime',
628
+ summary: '',
629
+ });
630
+ sessionId = session.id;
631
+ }
632
+ const body = {
633
+ project_id: spProject.id,
634
+ project_name: spProject.name,
635
+ truth_count: truths.getStats(spProject.id).current,
636
+ quest_count: spActiveQuests.length,
637
+ memory_count: memory.getStats(spProject.id).total,
638
+ session_id: sessionId,
639
+ sections,
640
+ };
641
+ const spResponse = renderResult(body, primeTemplate, PRIME_RENDER_OPTS);
642
+ // Spec 018: log recovered-context savings. The text we just assembled
643
+ // is context the AI would have had to re-elicit from the operator
644
+ // turn by turn — counting it as ~recovered tokens (conservative).
645
+ await logPrimeSavings(raw(), spProject.id, spResponse);
646
+ // READ_ONLY_TOOLS cache publish — dispatcher key convention.
647
+ cache.set(cacheKeyFor('wyrm_session_prime', JSON.stringify(args)), spResponse, 30000);
648
+ return spResponse;
649
+ },
650
+ },
651
+ ];
652
+ //# sourceMappingURL=session.js.map