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,26 +1,508 @@
1
- import{TOOL_ANNOTATIONS as z}from"../tool-annotations.js";import{renderResult as R,withGlyph as x}from"../render.js";import{ValidationError as E,asEnum as W,asString as w}from"../validate.js";import{getActor as B,runWithActor as G,sanitizeActorId as V}from"./boundary.js";import{extractCandidates as J,candidateToArtifact as Q,escapeLikePattern as K}from"../auto-capture.js";import{ulid as X}from"../ulid.js";const H=["start","join","status","debrief","end"],Z=["completed","failed","abandoned"],O=(e,n)=>e.prepare("SELECT * FROM runs WHERE run_id = ?").get(n),v=(e,n)=>e.prepare("SELECT agent_id, role, joined_at FROM run_agents WHERE run_id = ? ORDER BY joined_at, agent_id").all(n),ee=(e,n,s,r)=>{e.prepare("INSERT INTO runs (run_id, parent_run_id, orchestrator, status) VALUES (?, ?, ?, 'running')").run(n,s,r)},te=(e,n,s)=>{e.prepare("UPDATE runs SET status = ?, updated_at = datetime('now') WHERE run_id = ?").run(s,n)};function h(e,n){if(n==null)return null;const s=V(n);if(s===null)throw new E(e,'must be <=64 printable-ASCII chars (no ";")');return s}function q(e,n,s,r){e.prepare(`
1
+ /**
2
+ * Run domain — ToolSpec contract v2 (v7 F3 T027, spec FR-5 / G5).
3
+ *
4
+ * `wyrm_run` is the fleet-run lifecycle tool on the migration-20 runs /
5
+ * run_agents tables — the 32nd survivor (spec §7 criterion 1 lands the
6
+ * advertised surface at exactly the ≤32 pin):
7
+ *
8
+ * start — mints a ULID run_id (or idempotently adopts an explicit one —
9
+ * the F2 orphan shape where the orchestrator already exported
10
+ * WYRM_RUN_ID) and registers the orchestrator in run_agents.
11
+ * join — registers (agent_id, role) membership; re-join updates role.
12
+ * status — run row + members + run-quarantined failure counts + claims.
13
+ * debrief — fans each agent's submitted learnings through the EXISTING
14
+ * auto_capture pipeline (src/auto-capture.ts: the LOCAL
15
+ * WYRM_EXTRACT_MODEL Ollama slot / deterministic fallback —
16
+ * NEVER a cloud LLM, Article III) into a RUN-SCOPED review
17
+ * queue: needs_review=1 artifacts stamped with the run's run_id
18
+ * + the submitting agent's agent_id (via runWithActor) and
19
+ * tagged `run:<run_id>`.
20
+ * end — writes the run summary artifact (linked via
21
+ * runs.debrief_artifact_id), sets the terminal status
22
+ * (completed|failed|abandoned — the T015 sweep lifecycle), lets
23
+ * the T015 sweepRunQuarantine() explicit-verdict paths
24
+ * promote/expire this run's quarantined failures, and releases
25
+ * the run's quest claims (presence.releaseRunClaims).
26
+ *
27
+ * Attribution: full boundary-envelope stamping for free — `run_id` and
28
+ * `agent_id`-style params ride the same arg names the dispatcher's
29
+ * resolveActorEnvelope() reads, every memory.add() stamps the ambient
30
+ * envelope, and the debrief wraps each learning in runWithActor() so the
31
+ * SUBMITTING agent (not the orchestrator relaying the call) is the artifact's
32
+ * recorded author.
33
+ *
34
+ * Writes are LOCAL (no daemonOr seam): the runs/run_agents tables have no
35
+ * daemon-writer op, and the review-queue candidate path mirrors
36
+ * wyrm_auto_capture's direct memory.add() precedent — recorded T027
37
+ * deviation note.
38
+ *
39
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
40
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
41
+ */
42
+ import { TOOL_ANNOTATIONS } from '../tool-annotations.js';
43
+ import { renderResult, withGlyph } from '../render.js';
44
+ import { ValidationError, asEnum, asString } from '../validate.js';
45
+ import { getActor, runWithActor, sanitizeActorId } from './boundary.js';
46
+ import { extractCandidates, candidateToArtifact, escapeLikePattern } from '../auto-capture.js';
47
+ import { ulid } from '../ulid.js';
48
+ const RUN_ACTIONS = ['start', 'join', 'status', 'debrief', 'end'];
49
+ const TERMINAL_STATUSES = ['completed', 'failed', 'abandoned'];
50
+ export const getRun = (db, runId) => db.prepare('SELECT * FROM runs WHERE run_id = ?').get(runId);
51
+ export const getAgents = (db, runId) => db.prepare('SELECT agent_id, role, joined_at FROM run_agents WHERE run_id = ? ORDER BY joined_at, agent_id').all(runId);
52
+ /** Mint the run row (status defaults to 'running'). The ONE place the
53
+ * `INSERT INTO runs` lives — both the wyrm_run handler and the CLI call here. */
54
+ export const createRun = (db, runId, parentRunId, orchestrator) => {
55
+ db.prepare(`INSERT INTO runs (run_id, parent_run_id, orchestrator, status) VALUES (?, ?, ?, 'running')`).run(runId, parentRunId, orchestrator);
56
+ };
57
+ /** Set a run's terminal (or any) status + bump updated_at. The ONE place the
58
+ * status `UPDATE runs` lives. The handler's `end` wraps this with the full
59
+ * lifecycle (quarantine sweep, claim release, summary artifact); the CLI's
60
+ * `wyrm run end` is the minimal status flip. */
61
+ export const setRunStatus = (db, runId, status) => {
62
+ db.prepare(`UPDATE runs SET status = ?, updated_at = datetime('now') WHERE run_id = ?`)
63
+ .run(status, runId);
64
+ };
65
+ /** Boundary-validate an identity-shaped param (same rules the actor envelope
66
+ * applies: ≤64 printable-ASCII chars, no ';'). Required-ness is the caller's
67
+ * concern — null/undefined pass through as null. */
68
+ function asIdentity(field, v) {
69
+ if (v === undefined || v === null)
70
+ return null;
71
+ const safe = sanitizeActorId(v);
72
+ if (safe === null)
73
+ throw new ValidationError(field, 'must be <=64 printable-ASCII chars (no ";")');
74
+ return safe;
75
+ }
76
+ /** Register run membership (idempotent CAS on the (run_id, agent_id) PK;
77
+ * re-join refreshes the role when one is supplied — EXCEPT off
78
+ * 'orchestrator', which only `action=start` assigns and nothing strips.
79
+ *
80
+ * Second-round review of security pass #1 (confirmed finding): the plain
81
+ * COALESCE refresh let an envelope-less caller re-join AS the orchestrator's
82
+ * agent_id with any fleet role and silently DEMOTE the run's only
83
+ * 'orchestrator' row — gate (2) at join only guards CLAIMING the role, not
84
+ * stripping it — after which the start-door orphan-adoption branch (hasOrch
85
+ * now false) handed 'orchestrator' to a different agent: a two-step takeover
86
+ * that defeated the reserved-role invariant the pass introduced. The role is
87
+ * now sticky at the one seam every roster write passes through; the
88
+ * trusted-mesh self-identify re-join still succeeds, it just cannot mutate
89
+ * owner truth. */
90
+ export function registerAgent(db, runId, agentId, role) {
91
+ db.prepare(`
2
92
  INSERT INTO run_agents (run_id, agent_id, role) VALUES (?, ?, ?)
3
93
  ON CONFLICT(run_id, agent_id) DO UPDATE SET role =
4
94
  CASE WHEN run_agents.role = 'orchestrator' THEN run_agents.role
5
95
  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}`:""}.`)+`
7
- Subagents join with: wyrm_run({ action: 'join', run_id: '${l.run_id}', agent_id, role })
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 \u2014 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(`
96
+ `).run(runId, agentId, role);
97
+ }
98
+ // ── run authority (F3 security pass #1, confirmed findings) ────────────────
99
+ // The trusted-mesh model has no cryptographic identity, so these gates are
100
+ // accident/impersonation guards, not auth: roster mutations and run teardown
101
+ // were previously open to ANY caller holding the run_id (join injected
102
+ // arbitrary (agent_id, role) rows — including role='orchestrator', which
103
+ // presence.membershipRole treats as board truth — and end force-released
104
+ // EVERY claim the run held). Denials are visible (isError), never silent.
105
+ /** TRUE when `agentId` is registered on the run with the orchestrator role. */
106
+ function isOrchestratorMember(db, runId, agentId) {
107
+ if (!agentId)
108
+ return false;
109
+ return db.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ? AND role = 'orchestrator'").get(runId, agentId) !== undefined;
110
+ }
111
+ /** TRUE when `agentId` is a registered member of the run (any role). */
112
+ function isRunMember(db, runId, agentId) {
113
+ return db.prepare('SELECT 1 FROM run_agents WHERE run_id = ? AND agent_id = ?').get(runId, agentId) !== undefined;
114
+ }
115
+ /** TRUE when the ambient envelope carries an EXPLICIT agent identity (the
116
+ * hasExplicitAttribution rule, agent-field slice): param/meta/env levels
117
+ * only — the clientInfo fallback is what every anonymous caller gets for
118
+ * free, so it can never carry on-behalf-of authority. */
119
+ function hasExplicitAgent(ambient) {
120
+ return ambient.agent_id !== null && ambient.source !== 'client' && ambient.source !== 'legacy';
121
+ }
122
+ export const touchRun = (db, runId) => {
123
+ db.prepare(`UPDATE runs SET updated_at = datetime('now') WHERE run_id = ?`).run(runId);
124
+ };
125
+ const notFound = (runId) => ({
126
+ content: [{ type: 'text', text: `Run not found: ${runId}` }], isError: true,
127
+ });
128
+ export const runToolSpecs = [
129
+ {
130
+ name: "wyrm_run",
131
+ 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.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ action: { type: "string", enum: ["start", "join", "status", "debrief", "end"] },
136
+ run_id: { type: "string", description: "start mints if omitted" },
137
+ agent_id: { type: "string", description: "default: ambient envelope" },
138
+ role: { type: "string", description: "join" },
139
+ orchestrator: { type: "string", description: "start" },
140
+ parent_run_id: { type: "string", description: "start: nested fleets" },
141
+ project_path: { type: "string", description: "debrief/end" },
142
+ learnings: { type: "array", items: { type: "object" }, description: "debrief: [{agent_id?, text}]" },
143
+ status: { type: "string", enum: ["completed", "failed", "abandoned"], description: "end (default completed)" },
144
+ },
145
+ required: ["action"],
146
+ },
147
+ // Flat result union (lean under the 8K default-surface pin): the
148
+ // per-action count fields are top-level optionals, never nested objects.
149
+ outputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ action: { type: "string" },
153
+ run_id: { type: "string" },
154
+ status: { type: "string" },
155
+ orchestrator: { type: ["string", "null"] },
156
+ agents: {
157
+ type: "array",
158
+ items: {
159
+ type: "object",
160
+ properties: { agent_id: { type: "string" }, role: { type: ["string", "null"] } },
161
+ required: ["agent_id", "role"],
162
+ },
163
+ },
164
+ quarantined: { type: "integer" },
165
+ resolved: { type: "integer" },
166
+ claims: { type: "integer" },
167
+ method: { type: "string" },
168
+ queued: { type: "integer" },
169
+ skipped: { type: "integer" },
170
+ promoted: { type: "integer" },
171
+ expired: { type: "integer" },
172
+ claims_released: { type: "integer" },
173
+ summary_artifact_id: { type: ["integer", "null"] },
174
+ },
175
+ required: ["action", "run_id", "status"],
176
+ },
177
+ annotations: TOOL_ANNOTATIONS.wyrm_run,
178
+ aliases: [],
179
+ handler: async (args, ctx) => {
180
+ const action = asEnum('action', args.action, RUN_ACTIONS);
181
+ if (!action)
182
+ throw new ValidationError('action', `must be one of: ${RUN_ACTIONS.join(', ')}`);
183
+ const db = ctx.raw();
184
+ // Ambient boundary envelope (T009): the dispatcher stashed it in ALS
185
+ // before the registry fork — same read every attributed write uses.
186
+ const ambient = getActor();
187
+ // ── start ─────────────────────────────────────────────────────────────
188
+ if (action === 'start') {
189
+ const explicit = asIdentity('run_id', args.run_id);
190
+ const runId = explicit ?? ulid();
191
+ const orchestratorName = asString('orchestrator', args.orchestrator, { maxLen: 200 })
192
+ ?? ambient.agent_id ?? null;
193
+ const orchAgent = asIdentity('agent_id', args.agent_id) ?? ambient.agent_id;
194
+ const parentId = asIdentity('parent_run_id', args.parent_run_id);
195
+ const existing = getRun(db, runId);
196
+ if (existing) {
197
+ if (!explicit || existing.status !== 'running') {
198
+ // A minted-ULID collision is practically impossible; an explicit
199
+ // re-start of a TERMINAL run is an orchestration bug — visible,
200
+ // never silently re-opened.
201
+ return { content: [{ type: 'text', text: `Run ${runId} already exists (status: ${existing.status}).` }], isError: true };
202
+ }
203
+ // Idempotent re-start of a still-running explicit run (safe retry).
204
+ // Security pass #1: the retry must be by the SAME orchestrator —
205
+ // re-start with a different agent_id would otherwise quietly add a
206
+ // second 'orchestrator' roster row (the join-injection root cause
207
+ // through the start door). A run with no orchestrator member yet
208
+ // (the F2 orphan shape: WYRM_RUN_ID exported before any agent
209
+ // registered) may still adopt one.
210
+ if (orchAgent) {
211
+ const hasOrch = db.prepare("SELECT 1 FROM run_agents WHERE run_id = ? AND role = 'orchestrator' LIMIT 1").get(runId) !== undefined;
212
+ if (hasOrch && !isOrchestratorMember(db, runId, orchAgent)) {
213
+ return { content: [{ type: 'text', text: `Run ${runId} already has an orchestrator — re-start with the same agent_id, or join as a member instead.` }], isError: true };
214
+ }
215
+ registerAgent(db, runId, orchAgent, 'orchestrator');
216
+ }
217
+ touchRun(db, runId);
218
+ }
219
+ else {
220
+ if (parentId && !getRun(db, parentId)) {
221
+ return { content: [{ type: 'text', text: `Parent run not found: ${parentId}` }], isError: true };
222
+ }
223
+ createRun(db, runId, parentId, orchestratorName);
224
+ if (orchAgent)
225
+ registerAgent(db, runId, orchAgent, 'orchestrator');
226
+ }
227
+ const body = {
228
+ action: 'start',
229
+ run_id: runId,
230
+ status: 'running',
231
+ parent_run_id: parentId,
232
+ orchestrator: orchestratorName,
233
+ agents: orchAgent ? [{ agent_id: orchAgent, role: 'orchestrator' }] : [],
234
+ };
235
+ return renderResult(body, (b, g) => withGlyph(g.brand, `Run ${b.run_id} started${b.orchestrator ? ` by ${b.orchestrator}` : ''}.`)
236
+ + `\nSubagents join with: wyrm_run({ action: 'join', run_id: '${b.run_id}', agent_id, role })`
237
+ + `\nExport WYRM_RUN_ID=${b.run_id} so every write in the fleet carries run attribution.`
238
+ + `\nPrime ONCE per role: wyrm_session_prime({ run_id: '${b.run_id}', role, for_spawn: true }) -> embed the returned brief in each same-role agent's spawn prefix (don't re-prime per agent).`);
239
+ }
240
+ // ── everything else requires an existing run ─────────────────────────
241
+ const runId = asIdentity('run_id', args.run_id);
242
+ if (!runId)
243
+ throw new ValidationError('run_id', `is required for action=${action}`);
244
+ const run = getRun(db, runId);
245
+ if (!run)
246
+ return notFound(runId);
247
+ if (action === 'join') {
248
+ if (run.status !== 'running') {
249
+ return { content: [{ type: 'text', text: `Run ${runId} already ended (status: ${run.status}) — cannot join.` }], isError: true };
250
+ }
251
+ const explicitAgent = asIdentity('agent_id', args.agent_id);
252
+ const agentId = explicitAgent ?? ambient.agent_id;
253
+ if (!agentId) {
254
+ throw new ValidationError('agent_id', 'no agent identity — pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)');
255
+ }
256
+ const role = asString('role', args.role, { maxLen: 200 }) ?? null;
257
+ // ── roster authority (security pass #1) ──
258
+ // (1) A caller whose envelope carries an EXPLICIT identity may only
259
+ // self-join under it — registering a DIFFERENT agent_id is roster
260
+ // mutation on behalf of someone else, orchestrator-only. (An
261
+ // envelope-less caller passing agent_id IS self-identifying — the
262
+ // documented subagent flow — so it stays open; this gate narrows the
263
+ // attributed case without breaking the trusted-mesh join shape.)
264
+ if (explicitAgent && hasExplicitAgent(ambient) && explicitAgent !== ambient.agent_id
265
+ && !isOrchestratorMember(db, runId, ambient.agent_id)) {
266
+ return { content: [{ type: 'text', text: `Joining run ${runId} as '${explicitAgent}' while attributed as '${ambient.agent_id}' is orchestrator-only — join as yourself, or have the orchestrator register the agent.` }], isError: true };
267
+ }
268
+ // (2) role 'orchestrator' is RESERVED: presence.membershipRole and
269
+ // the status board read it as run-owner truth. Allowed only as an
270
+ // idempotent re-join of the existing orchestrator, or when delegated
271
+ // by the registered orchestrator's own envelope identity.
272
+ if (role === 'orchestrator'
273
+ && !isOrchestratorMember(db, runId, agentId)
274
+ && !isOrchestratorMember(db, runId, ambient.agent_id)) {
275
+ return { content: [{ type: 'text', text: `Role 'orchestrator' on run ${runId} is reserved — it is registered at action=start. Join with a fleet role instead.` }], isError: true };
276
+ }
277
+ registerAgent(db, runId, agentId, role);
278
+ touchRun(db, runId);
279
+ const agents = getAgents(db, runId);
280
+ const body = {
281
+ action: 'join',
282
+ run_id: runId,
283
+ status: run.status,
284
+ agents: agents.map((a) => ({ agent_id: a.agent_id, role: a.role })),
285
+ };
286
+ return renderResult(body, (b, g) => withGlyph(g.brand, `${agentId} joined run ${b.run_id}${role ? ` as ${role}` : ''} (${b.agents.length} agent(s) registered).`));
287
+ }
288
+ if (action === 'status') {
289
+ const agents = getAgents(db, runId);
290
+ const fails = db.prepare(`
10
291
  SELECT
11
292
  SUM(CASE WHEN quarantine_scope = 'run' AND resolved = 0 THEN 1 ELSE 0 END) AS quarantined,
12
293
  SUM(CASE WHEN resolved = 1 THEN 1 ELSE 0 END) AS resolved
13
294
  FROM failure_patterns WHERE run_id = ?
14
- `).get(t),d=r.prepare("SELECT COUNT(*) AS n FROM quest_claims WHERE run_id = ?").get(t).n,c={action:"status",run_id:t,status:g.status,parent_run_id:g.parent_run_id,orchestrator:g.orchestrator,agents:i.map(o=>({agent_id:o.agent_id,role:o.role})),quarantined:a.quarantined??0,resolved:a.resolved??0,claims:d};return R(c,(o,p)=>{const m=o.agents.map(l=>`${p.bullet} ${l.agent_id}${l.role?` (${l.role})`:""}`).join(`
15
- `);return x(p.brand,`Run ${o.run_id} [${o.status}]`)+`${o.orchestrator?`
16
- Orchestrator: ${o.orchestrator}`:""}
17
- Agents (${o.agents.length}):${m?`
18
- ${m}`:" none"}
19
- Failures: ${o.quarantined} run-quarantined unresolved, ${o.resolved} resolved
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 \u2014 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 \u2014 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(`
295
+ `).get(runId);
296
+ const claims = db.prepare('SELECT COUNT(*) AS n FROM quest_claims WHERE run_id = ?').get(runId).n;
297
+ const body = {
298
+ action: 'status',
299
+ run_id: runId,
300
+ status: run.status,
301
+ parent_run_id: run.parent_run_id,
302
+ orchestrator: run.orchestrator,
303
+ agents: agents.map((a) => ({ agent_id: a.agent_id, role: a.role })),
304
+ quarantined: fails.quarantined ?? 0,
305
+ resolved: fails.resolved ?? 0,
306
+ claims,
307
+ };
308
+ return renderResult(body, (b, g) => {
309
+ const lines = b.agents.map((a) => `${g.bullet} ${a.agent_id}${a.role ? ` (${a.role})` : ''}`).join('\n');
310
+ return withGlyph(g.brand, `Run ${b.run_id} [${b.status}]`)
311
+ + `${b.orchestrator ? `\nOrchestrator: ${b.orchestrator}` : ''}`
312
+ + `\nAgents (${b.agents.length}):${lines ? `\n${lines}` : ' none'}`
313
+ + `\nFailures: ${b.quarantined} run-quarantined unresolved, ${b.resolved} resolved`
314
+ + `\nQuest claims held: ${b.claims}`;
315
+ });
316
+ }
317
+ if (action === 'debrief') {
318
+ const projectPath = asString('project_path', args.project_path, { maxLen: 500 });
319
+ if (!projectPath)
320
+ throw new ValidationError('project_path', 'is required for action=debrief (the project the candidates file under)');
321
+ const project = ctx.store.getProject(projectPath);
322
+ if (!project)
323
+ return { content: [{ type: 'text', text: `Project not found: ${projectPath}` }], isError: true };
324
+ const rawLearnings = args.learnings;
325
+ if (!Array.isArray(rawLearnings) || rawLearnings.length === 0) {
326
+ throw new ValidationError('learnings', 'is required for action=debrief — an array of { agent_id?, text }');
327
+ }
328
+ if (rawLearnings.length > 64) {
329
+ throw new ValidationError('learnings', 'too many entries (max 64 per debrief call)');
330
+ }
331
+ // Boundary bound (F3 security pass #1, confirmed finding): every
332
+ // sibling string param is length-bounded; an unbounded learning
333
+ // would materialize a proportional segment array in the
334
+ // deterministic extractor (auto-capture.ts splits the FULL text —
335
+ // only the LLM path slices to 8000) — a memory-DoS on the shared
336
+ // stdio server. 32K chars ≫ any honest learning; oversize is a
337
+ // deterministic ValidationError, not a silent skip.
338
+ // Second-round review of that fix (confirmed): the bound must reject
339
+ // BEFORE any sibling is extracted/queued — checked mid-loop, an
340
+ // oversize entry at index N threw AFTER entries 0..N-1 had already
341
+ // written review-queue artifacts: a partial commit hiding behind a
342
+ // ValidationError that reads as "nothing happened". Pre-scan the
343
+ // whole batch at the boundary (T020: validation precedes side
344
+ // effects); the >=20 floor / non-string per-entry SKIP semantics
345
+ // stay in the processing loop unchanged.
346
+ for (const entry of rawLearnings) {
347
+ const t = entry && typeof entry === 'object' ? entry.text : undefined;
348
+ if (typeof t === 'string')
349
+ asString('learnings[].text', t, { maxLen: 32_000 });
350
+ }
351
+ let queued = 0;
352
+ let skipped = 0;
353
+ let candidateCount = 0;
354
+ const methods = new Set();
355
+ for (const entry of rawLearnings) {
356
+ const rawText = entry && typeof entry === 'object' ? entry.text : undefined;
357
+ if (typeof rawText !== 'string' || rawText.trim().length < 20) {
358
+ skipped++;
359
+ continue;
360
+ }
361
+ const text = rawText; // length pre-validated above (whole batch, pre-side-effect)
362
+ const submitter = asIdentity('learnings[].agent_id', entry.agent_id)
363
+ ?? ambient.agent_id;
364
+ // The EXISTING auto_capture pipeline: local Ollama (WYRM_EXTRACT_MODEL,
365
+ // the DragonSpark slot) or the deterministic fallback. Never a cloud
366
+ // LLM (Article III; locked by tests/run-lifecycle.test.ts).
367
+ const { candidates, method } = await extractCandidates(text);
368
+ methods.add(method);
369
+ candidateCount += candidates.length;
370
+ for (const c of candidates) {
371
+ const a = candidateToArtifact(c);
372
+ const sig = a.tags[a.tags.length - 1]; // the 'ax:' dedup signature (auto_capture convention)
373
+ // Security pass #1: sig is learning-derived, so its %/_ must be
374
+ // LITERAL in the probe (escapeLikePattern + ESCAPE) — unescaped,
375
+ // a wildcard-bearing learning broad-matched other ax: tags and
376
+ // silently suppressed its own capture.
377
+ if (db.prepare("SELECT 1 FROM memory_artifacts WHERE project_id = ? AND tags LIKE ? ESCAPE '\\' LIMIT 1")
378
+ .get(project.id, '%' + escapeLikePattern(sig) + '%')) {
379
+ skipped++;
380
+ continue;
381
+ }
382
+ // RUN-SCOPED review queue: stamp the SUBMITTING agent + THIS run
383
+ // on the artifact row (memory.add reads the ambient envelope) and
384
+ // tag it run:<id> so the queue slices per run.
385
+ runWithActor({ agent_id: submitter, run_id: runId, source: 'param' }, () => ctx.memory.add(project.id, {
386
+ kind: a.kind,
387
+ problem: a.problem,
388
+ tags: [...a.tags, `run:${runId}`],
389
+ confidence: a.confidence,
390
+ needsReview: 1,
391
+ createdBy: 'run-debrief',
392
+ }));
393
+ queued++;
394
+ }
395
+ }
396
+ touchRun(db, runId);
397
+ if (queued > 0)
398
+ ctx.cache.invalidate();
399
+ const body = {
400
+ action: 'debrief',
401
+ run_id: runId,
402
+ status: run.status,
403
+ method: methods.size === 1 ? [...methods][0] : methods.size === 0 ? 'none' : 'mixed',
404
+ submitted: rawLearnings.length,
405
+ candidates: candidateCount,
406
+ queued,
407
+ skipped,
408
+ };
409
+ return renderResult(body, (b, g) => withGlyph(g.brand, `Run ${b.run_id} debrief (${b.method}): ${b.submitted} learning(s) -> `
410
+ + `${b.queued} candidate(s) queued for review, ${b.skipped} skipped.`)
411
+ + `\nApprove/reject with wyrm_review (run-scoped tag run:${b.run_id}).`);
412
+ }
413
+ // ── end ───────────────────────────────────────────────────────────────
414
+ if (run.status !== 'running') {
415
+ return { content: [{ type: 'text', text: `Run ${runId} already ended (status: ${run.status}).` }], isError: true };
416
+ }
417
+ // end authority (security pass #1): end is MEMBER-only — it sets the
418
+ // terminal status, drives the quarantine verdict, and force-releases
419
+ // EVERY claim the run holds (releaseRunClaims is a run-wide bulk
420
+ // delete, unlike the holder-scoped releaseClaim), so an outsider with
421
+ // the run_id must not be able to tear a live fleet's leases down.
422
+ const endAgent = asIdentity('agent_id', args.agent_id) ?? ambient.agent_id;
423
+ if (!endAgent) {
424
+ throw new ValidationError('agent_id', 'no agent identity — action=end is member-only; pass agent_id or supply the actor envelope (actor param / _meta / WYRM_AGENT_ID)');
425
+ }
426
+ if (!isRunMember(db, runId, endAgent)) {
427
+ return { content: [{ type: 'text', text: `'${endAgent}' is not a registered member of run ${runId} — only run members may end it (action=join first).` }], isError: true };
428
+ }
429
+ const terminal = asEnum('status', args.status, TERMINAL_STATUSES, 'completed');
430
+ const projectPath = asString('project_path', args.project_path, { maxLen: 500 });
431
+ const project = projectPath ? ctx.store.getProject(projectPath) : undefined;
432
+ if (projectPath && !project) {
433
+ return { content: [{ type: 'text', text: `Project not found: ${projectPath}` }], isError: true };
434
+ }
435
+ // Snapshot this run's unresolved quarantined failures BEFORE the sweep so
436
+ // the per-run promoted/expired counts are exact (the sweep itself reports
437
+ // globals across every settled run).
438
+ const snapshot = db.prepare(`
22
439
  SELECT id FROM failure_patterns
23
440
  WHERE run_id = ? AND quarantine_scope = 'run' AND resolved = 0
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?`
25
- Summary artifact #${i.summary_artifact_id} filed.`:`
26
- No project_path given -- summary artifact skipped.`))}}];export{ee as createRun,v as getAgents,O as getRun,q as registerAgent,le as runToolSpecs,te as setRunStatus,C as touchRun};
441
+ `).all(runId).map((r) => r.id);
442
+ setRunStatus(db, runId, terminal);
443
+ // Promote/expire via the T015 API: with the terminal status set, the
444
+ // sweep's EXPLICIT-VERDICT paths act on this run (completed/failed →
445
+ // promote to project/global; abandoned → TTL auto-expiry) — they work
446
+ // even in WYRM_LIVE_MEMORY=0 degraded mode.
447
+ ctx.failures.sweepRunQuarantine();
448
+ let promoted = 0;
449
+ let expired = 0;
450
+ if (snapshot.length > 0) {
451
+ const stmt = db.prepare('SELECT quarantine_scope, resolved FROM failure_patterns WHERE id = ?');
452
+ for (const id of snapshot) {
453
+ const row = stmt.get(id);
454
+ if (!row)
455
+ continue;
456
+ if (row.quarantine_scope !== 'run')
457
+ promoted++;
458
+ else if (row.resolved === 1)
459
+ expired++;
460
+ }
461
+ }
462
+ const claimsReleased = ctx.presence.releaseRunClaims(runId);
463
+ // Security pass #1: evict this run's cached fleet briefs eagerly — a
464
+ // brief is only meaningful DURING its run, and the run_briefs cache is
465
+ // mintable via soft-ref run_id, so waiting 90 days for the maintenance
466
+ // retention prune left a growth window. (Maintenance also gained a
467
+ // count cap — WYRM_RUN_BRIEFS_MAX — for runs that never end cleanly.)
468
+ try {
469
+ db.prepare('DELETE FROM run_briefs WHERE run_id = ?').run(runId);
470
+ }
471
+ catch { /* run_briefs is a cache — never fail end on its eviction */ }
472
+ // The run summary artifact (spec FR-5: "end writes the run summary
473
+ // artifact") — filed when a project is given; runs are not project-bound,
474
+ // so a project-less end skips it visibly (summary_artifact_id: null).
475
+ let summaryArtifactId = null;
476
+ if (project) {
477
+ const agents = getAgents(db, runId);
478
+ const summary = ctx.memory.add(project.id, {
479
+ kind: 'reasoning_trace',
480
+ problem: `Fleet run ${runId} ended ${terminal}: ${agents.length} agent(s), `
481
+ + `${promoted} quarantined failure(s) promoted, ${expired} expired, ${claimsReleased} quest claim(s) released.`,
482
+ outcome: terminal === 'completed' ? 'positive' : terminal === 'failed' ? 'negative' : 'neutral',
483
+ tags: ['run-summary', `run:${runId}`],
484
+ confidence: 1.0,
485
+ createdBy: 'wyrm_run',
486
+ });
487
+ summaryArtifactId = summary.id;
488
+ db.prepare('UPDATE runs SET debrief_artifact_id = ? WHERE run_id = ?').run(summaryArtifactId, runId);
489
+ }
490
+ ctx.cache.invalidate();
491
+ const body = {
492
+ action: 'end',
493
+ run_id: runId,
494
+ status: terminal,
495
+ promoted,
496
+ expired,
497
+ claims_released: claimsReleased,
498
+ summary_artifact_id: summaryArtifactId,
499
+ };
500
+ return renderResult(body, (b, g) => withGlyph(g.brand, `Run ${b.run_id} ended (${b.status}): ${b.promoted} failure(s) promoted, `
501
+ + `${b.expired} expired, ${b.claims_released} claim(s) released.`)
502
+ + (b.summary_artifact_id !== null
503
+ ? `\nSummary artifact #${b.summary_artifact_id} filed.`
504
+ : '\nNo project_path given -- summary artifact skipped.'));
505
+ },
506
+ },
507
+ ];
508
+ //# sourceMappingURL=run.js.map