@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.30

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 (59) hide show
  1. package/dist/bin/rig-agent-dispatch.js +1165 -785
  2. package/dist/bin/rig-agent.js +458 -389
  3. package/dist/src/control-plane/agent-wrapper.js +1191 -504
  4. package/dist/src/control-plane/authority-files.js +12 -6
  5. package/dist/src/control-plane/harness-main.js +2186 -1786
  6. package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
  7. package/dist/src/control-plane/hooks/inject-context.js +193 -139
  8. package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
  9. package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
  10. package/dist/src/control-plane/materialize-task-config.js +64 -8
  11. package/dist/src/control-plane/native/git-ops.js +90 -64
  12. package/dist/src/control-plane/native/harness-cli.js +1989 -682
  13. package/dist/src/control-plane/native/pr-automation.js +1657 -54
  14. package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
  15. package/dist/src/control-plane/native/repo-ops.js +3 -0
  16. package/dist/src/control-plane/native/run-ops.js +39 -13
  17. package/dist/src/control-plane/native/task-ops.js +1819 -527
  18. package/dist/src/control-plane/native/validator.js +163 -109
  19. package/dist/src/control-plane/native/verifier.js +1616 -323
  20. package/dist/src/control-plane/native/workspace-ops.js +12 -6
  21. package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
  22. package/dist/src/control-plane/pi-sessiond/client.js +41 -0
  23. package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
  24. package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
  25. package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
  26. package/dist/src/control-plane/pi-sessiond/server.js +802 -0
  27. package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
  28. package/dist/src/control-plane/pi-sessiond/types.js +1 -0
  29. package/dist/src/control-plane/plugin-host-context.js +54 -0
  30. package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
  31. package/dist/src/control-plane/runtime/image/index.js +3 -0
  32. package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
  33. package/dist/src/control-plane/runtime/image.js +3 -0
  34. package/dist/src/control-plane/runtime/index.js +517 -722
  35. package/dist/src/control-plane/runtime/isolation/home.js +28 -6
  36. package/dist/src/control-plane/runtime/isolation/index.js +541 -461
  37. package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
  38. package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
  39. package/dist/src/control-plane/runtime/isolation.js +541 -461
  40. package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
  41. package/dist/src/control-plane/runtime/queue.js +458 -385
  42. package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
  43. package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
  44. package/dist/src/control-plane/skill-materializer.js +46 -0
  45. package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
  46. package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
  47. package/dist/src/index.js +27 -298
  48. package/dist/src/layout.js +12 -7
  49. package/dist/src/local-server.js +20 -14
  50. package/native/darwin-arm64/rig-git +0 -0
  51. package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
  52. package/native/darwin-arm64/rig-shell +0 -0
  53. package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
  54. package/native/darwin-arm64/rig-tools +0 -0
  55. package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
  56. package/native/darwin-arm64/runtime-native.dylib +0 -0
  57. package/package.json +8 -6
  58. package/dist/src/control-plane/runtime/plugins.js +0 -1131
  59. package/dist/src/plugins.js +0 -329
@@ -0,0 +1,540 @@
1
+ // @bun
2
+ // packages/runtime/src/control-plane/pi-sessiond/session-service.ts
3
+ import { randomUUID as randomUUID2 } from "crypto";
4
+ import { mkdirSync } from "fs";
5
+ import { join } from "path";
6
+ import {
7
+ AuthStorage,
8
+ createAgentSessionFromServices,
9
+ createAgentSessionRuntime,
10
+ createAgentSessionServices,
11
+ ModelRegistry,
12
+ SessionManager
13
+ } from "@earendil-works/pi-coding-agent";
14
+
15
+ // packages/runtime/src/control-plane/pi-sessiond/extension-ui-context.ts
16
+ import { randomUUID } from "crypto";
17
+ import {
18
+ initTheme
19
+ } from "@earendil-works/pi-coding-agent";
20
+ var headlessTheme = {
21
+ fg: (_color, text) => text,
22
+ bg: (_color, text) => text,
23
+ dim: (text) => text,
24
+ bold: (text) => text,
25
+ italic: (text) => text,
26
+ underline: (text) => text,
27
+ strikethrough: (text) => text,
28
+ reset: (text) => text,
29
+ getFgAnsi: () => "",
30
+ getBgAnsi: () => "",
31
+ name: "rig-headless"
32
+ };
33
+ function ensureThemeInitialized() {
34
+ try {
35
+ initTheme(undefined, false);
36
+ } catch {}
37
+ }
38
+
39
+ class RigPiExtensionUiBridge {
40
+ sessionId;
41
+ runId;
42
+ publish;
43
+ pending = new Map;
44
+ constructor(sessionId, runId, publish) {
45
+ this.sessionId = sessionId;
46
+ this.runId = runId;
47
+ this.publish = publish;
48
+ ensureThemeInitialized();
49
+ }
50
+ createContext() {
51
+ const dialog = (opts, defaultValue, request, parse) => {
52
+ if (opts?.signal?.aborted)
53
+ return Promise.resolve(defaultValue);
54
+ const id = randomUUID();
55
+ return new Promise((resolve) => {
56
+ let timeout;
57
+ const cleanup = () => {
58
+ if (timeout)
59
+ clearTimeout(timeout);
60
+ opts?.signal?.removeEventListener("abort", onAbort);
61
+ this.pending.delete(id);
62
+ };
63
+ const finish = (value) => {
64
+ cleanup();
65
+ resolve(value);
66
+ };
67
+ const onAbort = () => finish(defaultValue);
68
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
69
+ if (opts?.timeout)
70
+ timeout = setTimeout(() => finish(defaultValue), opts.timeout);
71
+ this.pending.set(id, {
72
+ timeout,
73
+ abort: onAbort,
74
+ resolve: (raw) => finish(parse(normalizeResponse(raw)))
75
+ });
76
+ this.publish({
77
+ type: "extension_ui_request",
78
+ sessionId: this.sessionId,
79
+ runId: this.runId,
80
+ request: { id, timeout: opts?.timeout, ...request }
81
+ });
82
+ });
83
+ };
84
+ return {
85
+ select: (title, options, opts) => dialog(opts, undefined, { method: "select", title, options }, (response) => {
86
+ if (response.cancelled)
87
+ return;
88
+ return typeof response.value === "string" ? response.value : undefined;
89
+ }),
90
+ confirm: (title, message, opts) => dialog(opts, false, { method: "confirm", title, message }, (response) => {
91
+ if (response.cancelled)
92
+ return false;
93
+ return response.confirmed === true || response.value === true;
94
+ }),
95
+ input: (title, placeholder, opts) => dialog(opts, undefined, { method: "input", title, placeholder }, (response) => {
96
+ if (response.cancelled)
97
+ return;
98
+ return typeof response.value === "string" ? response.value : undefined;
99
+ }),
100
+ notify: (message, type) => {
101
+ this.fire({ method: "notify", message, notifyType: type });
102
+ },
103
+ onTerminalInput: () => () => {},
104
+ setStatus: (key, text) => {
105
+ this.fire({ method: "setStatus", statusKey: key, statusText: text });
106
+ },
107
+ setWorkingMessage: (message) => {
108
+ this.fire({ method: "setWorkingMessage", message });
109
+ },
110
+ setWorkingVisible: (visible) => {
111
+ this.fire({ method: "setWorkingVisible", visible });
112
+ },
113
+ setWorkingIndicator: (options) => {
114
+ this.fire({ method: "setWorkingIndicator", options });
115
+ },
116
+ setHiddenThinkingLabel: (label) => {
117
+ this.fire({ method: "setHiddenThinkingLabel", label });
118
+ },
119
+ setWidget: (key, content, options) => {
120
+ if (content === undefined || Array.isArray(content)) {
121
+ this.fire({ method: "setWidget", widgetKey: key, widgetLines: content, widgetPlacement: options?.placement });
122
+ return;
123
+ }
124
+ this.fire({ method: "notify", message: `Extension widget '${key}' uses a component factory that Rig daemon mode cannot render.`, notifyType: "warning" });
125
+ },
126
+ setFooter: (factory) => {
127
+ if (factory !== undefined)
128
+ this.fire({ method: "notify", message: "Custom extension footers are not rendered in Rig daemon mode.", notifyType: "warning" });
129
+ },
130
+ setHeader: (factory) => {
131
+ if (factory !== undefined)
132
+ this.fire({ method: "notify", message: "Custom extension headers are not rendered in Rig daemon mode.", notifyType: "warning" });
133
+ },
134
+ setTitle: (title) => {
135
+ this.fire({ method: "setTitle", title });
136
+ },
137
+ custom: async () => {
138
+ this.fire({ method: "notify", message: "Custom extension UI components are not supported in Rig daemon mode.", notifyType: "warning" });
139
+ return;
140
+ },
141
+ pasteToEditor: (text) => {
142
+ this.fire({ method: "set_editor_text", text });
143
+ },
144
+ setEditorText: (text) => {
145
+ this.fire({ method: "set_editor_text", text });
146
+ },
147
+ getEditorText: () => "",
148
+ editor: (title, prefill) => dialog(undefined, undefined, { method: "editor", title, prefill }, (response) => {
149
+ if (response.cancelled)
150
+ return;
151
+ return typeof response.value === "string" ? response.value : undefined;
152
+ }),
153
+ addAutocompleteProvider: () => {},
154
+ setEditorComponent: (factory) => {
155
+ if (factory !== undefined)
156
+ this.fire({ method: "notify", message: "Custom editor components are not supported in Rig daemon mode.", notifyType: "warning" });
157
+ },
158
+ getEditorComponent: () => {
159
+ return;
160
+ },
161
+ get theme() {
162
+ return headlessTheme;
163
+ },
164
+ getAllThemes: () => [],
165
+ getTheme: () => {
166
+ return;
167
+ },
168
+ setTheme: () => ({ success: false, error: "Theme switching is not supported in Rig daemon mode" }),
169
+ getToolsExpanded: () => false,
170
+ setToolsExpanded: () => {}
171
+ };
172
+ }
173
+ respond(input) {
174
+ const pending = this.pending.get(input.requestId);
175
+ if (!pending)
176
+ return false;
177
+ pending.resolve(input);
178
+ return true;
179
+ }
180
+ cancelAll() {
181
+ for (const [id, pending] of this.pending.entries()) {
182
+ if (pending.timeout)
183
+ clearTimeout(pending.timeout);
184
+ pending.abort?.();
185
+ this.pending.delete(id);
186
+ pending.resolve({ requestId: id, cancelled: true });
187
+ }
188
+ }
189
+ fire(request) {
190
+ this.publish({
191
+ type: "extension_ui_request",
192
+ sessionId: this.sessionId,
193
+ runId: this.runId,
194
+ request: { id: randomUUID(), ...request }
195
+ });
196
+ }
197
+ }
198
+ function normalizeResponse(value) {
199
+ if (!value || typeof value !== "object" || Array.isArray(value))
200
+ return { requestId: "", cancelled: true };
201
+ const record = value;
202
+ return {
203
+ requestId: typeof record.requestId === "string" ? record.requestId : "",
204
+ value: record.value,
205
+ confirmed: typeof record.confirmed === "boolean" ? record.confirmed : undefined,
206
+ cancelled: record.cancelled === true
207
+ };
208
+ }
209
+
210
+ // packages/runtime/src/control-plane/pi-sessiond/session-service.ts
211
+ var BUILTIN_COMMANDS = [
212
+ { name: "session", description: "Show session info and stats", source: "builtin" },
213
+ { name: "name", description: "Set session display name", source: "builtin" },
214
+ { name: "compact", description: "Manually compact session context", source: "builtin" },
215
+ { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes", source: "builtin" },
216
+ { name: "quit", description: "Detach from the current local Pi frontend", source: "builtin" }
217
+ ];
218
+
219
+ class RigPiSessionService {
220
+ hub;
221
+ config;
222
+ activeBySession = new Map;
223
+ sessionIdByRun = new Map;
224
+ heartbeat;
225
+ constructor(hub, config) {
226
+ this.hub = hub;
227
+ this.config = config;
228
+ this.heartbeat = setInterval(() => this.publishHeartbeats(), 2000);
229
+ }
230
+ activeCount() {
231
+ return this.activeBySession.size;
232
+ }
233
+ async dispose() {
234
+ clearInterval(this.heartbeat);
235
+ const sessions = [...this.activeBySession.values()];
236
+ this.activeBySession.clear();
237
+ this.sessionIdByRun.clear();
238
+ await Promise.all(sessions.map(async (active) => {
239
+ active.unsubscribe();
240
+ active.ui.cancelAll();
241
+ try {
242
+ await active.runtime.session.abort();
243
+ } catch {}
244
+ await active.runtime.dispose();
245
+ }));
246
+ }
247
+ async startSession(input) {
248
+ const existing = this.getByRun(input.runId);
249
+ if (existing)
250
+ return existing.metadata;
251
+ mkdirSync(input.agentDir, { recursive: true });
252
+ mkdirSync(input.sessionDir, { recursive: true });
253
+ const authStorage = AuthStorage.create(join(input.agentDir, "auth.json"));
254
+ const modelRegistry = ModelRegistry.create(authStorage, join(input.agentDir, "models.json"));
255
+ const createRuntime = async ({ cwd, agentDir, sessionManager: sessionManager2, sessionStartEvent }) => {
256
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
257
+ const result = await createAgentSessionFromServices({ services, sessionManager: sessionManager2, sessionStartEvent });
258
+ return { ...result, services, diagnostics: services.diagnostics };
259
+ };
260
+ const sessionManager = SessionManager.create(input.cwd, input.sessionDir);
261
+ const runtime = await createAgentSessionRuntime(createRuntime, {
262
+ cwd: input.cwd,
263
+ agentDir: input.agentDir,
264
+ sessionManager,
265
+ sessionStartEvent: { type: "session_start", reason: "new" }
266
+ });
267
+ const now = new Date().toISOString();
268
+ const metadata = {
269
+ runId: input.runId,
270
+ sessionId: runtime.session.sessionId,
271
+ daemonId: this.config.daemonId,
272
+ cwd: input.cwd,
273
+ agentDir: input.agentDir,
274
+ sessionDir: input.sessionDir,
275
+ transport: "http-control-ws-events",
276
+ serverBasePath: `/api/runs/${encodeURIComponent(input.runId)}/pi`,
277
+ eventsPath: `/api/runs/${encodeURIComponent(input.runId)}/pi/events`,
278
+ createdAt: now,
279
+ updatedAt: now,
280
+ backend: {
281
+ kind: "worker-pi-sessiond",
282
+ version: this.config.version,
283
+ commit: this.config.commit,
284
+ pid: process.pid
285
+ }
286
+ };
287
+ const active = {
288
+ runId: input.runId,
289
+ runtime,
290
+ metadata,
291
+ unsubscribe: () => {},
292
+ ui: new RigPiExtensionUiBridge(runtime.session.sessionId, input.runId, (event) => this.publish(runtime.session.sessionId, input.runId, event))
293
+ };
294
+ await this.bindRuntime(active);
295
+ if (input.sessionName?.trim())
296
+ runtime.session.setSessionName(input.sessionName.trim());
297
+ this.activeBySession.set(runtime.session.sessionId, active);
298
+ this.sessionIdByRun.set(input.runId, runtime.session.sessionId);
299
+ this.publish(runtime.session.sessionId, input.runId, { type: "ready", metadata });
300
+ this.publishStatus(active);
301
+ this.publishActivity(active, "ready", "idle");
302
+ return metadata;
303
+ }
304
+ getByRun(runId) {
305
+ const sessionId = this.sessionIdByRun.get(runId);
306
+ return sessionId ? this.activeBySession.get(sessionId) : undefined;
307
+ }
308
+ getBySession(sessionId) {
309
+ return this.activeBySession.get(sessionId);
310
+ }
311
+ messages(sessionId) {
312
+ const active = this.requireActive(sessionId);
313
+ return [...active.runtime.session.messages];
314
+ }
315
+ status(sessionId) {
316
+ return this.statusFromActive(this.requireActive(sessionId));
317
+ }
318
+ commands(sessionId) {
319
+ const session = this.requireActive(sessionId).runtime.session;
320
+ const commands = [...BUILTIN_COMMANDS];
321
+ for (const command of session.extensionRunner.getRegisteredCommands()) {
322
+ commands.push({ name: command.invocationName, description: command.description, source: "extension" });
323
+ }
324
+ for (const template of session.promptTemplates) {
325
+ commands.push({ name: template.name, description: template.description, source: "prompt" });
326
+ }
327
+ for (const skill of session.resourceLoader.getSkills().skills) {
328
+ commands.push({ name: `skill:${skill.name}`, description: skill.description, source: "skill" });
329
+ }
330
+ return commands.sort((a, b) => a.name.localeCompare(b.name));
331
+ }
332
+ async prompt(sessionId, text, streamingBehavior) {
333
+ const active = this.requireActive(sessionId);
334
+ const session = active.runtime.session;
335
+ const behavior = session.isStreaming || session.isCompacting ? streamingBehavior ?? "steer" : undefined;
336
+ this.publishActivity(active, behavior === "followUp" ? "follow-up queued" : behavior === "steer" ? "steering queued" : "prompt accepted", "active");
337
+ session.prompt(text, behavior ? { streamingBehavior: behavior, source: "rpc" } : { source: "rpc" }).catch((error) => {
338
+ const message = error instanceof Error ? error.message : String(error);
339
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message });
340
+ this.publishActivity(active, "prompt failed", "error", message);
341
+ });
342
+ this.publishStatus(active);
343
+ return { accepted: true };
344
+ }
345
+ async shell(sessionId, text) {
346
+ const active = this.requireActive(sessionId);
347
+ const session = active.runtime.session;
348
+ const excluded = text.startsWith("!!");
349
+ const command = (excluded ? text.slice(2) : text.startsWith("!") ? text.slice(1) : text).trim();
350
+ if (!command)
351
+ throw new Error("Usage: !<shell command>");
352
+ if (session.isBashRunning)
353
+ throw new Error("A bash command is already running");
354
+ this.publishActivity(active, "running bash", "active", command);
355
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.start", command, excludeFromContext: excluded } });
356
+ session.executeBash(command, (chunk) => {
357
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.chunk", chunk } });
358
+ this.publishActivity(active, "running bash", "active", command);
359
+ this.publishStatus(active);
360
+ }, { excludeFromContext: excluded }).then((result) => {
361
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.end", ...result } });
362
+ this.publishActivity(active, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
363
+ this.publishStatus(active);
364
+ }).catch((error) => {
365
+ const message = error instanceof Error ? error.message : String(error);
366
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "pi.ui_event", sessionId, runId: active.runId, event: { type: "shell.end", output: message, isError: true } });
367
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message });
368
+ this.publishActivity(active, "bash failed", "error", message);
369
+ this.publishStatus(active);
370
+ });
371
+ return { accepted: true };
372
+ }
373
+ async runCommand(sessionId, text) {
374
+ const active = this.requireActive(sessionId);
375
+ const trimmed = text.trim();
376
+ const [rawName = "", ...args] = trimmed.replace(/^\//, "").split(/\s+/);
377
+ const rest = args.join(" ").trim();
378
+ if (rawName === "session")
379
+ return { type: "done", message: formatSessionStats(active.runtime.session) };
380
+ if (rawName === "name") {
381
+ if (!rest)
382
+ return { type: "unsupported", message: "Usage: /name <session name>" };
383
+ active.runtime.session.setSessionName(rest);
384
+ this.publishStatus(active);
385
+ return { type: "done", message: `Session named: ${rest}` };
386
+ }
387
+ if (rawName === "compact") {
388
+ active.runtime.session.compact(rest || undefined).catch((error) => {
389
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "error", message: error instanceof Error ? error.message : String(error) });
390
+ });
391
+ return { type: "done", message: "Compaction started\u2026" };
392
+ }
393
+ if (rawName === "reload") {
394
+ await active.runtime.session.reload();
395
+ await this.bindRuntime(active);
396
+ return { type: "done", message: "Pi resources reloaded" };
397
+ }
398
+ if (rawName === "quit")
399
+ return { type: "done", message: "Detach locally with /detach or /quit." };
400
+ await this.prompt(sessionId, trimmed, active.runtime.session.isStreaming ? "steer" : undefined);
401
+ return { type: "done", message: `Accepted ${trimmed}` };
402
+ }
403
+ respondToCommand(_sessionId, _requestId, _value) {
404
+ return { type: "unsupported", message: "No pending command selection is active." };
405
+ }
406
+ respondToExtensionUi(sessionId, input) {
407
+ const active = this.requireActive(sessionId);
408
+ return { accepted: active.ui.respond(input) };
409
+ }
410
+ async abort(sessionId) {
411
+ const active = this.requireActive(sessionId);
412
+ active.runtime.session.clearQueue();
413
+ await active.runtime.session.abort();
414
+ this.publishActivity(active, "stopped", "idle");
415
+ this.publishStatus(active);
416
+ return { aborted: true };
417
+ }
418
+ async bindRuntime(active) {
419
+ active.unsubscribe();
420
+ const session = active.runtime.session;
421
+ active.ui.cancelAll();
422
+ active.ui = new RigPiExtensionUiBridge(session.sessionId, active.runId, (event) => this.publish(session.sessionId, active.runId, event));
423
+ active.metadata = { ...active.metadata, sessionId: session.sessionId, updatedAt: new Date().toISOString() };
424
+ this.sessionIdByRun.set(active.runId, session.sessionId);
425
+ this.activeBySession.set(session.sessionId, active);
426
+ active.runtime.setRebindSession(async () => this.bindRuntime(active));
427
+ await session.bindExtensions({
428
+ uiContext: active.ui.createContext(),
429
+ commandContextActions: {
430
+ waitForIdle: () => session.agent.waitForIdle(),
431
+ newSession: async (options) => active.runtime.newSession(options),
432
+ fork: async (entryId, options) => {
433
+ const result = await active.runtime.fork(entryId, options);
434
+ return { cancelled: result.cancelled };
435
+ },
436
+ navigateTree: async (targetId, options) => session.navigateTree(targetId, options),
437
+ switchSession: async (sessionPath, options) => active.runtime.switchSession(sessionPath, options),
438
+ reload: async () => {
439
+ await session.reload();
440
+ }
441
+ },
442
+ shutdownHandler: () => {
443
+ this.abort(session.sessionId);
444
+ },
445
+ onError: (error) => {
446
+ this.publish(session.sessionId, active.runId, { type: "error", message: error.error, detail: error });
447
+ }
448
+ });
449
+ active.unsubscribe = session.subscribe((event) => {
450
+ this.publish(session.sessionId, active.runId, { type: "pi.event", sessionId: session.sessionId, runId: active.runId, event });
451
+ this.publishActivityForEvent(active, event);
452
+ this.publishStatus(active);
453
+ });
454
+ }
455
+ publish(sessionId, runId, event) {
456
+ this.hub.publish(sessionId, runId, event);
457
+ }
458
+ publishStatus(active) {
459
+ const status = this.statusFromActive(active);
460
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "status.update", sessionId: active.runtime.session.sessionId, runId: active.runId, status });
461
+ }
462
+ publishActivity(active, label, phase, detail) {
463
+ const activity = {
464
+ sessionId: active.runtime.session.sessionId,
465
+ runId: active.runId,
466
+ phase,
467
+ label,
468
+ detail,
469
+ at: new Date().toISOString()
470
+ };
471
+ active.activity = activity;
472
+ this.publish(active.runtime.session.sessionId, active.runId, { type: "activity.update", sessionId: active.runtime.session.sessionId, runId: active.runId, activity });
473
+ }
474
+ publishActivityForEvent(active, event) {
475
+ const type = event && typeof event === "object" && !Array.isArray(event) ? event.type : undefined;
476
+ if (type === "agent_start")
477
+ this.publishActivity(active, "agent running", "active");
478
+ else if (type === "agent_end")
479
+ this.publishActivity(active, "idle", "idle");
480
+ else if (type === "tool_execution_start")
481
+ this.publishActivity(active, `tool: ${String(event.toolName ?? "tool")}`, "active");
482
+ else if (type === "compaction_start")
483
+ this.publishActivity(active, "compacting", "active");
484
+ else if (type === "compaction_end")
485
+ this.publishActivity(active, "compaction complete", "idle");
486
+ }
487
+ publishHeartbeats() {
488
+ for (const active of this.activeBySession.values()) {
489
+ if (active.runtime.session.isStreaming || active.runtime.session.isCompacting || active.runtime.session.isBashRunning || active.runtime.session.pendingMessageCount > 0) {
490
+ this.publishStatus(active);
491
+ this.publishActivity(active, active.activity?.label ?? "active", "active", active.activity?.detail);
492
+ }
493
+ }
494
+ }
495
+ statusFromActive(active) {
496
+ const session = active.runtime.session;
497
+ return {
498
+ sessionId: session.sessionId,
499
+ runId: active.runId,
500
+ cwd: active.runtime.cwd,
501
+ sessionFile: session.sessionFile,
502
+ sessionName: session.sessionName,
503
+ model: session.model,
504
+ thinkingLevel: session.thinkingLevel,
505
+ isStreaming: session.isStreaming,
506
+ isCompacting: session.isCompacting,
507
+ isBashRunning: session.isBashRunning,
508
+ pendingMessageCount: session.pendingMessageCount,
509
+ messageCount: session.messages.length,
510
+ steeringMessages: session.getSteeringMessages(),
511
+ followUpMessages: session.getFollowUpMessages(),
512
+ contextUsage: session.getContextUsage(),
513
+ stats: session.getSessionStats()
514
+ };
515
+ }
516
+ requireActive(sessionId) {
517
+ const active = this.activeBySession.get(sessionId);
518
+ if (!active)
519
+ throw new Error("Pi session not found");
520
+ return active;
521
+ }
522
+ }
523
+ function formatSessionStats(session) {
524
+ const stats = session.getSessionStats();
525
+ return [
526
+ `Session: ${stats.sessionId}`,
527
+ `Messages: ${stats.totalMessages} (${stats.userMessages} user, ${stats.assistantMessages} assistant)`,
528
+ `Tool calls: ${stats.toolCalls}`,
529
+ `Tokens: \u2191${stats.tokens.input} \u2193${stats.tokens.output} total ${stats.tokens.total}`,
530
+ `Cost: $${stats.cost.toFixed(4)}`
531
+ ].join(`
532
+ `);
533
+ }
534
+ function createDaemonId() {
535
+ return `rig-pi-sessiond-${randomUUID2()}`;
536
+ }
537
+ export {
538
+ createDaemonId,
539
+ RigPiSessionService
540
+ };
@@ -0,0 +1 @@
1
+ // @bun
@@ -283,6 +283,49 @@ function safeReadJson(path) {
283
283
  }
284
284
  }
285
285
 
286
+ // packages/runtime/src/control-plane/skill-materializer.ts
287
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
288
+ import { resolve as resolve2 } from "path";
289
+ import { loadSkill } from "@rig/skill-loader";
290
+ var MARKER_FILENAME = ".rig-plugin";
291
+ function skillDirName(id) {
292
+ return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
293
+ }
294
+ async function materializeSkills(projectRoot, entries) {
295
+ const skillsRoot = resolve2(projectRoot, ".pi", "skills");
296
+ if (existsSync3(skillsRoot)) {
297
+ for (const name of readdirSync(skillsRoot)) {
298
+ const dir = resolve2(skillsRoot, name);
299
+ if (existsSync3(resolve2(dir, MARKER_FILENAME))) {
300
+ rmSync(dir, { recursive: true, force: true });
301
+ }
302
+ }
303
+ }
304
+ const written = [];
305
+ for (const { pluginName, skill } of entries) {
306
+ const sourcePath = resolve2(projectRoot, skill.path);
307
+ if (!existsSync3(sourcePath)) {
308
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
309
+ continue;
310
+ }
311
+ let body;
312
+ try {
313
+ await loadSkill(sourcePath);
314
+ body = readFileSync2(sourcePath, "utf-8");
315
+ } catch (err) {
316
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
317
+ continue;
318
+ }
319
+ const dir = resolve2(skillsRoot, skillDirName(skill.id));
320
+ mkdirSync2(dir, { recursive: true });
321
+ writeFileSync2(resolve2(dir, "SKILL.md"), body, "utf-8");
322
+ writeFileSync2(resolve2(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
323
+ `, "utf-8");
324
+ written.push({ id: skill.id, pluginName, directory: dir });
325
+ }
326
+ return written;
327
+ }
328
+
286
329
  // packages/runtime/src/control-plane/plugin-host-context.ts
287
330
  async function buildPluginHostContext(projectRoot) {
288
331
  let config;
@@ -319,6 +362,17 @@ async function buildPluginHostContext(projectRoot) {
319
362
  } catch (err) {
320
363
  console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
321
364
  }
365
+ try {
366
+ const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
367
+ pluginName: plugin.name,
368
+ skill
369
+ })));
370
+ if (skillEntries.length > 0) {
371
+ await materializeSkills(projectRoot, skillEntries);
372
+ }
373
+ } catch (err) {
374
+ console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
375
+ }
322
376
  return {
323
377
  config,
324
378
  pluginHost,
@@ -333,6 +333,9 @@ import { loadConfig } from "@rig/core/load-config";
333
333
  // packages/runtime/src/control-plane/repos/registry.ts
334
334
  var MANAGED_REPOS = new Map;
335
335
 
336
+ // packages/runtime/src/control-plane/skill-materializer.ts
337
+ import { loadSkill } from "@rig/skill-loader";
338
+
336
339
  // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
337
340
  var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
338
341
 
@@ -454,6 +454,9 @@ import { loadConfig } from "@rig/core/load-config";
454
454
  // packages/runtime/src/control-plane/repos/registry.ts
455
455
  var MANAGED_REPOS = new Map;
456
456
 
457
+ // packages/runtime/src/control-plane/skill-materializer.ts
458
+ import { loadSkill } from "@rig/skill-loader";
459
+
457
460
  // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
458
461
  var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
459
462
 
@@ -332,6 +332,9 @@ import { loadConfig } from "@rig/core/load-config";
332
332
  // packages/runtime/src/control-plane/repos/registry.ts
333
333
  var MANAGED_REPOS = new Map;
334
334
 
335
+ // packages/runtime/src/control-plane/skill-materializer.ts
336
+ import { loadSkill } from "@rig/skill-loader";
337
+
335
338
  // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
336
339
  var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
337
340
 
@@ -454,6 +454,9 @@ import { loadConfig } from "@rig/core/load-config";
454
454
  // packages/runtime/src/control-plane/repos/registry.ts
455
455
  var MANAGED_REPOS = new Map;
456
456
 
457
+ // packages/runtime/src/control-plane/skill-materializer.ts
458
+ import { loadSkill } from "@rig/skill-loader";
459
+
457
460
  // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
458
461
  var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
459
462