wyrm-mcp 7.2.0 → 7.2.2

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