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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/handlers/run.js
CHANGED
|
@@ -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(
|
|
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(
|
|
213
|
-
|
|
214
|
-
|
|
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(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
431
|
-
|
|
432
|
-
|
|
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};
|