@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.
- package/dist/bin/rig-agent-dispatch.js +1165 -785
- package/dist/bin/rig-agent.js +458 -389
- package/dist/src/control-plane/agent-wrapper.js +1191 -504
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +2186 -1786
- package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
- package/dist/src/control-plane/hooks/inject-context.js +193 -139
- package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
- package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
- package/dist/src/control-plane/materialize-task-config.js +64 -8
- package/dist/src/control-plane/native/git-ops.js +90 -64
- package/dist/src/control-plane/native/harness-cli.js +1989 -682
- package/dist/src/control-plane/native/pr-automation.js +1657 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
- package/dist/src/control-plane/native/repo-ops.js +3 -0
- package/dist/src/control-plane/native/run-ops.js +39 -13
- package/dist/src/control-plane/native/task-ops.js +1819 -527
- package/dist/src/control-plane/native/validator.js +163 -109
- package/dist/src/control-plane/native/verifier.js +1616 -323
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
- package/dist/src/control-plane/pi-sessiond/client.js +41 -0
- package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
- package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
- package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
- package/dist/src/control-plane/pi-sessiond/server.js +802 -0
- package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
- package/dist/src/control-plane/pi-sessiond/types.js +1 -0
- package/dist/src/control-plane/plugin-host-context.js +54 -0
- package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image/index.js +3 -0
- package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image.js +3 -0
- package/dist/src/control-plane/runtime/index.js +517 -722
- package/dist/src/control-plane/runtime/isolation/home.js +28 -6
- package/dist/src/control-plane/runtime/isolation/index.js +541 -461
- package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +541 -461
- package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
- package/dist/src/control-plane/runtime/queue.js +458 -385
- package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
- package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
- package/dist/src/control-plane/skill-materializer.js +46 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
- package/dist/src/index.js +27 -298
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +8 -6
- package/dist/src/control-plane/runtime/plugins.js +0 -1131
- 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
|
|