@jmfederico/pi-web 1.202609.0 → 1.202610.0
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/README.md +3 -3
- package/dist/.plugins-ready +1 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-_1GwJfSs.js +4176 -0
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
- package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
- package/dist/client/index.html +4 -4
- package/dist/docker/piWebDockerCommandPlan.js +9 -1
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +24 -17
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/nativeServices/serviceAction.js +38 -26
- package/dist/nativeServices/serviceAction.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +3 -3
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-packages/captains-log/README.md +11 -0
- package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
- package/dist/pi-packages/captains-log/dist/browser/index.js +145 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +107 -0
- package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
- package/dist/pi-packages/captains-log/dist/companion.js +126 -0
- package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
- package/dist/pi-packages/captains-log/dist/server.js +210 -0
- package/dist/pi-packages/captains-log/dist/store.js +75 -0
- package/dist/pi-packages/captains-log/docs/usage.md +32 -0
- package/dist/pi-packages/captains-log/package.json +35 -0
- package/dist/pi-packages/captains-log/tsconfig.json +21 -0
- package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
- package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
- package/dist/pi-packages/relays/prompts/relay.md +1 -1
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +21 -13
- package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
- package/dist/pi-web-plugins/files/package.json +15 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/git/git-backend.js +5 -5
- package/dist/pi-web-plugins/git/server-plugin.js +5 -3
- package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
- package/dist/pi-web-plugins/mermaid/package.json +13 -0
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
- package/dist/pi-web-plugins/terminal/package.json +16 -0
- package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
- package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +159 -79
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
- package/dist/piWebVersionReport.js +12 -4
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +235 -23
- package/dist/plugin-api.js +1 -0
- package/dist/pluginRecoveryCli.js +3 -2
- package/dist/pluginRecoveryCli.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +32 -28
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +39 -20
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/knownPiPackages.js +12 -0
- package/dist/server/knownPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +2 -2
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +101 -7
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/notices/serverNoticeStore.js +45 -4
- package/dist/server/notices/serverNoticeStore.js.map +1 -1
- package/dist/server/piPackageService.js +5 -5
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +46 -12
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginLifecycle.js +26 -3
- package/dist/server/piWebPluginLifecycle.js.map +1 -1
- package/dist/server/piWebPluginService.js +37 -2
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/pluginCallbackDrain.js +35 -0
- package/dist/server/pluginCallbackDrain.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +740 -0
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +1028 -107
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
- package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
- package/dist/server/projects/projectLifecycleService.js +91 -0
- package/dist/server/projects/projectLifecycleService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +22 -9
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/projectMutationRoutes.js +38 -0
- package/dist/server/sessiond/projectMutationRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +24 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +99 -24
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +2 -2
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/clientSessionPreview.js +14 -0
- package/dist/server/sessions/clientSessionPreview.js.map +1 -0
- package/dist/server/sessions/piSessionEventConnections.js +65 -0
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +8 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +411 -102
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionActivityMarker.js +105 -0
- package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/sessionMediaIndex.js +184 -0
- package/dist/server/sessions/sessionMediaIndex.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +3 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +64 -4
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionUnreadStore.js +26 -1
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +7 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +7 -2
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/transcriptMessages.js +54 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -0
- package/dist/server/terminals/requiredTerminalService.js +103 -0
- package/dist/server/terminals/requiredTerminalService.js.map +1 -0
- package/dist/server/webSocketBridge.js +339 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +1 -1
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/filePreviewService.js +27 -3
- package/dist/server/workspaces/filePreviewService.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceCatalog.js +3 -1
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +23 -1
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/server-plugin-api.d.ts +216 -22
- package/dist/server-plugin-api.js +319 -2
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/serverPluginRecovery.js +4 -0
- package/dist/serverPluginRecovery.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +27 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +47 -17
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +28 -7
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +21 -1
- package/dist/shared/pluginApiTypes.js +0 -1
- package/dist/shared/pluginBackendProtocol.js +163 -2
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginIds.js +8 -1
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/requiredTerminalPlugin.js +6 -0
- package/dist/shared/requiredTerminalPlugin.js.map +1 -0
- package/dist/shared/serverNoticeContract.js +88 -0
- package/dist/shared/serverNoticeContract.js.map +1 -0
- package/dist/shared/sessionDefaults.js +41 -0
- package/dist/shared/sessionDefaults.js.map +1 -0
- package/dist/shared/sessionMedia.js +6 -0
- package/dist/shared/sessionMedia.js.map +1 -0
- package/docs/config.md +50 -36
- package/docs/plugins.md +157 -1212
- package/examples/session-bridge-plugin/README.md +20 -0
- package/examples/session-bridge-plugin/docs/usage.md +54 -0
- package/examples/session-bridge-plugin/package.json +26 -0
- package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
- package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
- package/examples/session-bridge-plugin/src/companion.ts +65 -0
- package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
- package/examples/session-bridge-plugin/src/server.ts +85 -0
- package/examples/session-bridge-plugin/src/store.ts +57 -0
- package/examples/session-bridge-plugin/tsconfig.json +21 -0
- package/examples/workspace-provider-plugin/README.md +4 -4
- package/examples/workspace-provider-plugin/package.json +1 -1
- package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
- package/examples/workspace-provider-plugin/src/server.ts +15 -11
- package/package.json +19 -12
- package/dist/client/assets/CodeViewer-CGAlg9S8.js +0 -4
- package/dist/client/assets/TerminalPanel-xhJRhOas.js +0 -187
- package/dist/client/assets/index-DXQKhn1P.js +0 -4326
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
- package/dist/server/terminalProxyRoutes.js +0 -141
- package/dist/server/terminalProxyRoutes.js.map +0 -1
- package/dist/server/terminals/terminalRoutes.js +0 -155
- package/dist/server/terminals/terminalRoutes.js.map +0 -1
- package/dist/server/terminals/terminalService.js.map +0 -1
- package/dist/server/terminals/terminalSize.js +0 -17
- package/dist/server/terminals/terminalSize.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Generated from pi-packages/captains-log/src/roundtrip.ts. Do not edit directly.
|
|
2
|
+
import { MAX_FINDINGS, CAPTAIN_REPLY, CAPTAIN_REQUEST, SOURCE_REPLY, SOURCE_REQUEST } from "./browser/protocol.js";
|
|
3
|
+
export function collectReply(connection, requestId, text, lifetime, progress) {
|
|
4
|
+
return collect(connection, requestId, { requestId, text }, CAPTAIN_REQUEST, CAPTAIN_REPLY, lifetime, progress);
|
|
5
|
+
}
|
|
6
|
+
export function collectSourceReply(connection, requestId, lifetime, progress) {
|
|
7
|
+
return collect(connection, requestId, { requestId }, SOURCE_REQUEST, SOURCE_REPLY, lifetime, progress);
|
|
8
|
+
}
|
|
9
|
+
/** Subscribe before sending. Never retry an ambiguous send or cancel Pi on timeout. */
|
|
10
|
+
async function collect(connection, requestId, payload, requestEvent, replyEvent, lifetime, progress) {
|
|
11
|
+
const signal = AbortSignal.any([lifetime, connection.signal]);
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
let unsubscribe;
|
|
14
|
+
let onAbort;
|
|
15
|
+
let receiptTimer;
|
|
16
|
+
let completionTimer;
|
|
17
|
+
try {
|
|
18
|
+
return await new Promise((resolve, reject) => {
|
|
19
|
+
onAbort = () => { reject(new Error("Captain connection interrupted; agent work may continue in Sessions")); };
|
|
20
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
21
|
+
receiptTimer = setTimeout(() => { reject(new Error("No companion receipt within 5 seconds. Check package enablement/trust; inspect Sessions before retrying.")); }, 5_000);
|
|
22
|
+
completionTimer = setTimeout(() => { reject(new Error("Captain request exceeded 10 minutes; agent work may continue in Sessions. No translation saved.")); }, 600_000);
|
|
23
|
+
unsubscribe = connection.on(replyEvent, (data) => {
|
|
24
|
+
if (typeof data !== "object" || data === null || !("requestId" in data) || data.requestId !== requestId)
|
|
25
|
+
return;
|
|
26
|
+
if (!("status" in data))
|
|
27
|
+
return;
|
|
28
|
+
if (data.status === "accepted") {
|
|
29
|
+
clearTimeout(receiptTimer);
|
|
30
|
+
progress("Native companion received request");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (data.status === "running") {
|
|
34
|
+
progress("Native Pi agent running");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (data.status === "failed") {
|
|
38
|
+
reject(new Error("error" in data && typeof data.error === "string" ? data.error.slice(0, 4000) : "Captain request failed"));
|
|
39
|
+
}
|
|
40
|
+
else if (data.status === "completed") {
|
|
41
|
+
if (!("text" in data) || typeof data.text !== "string" || !data.text.trim() || data.text.length > MAX_FINDINGS) {
|
|
42
|
+
reject(new Error("Invalid companion reply"));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
progress("Backend received correlated final reply");
|
|
46
|
+
resolve(data.text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// Native listeners can reply synchronously.
|
|
51
|
+
progress("Backend sent request to native companion");
|
|
52
|
+
connection.emit(requestEvent, payload);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(receiptTimer);
|
|
57
|
+
clearTimeout(completionTimer);
|
|
58
|
+
if (onAbort)
|
|
59
|
+
signal.removeEventListener("abort", onAbort);
|
|
60
|
+
unsubscribe?.();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Generated from pi-packages/captains-log/src/server.ts. Do not edit directly.
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { PI_WEB_HOST_PI_SESSIONS_CAPABILITY, PI_WEB_HOST_PI_SESSION_EVENTS_CAPABILITY } from "@jmfederico/pi-web/server-plugin-api";
|
|
4
|
+
import {} from "./browser/protocol.js";
|
|
5
|
+
import { LOG_CHANNEL, entryFrames, isCaptainClientFrame } from "./browser/channelProtocol.js";
|
|
6
|
+
import { collectReply, collectSourceReply } from "./roundtrip.js";
|
|
7
|
+
import { LogStore, logScope } from "./store.js";
|
|
8
|
+
const plugin = {
|
|
9
|
+
apiVersion: 3,
|
|
10
|
+
name: "Captain's Log",
|
|
11
|
+
requires: [PI_WEB_HOST_PI_SESSIONS_CAPABILITY, PI_WEB_HOST_PI_SESSION_EVENTS_CAPABILITY],
|
|
12
|
+
activate(host) {
|
|
13
|
+
const store = new LogStore(host.dataDirectory);
|
|
14
|
+
const active = new Map();
|
|
15
|
+
const unsaved = new Map();
|
|
16
|
+
const currentSessions = new Map();
|
|
17
|
+
const subscribers = new Map();
|
|
18
|
+
const channelCleanups = new Set();
|
|
19
|
+
function publish(scope, entry, saved) {
|
|
20
|
+
for (const frame of entryFrames(entry, saved))
|
|
21
|
+
for (const send of subscribers.get(scope) ?? [])
|
|
22
|
+
send(frame);
|
|
23
|
+
}
|
|
24
|
+
const tasks = new Set();
|
|
25
|
+
let sessions;
|
|
26
|
+
let events;
|
|
27
|
+
function visible(scope, entry) {
|
|
28
|
+
const live = active.get(scope);
|
|
29
|
+
if (live?.id === entry.id)
|
|
30
|
+
return live;
|
|
31
|
+
return unsaved.get(entry.id) ?? (entry.status === "running"
|
|
32
|
+
? { ...entry, status: "interrupted", failureCode: "interrupted", text: "Captain's Log stopped before recording completion. Inspect the previous session before retrying. Nothing was resent automatically." }
|
|
33
|
+
: entry);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
start({ capabilities }) {
|
|
37
|
+
sessions = capabilities.resolve(PI_WEB_HOST_PI_SESSIONS_CAPABILITY);
|
|
38
|
+
events = capabilities.resolve(PI_WEB_HOST_PI_SESSION_EVENTS_CAPABILITY);
|
|
39
|
+
},
|
|
40
|
+
peer: {
|
|
41
|
+
async request(context) {
|
|
42
|
+
context.signal.throwIfAborted();
|
|
43
|
+
host.lifetimeSignal.throwIfAborted();
|
|
44
|
+
const selection = { projectId: context.project.id, workspaceId: context.workspace.id };
|
|
45
|
+
const scope = logScope(selection.projectId, selection.workspaceId);
|
|
46
|
+
if (context.operation === "list") {
|
|
47
|
+
const entries = (await store.list(scope)).map((entry) => visible(scope, entry));
|
|
48
|
+
const live = active.get(scope);
|
|
49
|
+
if (live && !entries.some((entry) => entry.id === live.id))
|
|
50
|
+
entries.unshift(live);
|
|
51
|
+
return entries.map((entry) => ({ ...entry, text: "" }));
|
|
52
|
+
}
|
|
53
|
+
if (context.operation === "read") {
|
|
54
|
+
const live = active.get(scope);
|
|
55
|
+
return { ...(live?.id === context.input ? live : visible(scope, await store.read(scope, context.input))) };
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Unknown Captain operation (translations require the log channel)");
|
|
58
|
+
},
|
|
59
|
+
openChannel(context) {
|
|
60
|
+
context.signal.throwIfAborted();
|
|
61
|
+
host.lifetimeSignal.throwIfAborted();
|
|
62
|
+
if (context.operation !== LOG_CHANNEL || context.input !== null)
|
|
63
|
+
throw new Error("Invalid Captain channel open");
|
|
64
|
+
const selection = { projectId: context.project.id, workspaceId: context.workspace.id };
|
|
65
|
+
const scope = logScope(selection.projectId, selection.workspaceId);
|
|
66
|
+
let listening = true;
|
|
67
|
+
const send = (frame) => {
|
|
68
|
+
if (!listening)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
context.send(frame.type === "entry" ? { ...frame, entry: { ...frame.entry } } : frame);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
cleanup();
|
|
75
|
+
host.logger.warn(`Captain subscriber disconnected: ${String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const cleanup = () => {
|
|
79
|
+
listening = false;
|
|
80
|
+
subscribers.get(scope)?.delete(send);
|
|
81
|
+
if ((subscribers.get(scope)?.size ?? 0) === 0)
|
|
82
|
+
subscribers.delete(scope);
|
|
83
|
+
context.signal.removeEventListener("abort", cleanup);
|
|
84
|
+
host.lifetimeSignal.removeEventListener("abort", cleanup);
|
|
85
|
+
channelCleanups.delete(cleanup);
|
|
86
|
+
};
|
|
87
|
+
if (!subscribers.has(scope))
|
|
88
|
+
subscribers.set(scope, new Set());
|
|
89
|
+
subscribers.get(scope)?.add(send);
|
|
90
|
+
context.signal.addEventListener("abort", cleanup, { once: true });
|
|
91
|
+
host.lifetimeSignal.addEventListener("abort", cleanup, { once: true });
|
|
92
|
+
channelCleanups.add(cleanup);
|
|
93
|
+
return {
|
|
94
|
+
close: cleanup,
|
|
95
|
+
async receive(data, signal) {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
host.lifetimeSignal.throwIfAborted();
|
|
98
|
+
if (!isCaptainClientFrame(data))
|
|
99
|
+
throw new Error("Invalid Captain translation frame");
|
|
100
|
+
const reject = (message) => { send({ type: "rejected", requestId: data.requestId, message: message.slice(0, 2000) }); };
|
|
101
|
+
if (active.has(scope)) {
|
|
102
|
+
reject("Captain is already working.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const entry = { id: randomUUID(), createdAt: new Date().toISOString(), sessionId: "", question: "Translate last reply", sourceSessionId: data.sourceSessionId, status: "running", stages: ["Backend admitted translation"], text: "Preparing Captain's Log…" };
|
|
106
|
+
// Lock before any I/O so concurrent tabs cannot create two dedicated sessions.
|
|
107
|
+
active.set(scope, entry);
|
|
108
|
+
try {
|
|
109
|
+
const previous = await store.list(scope);
|
|
110
|
+
// Strictly increasing admission times make persisted session selection independent of UUID ties.
|
|
111
|
+
const latestTime = Math.max(0, ...previous.map((record) => Date.parse(record.createdAt)).filter(Number.isFinite));
|
|
112
|
+
entry.createdAt = new Date(Math.max(Date.now(), latestTime + 1)).toISOString();
|
|
113
|
+
entry.sessionId = currentSessions.get(scope) ?? previous.find((record) => record.sessionId)?.sessionId ?? "";
|
|
114
|
+
if (entry.sessionId === data.sourceSessionId || previous.some((record) => record.sessionId === data.sourceSessionId))
|
|
115
|
+
throw new Error("Select a source session other than the pirate conversation.");
|
|
116
|
+
await store.save(scope, entry);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
active.delete(scope);
|
|
120
|
+
reject(String(error));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
send({ type: "admitted", requestId: data.requestId, id: entry.id });
|
|
124
|
+
publish(scope, entry, true);
|
|
125
|
+
const progress = (stage) => {
|
|
126
|
+
if (!entry.stages.includes(stage) && entry.stages.length < 16)
|
|
127
|
+
entry.stages.push(stage);
|
|
128
|
+
publish(scope, entry, false);
|
|
129
|
+
};
|
|
130
|
+
const task = (async () => {
|
|
131
|
+
try {
|
|
132
|
+
host.lifetimeSignal.throwIfAborted();
|
|
133
|
+
const source = await events.connect({ ...selection, sessionId: data.sourceSessionId });
|
|
134
|
+
let sourceText;
|
|
135
|
+
try {
|
|
136
|
+
sourceText = await collectSourceReply(source, entry.id, host.lifetimeSignal, progress);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
source.close();
|
|
140
|
+
}
|
|
141
|
+
progress("Source reply read without changing source");
|
|
142
|
+
const createPirate = async () => {
|
|
143
|
+
({ sessionId: entry.sessionId } = await sessions.create(selection));
|
|
144
|
+
currentSessions.set(scope, entry.sessionId);
|
|
145
|
+
// Persist identity before any potentially ambiguous send.
|
|
146
|
+
await store.save(scope, entry);
|
|
147
|
+
return events.connect({ ...selection, sessionId: entry.sessionId });
|
|
148
|
+
};
|
|
149
|
+
const previousSessionId = entry.sessionId;
|
|
150
|
+
const connection = previousSessionId
|
|
151
|
+
? await events.connect({ ...selection, sessionId: previousSessionId }).catch((error) => {
|
|
152
|
+
// Only a definitive rejection before sending permits automatic replacement.
|
|
153
|
+
if (error instanceof Error && error.message === "Selected session is not hosted in this workspace on this machine")
|
|
154
|
+
return createPirate();
|
|
155
|
+
throw error;
|
|
156
|
+
})
|
|
157
|
+
: await createPirate();
|
|
158
|
+
progress("Pirate conversation selected");
|
|
159
|
+
try {
|
|
160
|
+
entry.text = await collectReply(connection, entry.id, sourceText, host.lifetimeSignal, progress);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
connection.close();
|
|
164
|
+
}
|
|
165
|
+
entry.status = "completed";
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
entry.status = "failed";
|
|
169
|
+
entry.failureCode ??= "request-failed";
|
|
170
|
+
entry.text = `Captain could not finish this request. Inspect the session before retrying; nothing was resent automatically.\n${error instanceof Error ? error.message : String(error)}`.slice(0, 4000);
|
|
171
|
+
progress("Backend recorded failure (not success)");
|
|
172
|
+
}
|
|
173
|
+
let saved = false;
|
|
174
|
+
try {
|
|
175
|
+
if (entry.status === "completed")
|
|
176
|
+
entry.stages.push("Backend saved completed roundtrip");
|
|
177
|
+
await store.save(scope, entry);
|
|
178
|
+
saved = true;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
entry.status = "failed";
|
|
182
|
+
entry.failureCode = "request-failed";
|
|
183
|
+
entry.stages = entry.stages.filter((stage) => stage !== "Backend saved completed roundtrip");
|
|
184
|
+
entry.text = `Could not save Captain's Log: ${String(error)}. Inspect session ${entry.sessionId}.`.slice(0, 4000);
|
|
185
|
+
unsaved.set(entry.id, { ...entry });
|
|
186
|
+
host.logger.error(entry.text);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
active.delete(scope);
|
|
190
|
+
publish(scope, entry, saved);
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
tasks.add(task);
|
|
194
|
+
void task.finally(() => { tasks.delete(task); });
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
async dispose() {
|
|
200
|
+
await Promise.all(tasks);
|
|
201
|
+
for (const cleanup of channelCleanups)
|
|
202
|
+
cleanup();
|
|
203
|
+
active.clear();
|
|
204
|
+
unsaved.clear();
|
|
205
|
+
currentSessions.clear();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
export default plugin;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generated from pi-packages/captains-log/src/store.ts. Do not edit directly.
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, open, readdir, rename, rm } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { isLogEntry } from "./browser/protocol.js";
|
|
7
|
+
/** Only host-resolved ids enter the scope hash; peer input is never a filesystem path. */
|
|
8
|
+
export function logScope(projectId, workspaceId) {
|
|
9
|
+
return createHash("sha256").update(JSON.stringify([projectId, workspaceId])).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
export class LogStore {
|
|
12
|
+
dataDirectory;
|
|
13
|
+
constructor(dataDirectory) {
|
|
14
|
+
this.dataDirectory = dataDirectory;
|
|
15
|
+
}
|
|
16
|
+
async directory(scope) {
|
|
17
|
+
if (!/^[a-f0-9]{64}$/.test(scope))
|
|
18
|
+
throw new Error("Invalid captain scope");
|
|
19
|
+
const directory = join(this.dataDirectory, scope);
|
|
20
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
21
|
+
if (!(await lstat(directory)).isDirectory())
|
|
22
|
+
throw new Error("LogEntry directory must not be a symlink");
|
|
23
|
+
return directory;
|
|
24
|
+
}
|
|
25
|
+
async save(scope, captain) {
|
|
26
|
+
if (!isLogEntry(captain))
|
|
27
|
+
throw new Error("Invalid captain record");
|
|
28
|
+
const directory = await this.directory(scope);
|
|
29
|
+
const temporary = join(directory, `.${randomUUID()}.tmp`);
|
|
30
|
+
try {
|
|
31
|
+
const file = await open(temporary, "wx", 0o600);
|
|
32
|
+
try {
|
|
33
|
+
await file.writeFile(JSON.stringify(captain));
|
|
34
|
+
await file.sync();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await file.close();
|
|
38
|
+
}
|
|
39
|
+
await rename(temporary, join(directory, `${captain.id}.json`));
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await rm(temporary, { force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async read(scope, id) {
|
|
46
|
+
if (typeof id !== "string" || !/^[a-f0-9-]{36}$/.test(id))
|
|
47
|
+
throw new Error("Invalid captain id");
|
|
48
|
+
const path = join(await this.directory(scope), `${id}.json`);
|
|
49
|
+
// O_NOFOLLOW is not enforced on Windows; reject existing links there too.
|
|
50
|
+
if (!(await lstat(path)).isFile())
|
|
51
|
+
throw new Error("Invalid captain file");
|
|
52
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
53
|
+
try {
|
|
54
|
+
const stat = await file.stat();
|
|
55
|
+
if (!stat.isFile() || stat.size > 300_000)
|
|
56
|
+
throw new Error("Invalid captain file");
|
|
57
|
+
const value = JSON.parse(await file.readFile("utf8"));
|
|
58
|
+
if (!isLogEntry(value) || value.id !== id)
|
|
59
|
+
throw new Error("Invalid saved captain");
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await file.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async list(scope) {
|
|
67
|
+
const names = await readdir(await this.directory(scope));
|
|
68
|
+
const captains = [];
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
if (/^[a-f0-9-]{36}\.json$/.test(name))
|
|
71
|
+
captains.push(await this.read(scope, name.slice(0, -5)));
|
|
72
|
+
}
|
|
73
|
+
return captains.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Using Captain's Log
|
|
2
|
+
|
|
3
|
+
Select a conversation in the current workspace, open **Captain's Log**, and click **Let the Captain tell it**. The companion reads that session's latest completed assistant reply without sending it a prompt or changing its transcript. A separate pirate conversation retells the text using your configured model. The prompt asks for a fresh, theatrical crew briefing with nautical metaphors and humor—not just a few pirate words. It asks the model to preserve important facts, warnings, and next steps, and keep code and commands exact.
|
|
4
|
+
|
|
5
|
+
The pirate gets a short introduction on its first prompt and keeps speaking pirate. Further translations use the same conversation while it is loaded in the session daemon. If it is no longer loaded, the next translation automatically creates a new pirate conversation. There are no confirmation dialogs, workspace reviews, or plain-English mode.
|
|
6
|
+
|
|
7
|
+
To keep the previous pirate's context after a daemon restart, open the most recent **Captain's Log** conversation in Sessions before translating. Once loading finishes, the plugin can reuse it; it need not remain selected. Then select the conversation whose reply you want translated. The plugin does not automatically reopen saved sessions or stop/delete old sessions.
|
|
8
|
+
|
|
9
|
+
## Results and connections
|
|
10
|
+
|
|
11
|
+
Use **Open source session** beneath a translation to return to its original conversation in chat, even when another conversation is selected. This action uses the record's machine/project/workspace scope and source session ID. If the source is no longer available, PI WEB shows its normal unavailable-destination message; it does not create a replacement conversation.
|
|
12
|
+
|
|
13
|
+
- The translation is rendered as Markdown, including headings, lists, emphasis, links, tables, and code blocks. Embedded HTML stays literal, unsafe links are inert, and images are not loaded automatically. **Previous translations** holds earlier results; **Diagnostics** holds session/request IDs and the message trail. Both start collapsed. Existing review-era logs remain on disk but are not shown as translations.
|
|
14
|
+
- The panel inherits PI WEB's workspace styles and themes. The action-palette **Open pirate translator** opens it without spending model tokens.
|
|
15
|
+
- Browser requests and backend progress use a bidirectional peer channel, with no polling. Opening or reconnecting loads a snapshot. Progress is not token-by-token model streaming; the completed translation arrives when the pirate finishes.
|
|
16
|
+
- Closing the panel or browser does not cancel admitted work. Reconnect to recover the result. Requests with uncertain delivery are never automatically resent. Automatic pirate creation happens only before sending a translation, when the previous pirate is confirmed unhosted—not after a timeout or arbitrary connection failure.
|
|
17
|
+
|
|
18
|
+
## When a translation cannot run
|
|
19
|
+
|
|
20
|
+
Select an ordinary, unarchived conversation in the same workspace and machine, not the pirate itself. Wait for it to finish responding. It needs a completed assistant reply with text; partial, aborted, failed, and tool-call-only replies are not translations. If the native companion is missing from an existing session, use `/reload` in that session and try again.
|
|
21
|
+
|
|
22
|
+
Only one panel translation per workspace runs at a time. Busy pirates, direct user messages during a translation, provider errors, aborted or truncated replies, and runtime replacement are reported as failures rather than successful translations. A receipt timeout or completion timeout detaches listeners but does not stop Pi. Inspect the pirate conversation before retrying an uncertain request. Very large source replies or results are rejected rather than silently truncated; see Diagnostics for the reason.
|
|
23
|
+
|
|
24
|
+
Translations and metadata are stored under the host-provided plugin data directory, scoped to the project/workspace. Pi owns the conversation transcripts. Interrupted records remain visible; there is no durable job resumption or automatic retention policy. The source text is sent to the pirate's model provider. This is trusted agent code, not a tool sandbox.
|
|
25
|
+
|
|
26
|
+
## Installation and activation
|
|
27
|
+
|
|
28
|
+
Install from **Settings → Pi packages → Available packages** on the target machine, then explicitly enable **Captain's Log** in **Settings → PI WEB plugins**. The package ships compiled. Manual installation can use `pi install /absolute/path/to/dist/pi-packages/captains-log` with the target machine's Pi profile.
|
|
29
|
+
|
|
30
|
+
Restart the target session daemon manually when safe, **from outside its hosted sessions**, then reload the browser. A web/API restart alone cannot activate the backend. Existing sessions need `/reload` to load an updated native companion. Project-scoped packages require normal project trust.
|
|
31
|
+
|
|
32
|
+
Disabling the PI WEB plugin controls its panel/backend, not its native extension. Remove the Pi package to remove both resource declarations; saved conversations and plugin data are retained. The older Workspace Reviews example has a separate plugin ID and must be disabled or removed separately.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jmfederico/pi-captains-log",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Opt-in Captain's Log: translate the selected session's last reply into pirate through a native Pi companion and PI WEB panel",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": ["pi-package"],
|
|
8
|
+
"files": ["dist", "src", "docs", "test", "tsconfig.json", "vite.config.mjs"],
|
|
9
|
+
"scripts": { "build": "tsc -p tsconfig.json && vite build", "test": "node --test test/*.test.mjs" },
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"marked": "^18.0.6"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@jmfederico/pi-web": "^1.202609.1",
|
|
15
|
+
"@earendil-works/pi-coding-agent": "^0.87.0",
|
|
16
|
+
"@types/node": "^24.13.3",
|
|
17
|
+
"typescript": "^6.0.3",
|
|
18
|
+
"vite": "^8.1.4"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
22
|
+
"@jmfederico/pi-web": "^1.202609.1"
|
|
23
|
+
},
|
|
24
|
+
"pi": { "extensions": ["./dist/companion.js"] },
|
|
25
|
+
"piWeb": {
|
|
26
|
+
"plugins": [{
|
|
27
|
+
"id": "captains-log",
|
|
28
|
+
"defaultEnabled": false,
|
|
29
|
+
"browserRoot": "dist/browser",
|
|
30
|
+
"module": "dist/browser/index.js",
|
|
31
|
+
"serverModule": "dist/server.js",
|
|
32
|
+
"machineSpecific": true
|
|
33
|
+
}]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"noEmitOnError": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
const packageDir = fileURLToPath(new URL(".", import.meta.url));
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
root: packageDir,
|
|
8
|
+
base: "./",
|
|
9
|
+
publicDir: false,
|
|
10
|
+
build: {
|
|
11
|
+
outDir: "dist/browser",
|
|
12
|
+
// Retain the transpiled filenames for existing package/install consumers.
|
|
13
|
+
emptyOutDir: false,
|
|
14
|
+
copyPublicDir: false,
|
|
15
|
+
target: "es2022",
|
|
16
|
+
sourcemap: false,
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
input: fileURLToPath(new URL("src/browser/index.ts", import.meta.url)),
|
|
19
|
+
preserveEntrySignatures: "strict",
|
|
20
|
+
output: {
|
|
21
|
+
format: "es",
|
|
22
|
+
entryFileNames: "index.js",
|
|
23
|
+
chunkFileNames: "assets/[name]-[hash].js",
|
|
24
|
+
assetFileNames: "assets/[name]-[hash][extname]",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { RELAYS_ROOT } from "./relayDiscovery.js";
|
|
3
3
|
import { defineRelaysPanelElement } from "./relaysPanelElement.js";
|
|
4
4
|
const plugin = {
|
|
5
|
-
apiVersion:
|
|
5
|
+
apiVersion: 4,
|
|
6
6
|
name: "Relays",
|
|
7
7
|
activate: ({ runtimePluginId, html, svg }) => {
|
|
8
8
|
defineRelaysPanelElement();
|
|
@@ -19,7 +19,7 @@ The quality of the Relay depends on a clear shared understanding of its destinat
|
|
|
19
19
|
1. **Start a reviewable draft packet.** Choose a clear Relay name and create `.pi-web/relays/<name>/` in the current drafting checkout. Seed draft `charter.md`, `operations.md`, `status.md`, and `log.md`; mark status **Draft — awaiting approval; not dispatched**. Update these documents as understanding improves so the human can review them through the Relays UI. Draft creation is not dispatch authorization.
|
|
20
20
|
2. **Understand the request and baseline.** Read the repository's canonical agent/contributor instructions and task-relevant project material. Inspect enough current behavior, repository state, and delivery context to distinguish the user's intended outcome from an assumed solution. Keep this bounded to facts that affect the goal, scope edges, feasibility, working location, or first leg; leave implementation archaeology to the Relay.
|
|
21
21
|
3. **Capture understanding without making a roadmap.** Keep `charter.md` to the plain-language goal and observable finish line, minimum outcome acceptance, in-scope edges, explicit non-goals, directly affected behavior to preserve, and material assumptions or human decisions. Put packet/profile identity, proposed operating mode/base, canonical project-guidance pointers, verification/delivery mechanics, and packet isolation in `operations.md`. Put only the proposed first bounded leg and current approval state in `status.md`.
|
|
22
|
-
4. **Do not pre-plan the chain.** Do not create a fixed leg count, work-package hierarchy, exhaustive stage list, expected file/layer map, `plan.md`, or technical design merely to make the Relay look prepared. The route is adaptive.
|
|
22
|
+
4. **Do not pre-plan the chain.** Do not create a fixed leg count, work-package hierarchy, exhaustive stage list, expected file/layer map, `plan.md`, or technical design merely to make the Relay look prepared. The route is adaptive. Select an implementation-first leg under the runner profile. Reserve research-only work for a concrete unanswered question blocking implementation; routine investigation belongs inside the implementation leg.
|
|
23
23
|
5. **Discuss and refine.** Point the user at the draft packet, summarize the current understanding, ask related material questions together with `ask_user`, and incorporate the answers into the drafts. Do not hide assumptions or settle product, destructive-data, target, base, or delivery choices by guesswork.
|
|
24
24
|
6. **Request approval against the final drafts.** After the user has had a chance to review the final document purposes and content, provide the packet path plus a concise goal/edges, operating-setup, and first-leg summary. Use `ask_user` to offer **Approve and dispatch**, **Revise**, or **Do not dispatch**. The initial request, a clear task, prior general enthusiasm, silence, or permission to create drafts is not dispatch approval. If goal, edges, operating target, or first leg changes materially after approval, update the drafts and obtain fresh approval.
|
|
25
25
|
|
|
@@ -15,6 +15,7 @@ For a Relay using this profile:
|
|
|
15
15
|
|
|
16
16
|
- Have the preparation prompt select `relay-runner` before drafting the packet. Record it immediately in draft `operations.md` and retain it as required for every active leg, including implementation, review, remediation, and delivery.
|
|
17
17
|
- Name both `relay` and `relay-runner` in every handoff prompt. Fresh sessions cannot inherit a prior runner's loaded skills.
|
|
18
|
+
- Keep every Relay handoff on the current runner's model by omitting the `model` argument, unless instructed to use a specific model or to choose an appropriate one. Record any Relay-wide model instruction in `operations.md` so it remains available to later runners.
|
|
18
19
|
- Treat the charter as destination and scope edges, `operations.md` as this profile's repository and execution bindings, `status.md` as current position and next work, and repository instructions as the quality and implementation authority.
|
|
19
20
|
- Follow canonical repository policy when it conflicts with a profile default. Record a material operational adaptation in `operations.md`, not the charter. Stop for human intervention when reconciling the conflict would change the finish line or an edge, weaken a protected invariant, or require a product or business decision.
|
|
20
21
|
|
|
@@ -53,19 +54,26 @@ Relay intentionally does not predict the chain. Before dispatch, establish the d
|
|
|
53
54
|
|
|
54
55
|
Expected files, subsystems, dependencies, architecture, and sequencing are route assumptions. Keep only what the current or next leg needs in status or a targeted temporary artifact; do not turn route assumptions into stable agreement.
|
|
55
56
|
|
|
56
|
-
A leg is one context-contained, reviewable slice that
|
|
57
|
+
A leg is one context-contained, reviewable slice that advances the finish line. Development legs implement and verify by default: optimize for the smallest useful behaviour, not the smallest independently testable artifact. A functional checkpoint makes an observable charter outcome available and verifies it through the relevant application boundary. A tested but unused module is a prerequisite, not a functional outcome. Setup, required verification, review and delivery remain legitimate lifecycle legs.
|
|
58
|
+
|
|
59
|
+
Slice by supported cases across the necessary layers. When a journey is too large, choose a narrower case or a safe partial capability with explicit limitations, keeping the full charter outstanding. Keep directly coupled implementation, checks, contracts, generated outputs, documentation and integration together.
|
|
57
60
|
|
|
58
61
|
Before substantial work, state the leg's:
|
|
59
62
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
|
|
63
|
+
- observable outcome and application boundary;
|
|
64
|
+
- bounded change surface and coupled verification;
|
|
65
|
+
- remaining connection to usable behaviour, if any; and
|
|
66
|
+
- checkpoint type: functional, necessary prerequisite or profile-authorized transitional.
|
|
67
|
+
|
|
68
|
+
An unconnected prerequisite must name the behaviour it enables and why a connected slice cannot reasonably fit. Its successor prioritizes that connection. Before adding another prerequisite-only leg, reassess the slice and simplify the route; continue only when a concrete dependency still prevents connection.
|
|
69
|
+
|
|
70
|
+
Routine reading, design, sizing and test investigation belong inside implementation. A research-only leg requires a specific unanswered question blocking responsible implementation: record the blocked behaviour, why available evidence is insufficient and the result needed to proceed. Answer that question and hand off to implementation; intervene if it remains unresolved. Size or complexity alone calls for a smaller implementation slice.
|
|
71
|
+
|
|
72
|
+
Choose the simplest design satisfying the charter, canonical project rules and concrete risks introduced by the change. Justify additional safeguards against those obligations; keep speculative recovery systems and future-use abstractions outside the slice. Preserve safety when narrowing behaviour.
|
|
65
73
|
|
|
66
|
-
Persist only conclusions useful to the next runner.
|
|
74
|
+
Persist only conclusions useful to the next runner. Keep leg history in the packet log and project documentation focused on current behaviour and contracts.
|
|
67
75
|
|
|
68
|
-
If a leg grows beyond its context-contained slice, stop broadening before context exhaustion. Finish or revert to a functional checkpoint, or use an already authorized transition. Record the next bounded slice. Do not promise a fixed total leg count.
|
|
76
|
+
If a leg grows beyond its context-contained slice, stop broadening before context exhaustion. Finish or revert to a coherent functional or justified prerequisite checkpoint, or use an already authorized transition. Record the next bounded slice. Do not promise a fixed total leg count.
|
|
69
77
|
|
|
70
78
|
When status does not name the next task, choose the smallest coherent slice that advances the critical path or unblocks an agreed outcome. Do not invent cleanup or speculative follow-up merely to fill a leg.
|
|
71
79
|
|
|
@@ -130,7 +138,7 @@ Do **not** put a quality bar, technical design, expected files or subsystems, ve
|
|
|
130
138
|
Write the profile's mechanics to `operations.md` instead:
|
|
131
139
|
|
|
132
140
|
- packet identity and `relay-runner` as the profile required in every leg and handoff;
|
|
133
|
-
-
|
|
141
|
+
- implementation-first leg sizing and outcome-led task selection under this profile, with route assumptions provisional and no fixed leg count;
|
|
134
142
|
- pointers to canonical repository instructions and applicable project skills;
|
|
135
143
|
- checkpoint and transition policy;
|
|
136
144
|
- packet root, working location, branch, immutable base commit, initial HEAD, pre-existing state, packet isolation, and exact review range;
|
|
@@ -158,11 +166,11 @@ Use this loop:
|
|
|
158
166
|
|
|
159
167
|
1. Read `charter.md`, `operations.md`, `status.md`, and only targeted context they reference. Then load canonical project skills applicable to this leg.
|
|
160
168
|
2. Confirm that the prompt's leg identifier, status baton, working location, branch, and blockers are consistent. Resolve small baton defects with targeted inspection; intervene instead of broad archaeology or guessing about intent.
|
|
161
|
-
3.
|
|
169
|
+
3. Use status's next outcome as the starting point. Revise inherited sequencing or design when a simpler connected slice better advances it. Safe implementation splits within the charter are runner decisions, not approval requests. If the intended outcome remains ambiguous or falls outside the finish line, intervene.
|
|
162
170
|
4. Re-anchor to the finish line and perform the containment check. Do exactly one bounded slice. Do not audit unrelated code, execute the next nominal leg too, or expand scope under the guise of quality.
|
|
163
171
|
5. Run every focused check meaningful for the slice and report exact results. Before whole review, run the full verification named by `operations.md`. Never describe failed, skipped, or incomplete verification as passing.
|
|
164
172
|
6. Make delivery changes durable under the operations record's commit policy without absorbing unrelated state. Keep packet-only writes out of delivery commits.
|
|
165
|
-
7. Update status with the
|
|
173
|
+
7. Update status with the observable progress, completed and next leg identifiers, next outcome, existing seam to connect, concrete constraints, remaining unconnected work, targeted context, verification state, blockers and active transition/review pointers. Keep route choices provisional. Append the concise leg entry to the log before stopping or handing off.
|
|
166
174
|
8. Stop without spawning when Relay completion is recorded, the leg is blocked, or an intervention trigger fires. When the chartered outcome is implemented but required review, approval, or delivery remains, name that lifecycle work as the next leg instead of stopping. Surface the intervention signal clearly. Otherwise hand off exactly once at the end with `spawn_session`.
|
|
167
175
|
|
|
168
176
|
Use this handoff shape, substituting the actual paths and next identifier:
|
|
@@ -177,7 +185,7 @@ Load the `relay` and `relay-runner` skills, then read:
|
|
|
177
185
|
- .pi-web/relays/<name>/status.md
|
|
178
186
|
|
|
179
187
|
Do not read log.md end-to-end. Use only targeted entries referenced by charter.md, operations.md, or status.md.
|
|
180
|
-
Run exactly one leg
|
|
188
|
+
Run exactly one outcome-led leg under the profile's implementation-first rules. Use status's next outcome and connection priority; adapt the route within the charter. Make work and packet updates durable, then either hand off once or stop with the recorded intervention signal.
|
|
181
189
|
```
|
|
182
190
|
|
|
183
191
|
## Whole-work review and remediation
|
|
@@ -200,7 +208,7 @@ For each attempt:
|
|
|
200
208
|
- When attempt 2 has a blocking finding, do not automatically consume attempt 3. Invoke the remediation contingency only when concrete evidence shows that an attempted remediation reasonably expected to resolve a blocker did not, remediation introduced an in-scope regression, or materially new evidence reveals a blocker that could not reasonably have been assessed earlier—and one bounded remediation is likely to resolve it. Record that justification, mark the contingency invoked, and dispatch one coherent remediation leg followed by attempt 3. Expanded scrutiny, a changed standard, deferred already-available evidence, or speculative possibility does not qualify. If a genuine blocker remains but the contingency is not justified or bounded, intervene.
|
|
201
209
|
- When exceptional attempt 3 still has any blocking finding, stop the automatic loop. Record the unresolved findings and exact decision needed, raise the human intervention signal, and do **not** dispatch remediation or a fourth review. A human may accept an authorized risk, change the agreement, stop the Relay, or explicitly grant a bounded additional remediation/review attempt.
|
|
202
210
|
|
|
203
|
-
The reviewer chooses proportionate independent review and records that decision. Independent subreviews are optional; use them only when a specific changed surface benefits from focused expertise, not to create review theater or increase the chance of finding something. The reviewer may use `spawn_subsession` for report-only focused reviews, then `yield_to_subsessions` and consolidate. Subreview prompts must identify repository, base and exact diff, the charter's goal and edges, canonical quality instructions, and the prohibition on all writes including packet changes. The consolidating reviewer is the sole packet writer.
|
|
211
|
+
The reviewer chooses proportionate independent review and records that decision. Independent subreviews are optional; use them only when a specific changed surface benefits from focused expertise, not to create review theater or increase the chance of finding something. The reviewer may use `spawn_subsession` for report-only focused reviews, then `yield_to_subsessions` and consolidate. Subreview prompts must identify repository, base and exact diff, the charter's goal and edges, canonical quality instructions, and the prohibition on all writes including packet changes. The consolidating reviewer is the sole packet writer. The Relay handoff still uses one `spawn_session` only at the end of the leg.
|
|
204
212
|
|
|
205
213
|
A blocking finding needs concrete evidence of an in-scope defect: for example a reproduced failure, a failing relevant check, a specific execution path that violates the approved outcome, or a clear breach of a material contract, invariant, security, authorization, or data-integrity boundary. Mere possibility, stylistic preference, optional hardening, hypothetical future requirements, or “there could be an edge case” is not blocking. If proportionate targeted inspection cannot establish applicability and impact, classify the concern as non-blocking or omit it rather than forcing remediation.
|
|
206
214
|
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2h6.5A2.5 2.5 0 0 1 21 8.5v8A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5z"/>
|
|
3
|
+
</svg>
|