pi-daddy 0.48.2 → 0.49.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/CHANGELOG.md +15 -0
- package/README.md +74 -8
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -1
- package/dist/executors/captured-execution.d.ts.map +1 -1
- package/dist/executors/captured-execution.js +12 -1
- package/dist/executors/captured-execution.js.map +1 -1
- package/dist/executors/herdr-launcher.d.ts.map +1 -1
- package/dist/executors/herdr-launcher.js +22 -3
- package/dist/executors/herdr-launcher.js.map +1 -1
- package/dist/executors/owned-pi-ui.d.ts +3 -0
- package/dist/executors/owned-pi-ui.d.ts.map +1 -0
- package/dist/executors/owned-pi-ui.js +114 -0
- package/dist/executors/owned-pi-ui.js.map +1 -0
- package/dist/executors/owned-pi-view.d.ts +5 -0
- package/dist/executors/owned-pi-view.d.ts.map +1 -0
- package/dist/executors/owned-pi-view.js +60 -0
- package/dist/executors/owned-pi-view.js.map +1 -0
- package/dist/executors/owned-worker.d.ts +2 -0
- package/dist/executors/owned-worker.d.ts.map +1 -1
- package/dist/executors/owned-worker.js +27 -11
- package/dist/executors/owned-worker.js.map +1 -1
- package/dist/governance/approval-prompt.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts.map +1 -1
- package/dist/kernel/selected-definitions.js +6 -3
- package/dist/kernel/selected-definitions.js.map +1 -1
- package/dist/products/dashboard-cli.d.ts.map +1 -1
- package/dist/products/dashboard-cli.js +46 -1
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-display-controls.d.ts +1 -1
- package/dist/products/dashboard-display-controls.d.ts.map +1 -1
- package/dist/products/dashboard-display-controls.js +11 -3
- package/dist/products/dashboard-display-controls.js.map +1 -1
- package/dist/products/dashboard-screen.d.ts +3 -1
- package/dist/products/dashboard-screen.d.ts.map +1 -1
- package/dist/products/dashboard-screen.js +21 -8
- package/dist/products/dashboard-screen.js.map +1 -1
- package/dist/products/dashboard-session-client.d.ts +9 -0
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/dashboard-settings.d.ts +28 -0
- package/dist/products/dashboard-settings.d.ts.map +1 -0
- package/dist/products/dashboard-settings.js +42 -0
- package/dist/products/dashboard-settings.js.map +1 -0
- package/dist/products/diagnostic-export.d.ts +23 -0
- package/dist/products/diagnostic-export.d.ts.map +1 -0
- package/dist/products/diagnostic-export.js +264 -0
- package/dist/products/diagnostic-export.js.map +1 -0
- package/dist/products/diagnostic-filter.d.ts +7 -0
- package/dist/products/diagnostic-filter.d.ts.map +1 -0
- package/dist/products/diagnostic-filter.js +135 -0
- package/dist/products/diagnostic-filter.js.map +1 -0
- package/extensions/approvals.ts +3 -2
- package/extensions/dashboard-session-server.ts +17 -2
- package/extensions/dashboard-settings.ts +149 -0
- package/extensions/delegation-operations.ts +81 -2
- package/extensions/execute-child.ts +10 -11
- package/extensions/execution-controls.ts +7 -2
- package/extensions/grants-command.ts +3 -1
- package/extensions/grants.ts +3 -0
- package/extensions/reload-environment.ts +3 -0
- package/extensions/run-delegation.ts +3 -1
- package/extensions/runtime-snapshot.ts +9 -1
- package/extensions/session.ts +3 -8
- package/package.json +1 -1
- package/src/cli.ts +24 -1
- package/src/executors/captured-execution.ts +13 -1
- package/src/executors/herdr-launcher.ts +15 -2
- package/src/executors/owned-pi-ui.ts +116 -0
- package/src/executors/owned-pi-view.ts +58 -0
- package/src/executors/owned-worker.ts +29 -11
- package/src/governance/approval-prompt.ts +1 -1
- package/src/index.ts +2 -0
- package/src/kernel/selected-definitions.ts +5 -3
- package/src/products/dashboard-cli.ts +38 -1
- package/src/products/dashboard-display-controls.ts +10 -4
- package/src/products/dashboard-screen.ts +21 -10
- package/src/products/dashboard-session-client.ts +7 -1
- package/src/products/dashboard-settings.ts +72 -0
- package/src/products/diagnostic-export.ts +288 -0
- package/src/products/diagnostic-filter.ts +140 -0
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
claimDelegationOperation,
|
|
3
3
|
existingOperationOutcome,
|
|
4
4
|
finishDelegationOperation,
|
|
5
|
+
operationFinalScope,
|
|
5
6
|
} from "./delegation-operations.ts";
|
|
6
7
|
import type { OperationClaim } from "../src/kernel/dispatch-operation.ts";
|
|
7
8
|
import { reconcileDelegationCapacity, reserveDelegationCapacity } from "./session-capacity.ts";
|
|
@@ -552,6 +553,7 @@ export async function runOneDelegation(
|
|
|
552
553
|
...args: Parameters<typeof runOneDelegationImplementation>
|
|
553
554
|
): Promise<DelegationOutcome> {
|
|
554
555
|
const [session, spec, ids, , ctx, signal] = args;
|
|
556
|
+
const finalScope = operationFinalScope(session);
|
|
555
557
|
const options = args[6] ?? {};
|
|
556
558
|
const claim = options.operationClaim ?? (await claimDelegationOperation(session, spec, ids, ctx, signal));
|
|
557
559
|
if (claim?.reused) {
|
|
@@ -571,7 +573,7 @@ export async function runOneDelegation(
|
|
|
571
573
|
const result = await runOneDelegationImplementation(...args);
|
|
572
574
|
if (claim) {
|
|
573
575
|
try {
|
|
574
|
-
await finishDelegationOperation(claim, result);
|
|
576
|
+
await finishDelegationOperation(claim, result, session, finalScope);
|
|
575
577
|
} catch (error) {
|
|
576
578
|
// A post-execution observation failure must not erase the native final or settlement receipt.
|
|
577
579
|
result.control = "failed";
|
|
@@ -5,6 +5,7 @@ import { openRuntimeSettlement, settlementHash } from "../src/governance/runtime
|
|
|
5
5
|
import { reconcileDelegationCapacity } from "./session-capacity.ts";
|
|
6
6
|
import { agentDir } from "../src/kernel/project-paths.ts";
|
|
7
7
|
import type { GrantsSession } from "./session.ts";
|
|
8
|
+
import { bindOperationFinals, operationFinal } from "./delegation-operations.ts";
|
|
8
9
|
export const OPERATION_STATUS_EVENT = "pi-daddy:operation-status:v1";
|
|
9
10
|
export const RUNTIME_SNAPSHOT_EVENT = "pi-daddy:runtime-snapshot:v1";
|
|
10
11
|
export function registerRuntimeSnapshot(
|
|
@@ -99,6 +100,7 @@ export function registerRuntimeSnapshot(
|
|
|
99
100
|
sessionId?: string;
|
|
100
101
|
cwd?: string;
|
|
101
102
|
operationId?: string;
|
|
103
|
+
includeFinal?: boolean;
|
|
102
104
|
reply?: (value: unknown) => void;
|
|
103
105
|
};
|
|
104
106
|
if (
|
|
@@ -139,7 +141,12 @@ export function registerRuntimeSnapshot(
|
|
|
139
141
|
lifecycle.runtimeSnapshotGeneration !== generation
|
|
140
142
|
)
|
|
141
143
|
throw Error("operation owner changed during read");
|
|
142
|
-
request.reply!({
|
|
144
|
+
request.reply!({
|
|
145
|
+
...envelope,
|
|
146
|
+
qualified: true,
|
|
147
|
+
operation,
|
|
148
|
+
...(request.includeFinal === true && operation ? { final: operationFinal(session, operation) } : {}),
|
|
149
|
+
});
|
|
143
150
|
} catch (error) {
|
|
144
151
|
request.reply!({ ...envelope, qualified: false, operation: null, reason: String(error) });
|
|
145
152
|
}
|
|
@@ -172,6 +179,7 @@ export function registerRuntimeSnapshot(
|
|
|
172
179
|
}
|
|
173
180
|
if (sequence !== bindSequence || session.reloadLifecycle !== lifecycle) return;
|
|
174
181
|
lifecycle.runtimeSettlement = runtime;
|
|
182
|
+
bindOperationFinals(session, sessionId, cwd);
|
|
175
183
|
bound = { sessionId, cwd, lifecycle: session.reloadLifecycle };
|
|
176
184
|
} catch (error) {
|
|
177
185
|
if (sequence === bindSequence && session.reloadLifecycle === lifecycle) problem = String(error);
|
package/extensions/session.ts
CHANGED
|
@@ -694,14 +694,9 @@ export function createGrantsSession(
|
|
|
694
694
|
skillPaths: skillPathsFromCatalog(await delegationCatalog(session)),
|
|
695
695
|
// ADR-0016: operator-authored SKILL.md definitions, so `delegate({agent})` can name one.
|
|
696
696
|
definitions: session.definitions,
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
// Read live off `session.executor` (ADR-0031) rather than a boolean captured in the factory: the probe
|
|
702
|
-
// has not run when this session object is built, so a captured value would plan `--print` for a session
|
|
703
|
-
// that turns out to use panes — and `runHerdrPane` refuses a plan containing `--print` by design.
|
|
704
|
-
interactive: false, // Both qualified backends use the same one-shot Pi JSON protocol.
|
|
697
|
+
// Plan the real terminal invocation explicitly; the captured backend remains one-shot JSON.
|
|
698
|
+
// Read the selected executor at dispatch, after its live qualification completes.
|
|
699
|
+
interactive: session.executor.kind === "herdr",
|
|
705
700
|
...(approved ? { approved } : {}),
|
|
706
701
|
};
|
|
707
702
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-daddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/cli.ts
CHANGED
|
@@ -50,6 +50,7 @@ Usage:
|
|
|
50
50
|
pi-daddy report [--since <date>] [--definition <name>] [--model <id>]
|
|
51
51
|
[--group-by definition|model|thinking] [--json]
|
|
52
52
|
pi-daddy outcomes append changed episode outcomes from Git and CI
|
|
53
|
+
pi-daddy diagnostics export <selection.json> <new-directory> private filtered local evidence
|
|
53
54
|
pi-daddy --help | --version
|
|
54
55
|
|
|
55
56
|
init references skills already enabled in Pi at their installed or local paths. Legacy unregistered npm
|
|
@@ -61,8 +62,11 @@ stays unspawnable. Capabilities that can change your machine
|
|
|
61
62
|
It never rewrites ${SETTINGS_REL} — delete that file if you want it regenerated.`;
|
|
62
63
|
|
|
63
64
|
export interface ParsedArgs {
|
|
64
|
-
command:
|
|
65
|
+
command:
|
|
66
|
+
"init" | "ledger-repair" | "ledger-import" | "report" | "outcomes" | "diagnostics-export" | "help" | "version";
|
|
65
67
|
importTarget?: string;
|
|
68
|
+
selectionPath?: string;
|
|
69
|
+
exportDirectory?: string;
|
|
66
70
|
/** `ledger repair <path>`: the ledger file; `yes` applies, otherwise preview only. */
|
|
67
71
|
ledgerPath?: string;
|
|
68
72
|
yes?: boolean;
|
|
@@ -91,6 +95,19 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
91
95
|
if (args.includes("--version") || args.includes("-v")) return { command: "version", force: false, errors: [] };
|
|
92
96
|
|
|
93
97
|
const [command, ...tail] = args;
|
|
98
|
+
if (command === "diagnostics") {
|
|
99
|
+
const [verb, selectionPath, exportDirectory, ...extra] = tail;
|
|
100
|
+
if (
|
|
101
|
+
verb !== "export" ||
|
|
102
|
+
!selectionPath ||
|
|
103
|
+
!exportDirectory ||
|
|
104
|
+
selectionPath.startsWith("-") ||
|
|
105
|
+
exportDirectory.startsWith("-") ||
|
|
106
|
+
extra.length
|
|
107
|
+
)
|
|
108
|
+
return { command: "help", force: false, errors: ["diagnostics needs: export <selection.json> <new-directory>"] };
|
|
109
|
+
return { command: "diagnostics-export", force: false, errors: [], selectionPath, exportDirectory };
|
|
110
|
+
}
|
|
94
111
|
if (command === "ledger") {
|
|
95
112
|
// ADR-0076 PR 3d: `ledger repair <path> [--yes]` previews, --yes truncates; `ledger import <source> <target>`.
|
|
96
113
|
const [verb, target, ...flags] = tail;
|
|
@@ -388,6 +405,12 @@ export async function main(
|
|
|
388
405
|
console.log(USAGE);
|
|
389
406
|
return 0;
|
|
390
407
|
}
|
|
408
|
+
if (parsed.command === "diagnostics-export") {
|
|
409
|
+
const { exportDiagnostics } = await import("./products/diagnostic-export.ts");
|
|
410
|
+
const result = await exportDiagnostics(parsed.selectionPath!, parsed.exportDirectory!);
|
|
411
|
+
console.log(JSON.stringify(result));
|
|
412
|
+
return result.missing ? 1 : 0;
|
|
413
|
+
}
|
|
391
414
|
if (parsed.command === "outcomes") {
|
|
392
415
|
const cwd = process.cwd();
|
|
393
416
|
const result = await updateEpisodeOutcomes({
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { mkdtemp } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { runOwnedChild, type OwnedChildRunRequest, type OwnedChildRunResult } from "./owned-worker.ts";
|
|
6
7
|
import type { BoundedReadCleanupError } from "../kernel/bounded-read.ts";
|
|
7
8
|
import { ChildFinalCapture, type ChildFinal } from "./child-final.ts";
|
|
@@ -36,7 +37,18 @@ export async function runCapturedExecution(
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
const capture = new ChildFinalCapture(request.onOutput, request.onReadCleanup);
|
|
39
|
-
const args = [...request.args
|
|
40
|
+
const args = [...request.args];
|
|
41
|
+
if (request.terminalUi) {
|
|
42
|
+
const extension = (name: string) =>
|
|
43
|
+
fileURLToPath(new URL("./" + name + (import.meta.url.endsWith(".ts") ? ".ts" : ".js"), import.meta.url));
|
|
44
|
+
// Pi enables editor commands before awaiting session_start handlers. The view guard must be first;
|
|
45
|
+
// the event observer remains last so earlier message transformations are captured exactly.
|
|
46
|
+
const firstExtension = args.findIndex(
|
|
47
|
+
(arg, index) => index < args.length - 1 && ["--no-extensions", "-ne", "-e", "--extension"].includes(arg),
|
|
48
|
+
);
|
|
49
|
+
args.splice(firstExtension < 0 ? args.length - 1 : firstExtension, 0, "-e", extension("owned-pi-view"));
|
|
50
|
+
args.splice(args.length - 1, 0, "-e", extension("owned-pi-ui"), "--tui-mode", "regular");
|
|
51
|
+
} else args.splice(args.length - 1, 0, "--mode", "json");
|
|
40
52
|
const output = await runOwned({
|
|
41
53
|
...request,
|
|
42
54
|
args,
|
|
@@ -10,6 +10,7 @@ export async function launchHerdrWorker(socketPath: string, token: string): Prom
|
|
|
10
10
|
const abort = new AbortController();
|
|
11
11
|
let started = false,
|
|
12
12
|
authenticated = false,
|
|
13
|
+
terminalUi = false,
|
|
13
14
|
acknowledge: (() => void) | undefined,
|
|
14
15
|
rejectGate: ((error: Error) => void) | undefined;
|
|
15
16
|
const lost = (error: Error) => {
|
|
@@ -32,7 +33,7 @@ export async function launchHerdrWorker(socketPath: string, token: string): Prom
|
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
35
|
if (message?.type === "display" && authenticated && typeof message.text === "string") {
|
|
35
|
-
process.stdout.write(message.text);
|
|
36
|
+
if (!terminalUi) process.stdout.write(message.text);
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
if (message?.type !== "request" || started) {
|
|
@@ -43,11 +44,14 @@ export async function launchHerdrWorker(socketPath: string, token: string): Prom
|
|
|
43
44
|
started = true;
|
|
44
45
|
authenticated = true;
|
|
45
46
|
const request = message.request as OwnedChildRunRequest & { sessionPath?: string };
|
|
47
|
+
terminalUi = request.terminalUi === true;
|
|
46
48
|
let childPid: number | undefined;
|
|
47
49
|
const display = herdrAgentDisplay(message.display);
|
|
48
50
|
let settled = false;
|
|
49
51
|
void (async () => {
|
|
52
|
+
const wasRaw = process.stdin.isRaw;
|
|
50
53
|
try {
|
|
54
|
+
if (terminalUi && process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
51
55
|
const result = await runOwnedChild({
|
|
52
56
|
...request,
|
|
53
57
|
signal: abort.signal,
|
|
@@ -88,10 +92,13 @@ export async function launchHerdrWorker(socketPath: string, token: string): Prom
|
|
|
88
92
|
childPid = pid;
|
|
89
93
|
void display.working();
|
|
90
94
|
send({ type: "spawned", pid });
|
|
91
|
-
process.stdout.write("pi-daddy: governed child running\n");
|
|
95
|
+
if (!terminalUi) process.stdout.write("pi-daddy: governed child running\n");
|
|
92
96
|
},
|
|
93
97
|
});
|
|
94
98
|
settled = result.cleanup.state === "settled" || result.cleanup.state === "not-started";
|
|
99
|
+
// Mark only this owned terminal as settled; this is cosmetic, not process or sidebar authority.
|
|
100
|
+
// Unknown cleanup keeps its existing title, and a killed launcher cannot reset it.
|
|
101
|
+
if (terminalUi && settled) process.stdout.write("\x1b]0;Task settled\x07");
|
|
95
102
|
const displayReleased = await display.finish(settled);
|
|
96
103
|
if (!socket.destroyed) {
|
|
97
104
|
send({ type: "result", result, displayReleased: settled && displayReleased });
|
|
@@ -107,6 +114,12 @@ export async function launchHerdrWorker(socketPath: string, token: string): Prom
|
|
|
107
114
|
socket.end();
|
|
108
115
|
}
|
|
109
116
|
}
|
|
117
|
+
} finally {
|
|
118
|
+
try {
|
|
119
|
+
if (terminalUi && process.stdin.isTTY) process.stdin.setRawMode(wasRaw);
|
|
120
|
+
} catch {
|
|
121
|
+
// A lost pane cannot be restored; native cleanup remains independently recorded.
|
|
122
|
+
}
|
|
110
123
|
}
|
|
111
124
|
})();
|
|
112
125
|
},
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Install LAST: observe final extension-transformed Pi events through the private inherited pipe. */
|
|
2
|
+
import { fstatSync } from "node:fs";
|
|
3
|
+
import { Socket } from "node:net";
|
|
4
|
+
import { getAgentDir, ProjectTrustStore, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ExtensionAPI, MessageUpdateEvent } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { OWNED_PI_VIEW_READY, failOwnedPiUi as fail } from "./owned-pi-view.ts";
|
|
8
|
+
|
|
9
|
+
const EVENT_FD = 6;
|
|
10
|
+
|
|
11
|
+
export default function ownedPiUi(pi: ExtensionAPI): void {
|
|
12
|
+
try {
|
|
13
|
+
const channel = fstatSync(EVENT_FD);
|
|
14
|
+
if (!channel.isFIFO() && !channel.isSocket()) throw Error("fd 6 is not an inherited event pipe");
|
|
15
|
+
const output = new Socket({ fd: EVENT_FD, readable: false, writable: true });
|
|
16
|
+
output.on("error", fail);
|
|
17
|
+
let pending = Promise.resolve();
|
|
18
|
+
const send = (event: unknown): Promise<void> => {
|
|
19
|
+
let record: string;
|
|
20
|
+
try {
|
|
21
|
+
record = JSON.stringify(event) + "\n";
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return fail(error);
|
|
24
|
+
}
|
|
25
|
+
pending = pending
|
|
26
|
+
.then(
|
|
27
|
+
() =>
|
|
28
|
+
new Promise<void>((resolve) => {
|
|
29
|
+
output.write(record, (error) => (error ? fail(error) : resolve()));
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
.catch(fail);
|
|
33
|
+
return pending;
|
|
34
|
+
};
|
|
35
|
+
// Keep the previous noninteractive trust policy; native UI must not introduce a blocking trust dialog.
|
|
36
|
+
pi.on("project_trust", (event) => {
|
|
37
|
+
try {
|
|
38
|
+
const agentDir = getAgentDir();
|
|
39
|
+
const remembered = new ProjectTrustStore(agentDir).get(event.cwd);
|
|
40
|
+
const trusted =
|
|
41
|
+
remembered ??
|
|
42
|
+
SettingsManager.create(event.cwd, agentDir, { projectTrusted: false }).getDefaultProjectTrust() === "always";
|
|
43
|
+
return { trusted: trusted ? "yes" : "no", remember: false };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return fail(error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
let guardedSession: { sessionManager: unknown; sessionId: unknown } | undefined;
|
|
49
|
+
pi.events.on(OWNED_PI_VIEW_READY, (ready) => {
|
|
50
|
+
if (ready && typeof ready === "object" && "sessionManager" in ready && "sessionId" in ready)
|
|
51
|
+
guardedSession = { sessionManager: ready.sessionManager, sessionId: ready.sessionId };
|
|
52
|
+
});
|
|
53
|
+
let started = false;
|
|
54
|
+
pi.on("session_start", (_event, ctx) => {
|
|
55
|
+
try {
|
|
56
|
+
if (started) throw Error("the owned session cannot be replaced");
|
|
57
|
+
if (
|
|
58
|
+
ctx.mode !== "tui" ||
|
|
59
|
+
guardedSession?.sessionManager !== ctx.sessionManager ||
|
|
60
|
+
guardedSession.sessionId !== ctx.sessionManager.getSessionId()
|
|
61
|
+
)
|
|
62
|
+
throw Error("the native Pi view guard is not ready for this session");
|
|
63
|
+
const header = ctx.sessionManager.getHeader();
|
|
64
|
+
if (
|
|
65
|
+
!header ||
|
|
66
|
+
header.type !== "session" ||
|
|
67
|
+
header.version !== 3 ||
|
|
68
|
+
header.id !== ctx.sessionManager.getSessionId()
|
|
69
|
+
)
|
|
70
|
+
throw Error("missing or mismatched actual Pi session header");
|
|
71
|
+
started = true;
|
|
72
|
+
return send(header);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return fail(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const forward = (event: unknown): Promise<void> => {
|
|
78
|
+
if (!started) fail("Pi event arrived before the guarded session");
|
|
79
|
+
return send(event);
|
|
80
|
+
};
|
|
81
|
+
pi.on("agent_start", forward);
|
|
82
|
+
pi.on("agent_end", forward);
|
|
83
|
+
pi.on("turn_start", forward);
|
|
84
|
+
pi.on("turn_end", forward);
|
|
85
|
+
pi.on("message_start", forward);
|
|
86
|
+
pi.on("message_end", forward);
|
|
87
|
+
pi.on("tool_execution_start", forward);
|
|
88
|
+
pi.on("tool_execution_update", forward);
|
|
89
|
+
pi.on("tool_execution_end", forward);
|
|
90
|
+
pi.on("message_update", (event: MessageUpdateEvent) => {
|
|
91
|
+
// Match Pi's JSON delta projection without copying growing partial-message snapshots.
|
|
92
|
+
if (event.message.role !== "assistant") fail("message_update is not an assistant message");
|
|
93
|
+
const usage = event.message.usage,
|
|
94
|
+
update = event.assistantMessageEvent;
|
|
95
|
+
if (!("partial" in update)) return forward({ type: event.type, usage, assistantMessageEvent: update });
|
|
96
|
+
const { partial, ...delta } = update;
|
|
97
|
+
if (update.type === "toolcall_start") {
|
|
98
|
+
const tool = partial.content[update.contentIndex];
|
|
99
|
+
if (tool?.type !== "toolCall") fail("toolcall_start lacks its actual tool call");
|
|
100
|
+
return forward({
|
|
101
|
+
type: event.type,
|
|
102
|
+
usage,
|
|
103
|
+
assistantMessageEvent: { ...delta, id: tool.id, toolName: tool.name },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return forward({ type: event.type, usage, assistantMessageEvent: delta });
|
|
107
|
+
});
|
|
108
|
+
pi.on("agent_settled", async (event, ctx) => {
|
|
109
|
+
await forward(event);
|
|
110
|
+
// The pipe has flushed before orderly shutdown; native receipt/session checks remain authoritative.
|
|
111
|
+
ctx.shutdown();
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
fail(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Install FIRST: native Pi remains observable, but only its parent can control work. */
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const OWNED_PI_VIEW_READY = "pi-daddy:owned-pi-view-ready";
|
|
6
|
+
export function failOwnedPiUi(error: unknown): never {
|
|
7
|
+
try {
|
|
8
|
+
writeSync(2, "pi-daddy: owned Pi UI refused: " + String(error) + "\n");
|
|
9
|
+
} finally {
|
|
10
|
+
process.exit(70);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function ownedPiView(pi: ExtensionAPI): void {
|
|
15
|
+
try {
|
|
16
|
+
if (typeof pi.registerMarkdownTransformer !== "function") throw Error("Pi lacks its public markdown renderer hook");
|
|
17
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
18
|
+
context.messageType === "assistant-thinking" ? "Thinking hidden" : markdown,
|
|
19
|
+
);
|
|
20
|
+
let guarded = false;
|
|
21
|
+
// No await here: the first session handler installs the guard before another handler can yield.
|
|
22
|
+
pi.on("session_start", (_event, ctx) => {
|
|
23
|
+
try {
|
|
24
|
+
if (guarded) throw Error("the owned session cannot be replaced");
|
|
25
|
+
if (
|
|
26
|
+
ctx.mode !== "tui" ||
|
|
27
|
+
typeof ctx.ui.onTerminalInput !== "function" ||
|
|
28
|
+
typeof ctx.ui.getToolsExpanded !== "function" ||
|
|
29
|
+
typeof ctx.ui.setToolsExpanded !== "function"
|
|
30
|
+
)
|
|
31
|
+
throw Error("Pi lacks the required native TUI controls");
|
|
32
|
+
const unsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
33
|
+
// The renderer consumes this exact terminal response before dispatching any user action.
|
|
34
|
+
if (/^\x1b\[6;\d+;\d+t$/.test(data)) return;
|
|
35
|
+
try {
|
|
36
|
+
if (["\x0f", "\x1b[111;5u", "\x1b[111;5:1u", "\x1b[111;5:2u", "\x1b[27;5;111~"].includes(data))
|
|
37
|
+
ctx.ui.setToolsExpanded(!ctx.ui.getToolsExpanded());
|
|
38
|
+
} catch (error) {
|
|
39
|
+
failOwnedPiUi(error);
|
|
40
|
+
}
|
|
41
|
+
return { consume: true };
|
|
42
|
+
});
|
|
43
|
+
if (typeof unsubscribe !== "function") throw Error("Pi did not install the terminal input guard");
|
|
44
|
+
ctx.ui.setEditorText("");
|
|
45
|
+
ctx.ui.setStatus("pi-daddy-owned", "View only · parent controls the task · Ctrl+O expands tools");
|
|
46
|
+
guarded = true;
|
|
47
|
+
pi.events.emit(OWNED_PI_VIEW_READY, {
|
|
48
|
+
sessionManager: ctx.sessionManager,
|
|
49
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
failOwnedPiUi(error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
failOwnedPiUi(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface OwnedChildRunRequest extends ChildRunRequest {
|
|
|
25
25
|
onOwnership?: (identity: CapturedWorkerIdentity) => Promise<void> | void;
|
|
26
26
|
/** JSON protocol consumers observe all bytes without capturing enormous replay streams as answer text. */
|
|
27
27
|
captureStdout?: boolean;
|
|
28
|
+
/** Actual pane terminal for Pi's native view. Protocol events arrive separately on fd 6. */
|
|
29
|
+
terminalUi?: boolean;
|
|
28
30
|
/** Bounded diagnostics may truncate without stopping independently captured primary output. */
|
|
29
31
|
stopOnOutputLimit?: boolean;
|
|
30
32
|
}
|
|
@@ -46,6 +48,8 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
46
48
|
if (request.signal?.aborted) return notStarted("cancelled before helper launch", true);
|
|
47
49
|
if (process.platform !== "linux" || process.arch !== "x64")
|
|
48
50
|
return notStarted("captured worker requires qualified Linux x64 helper");
|
|
51
|
+
if (request.terminalUi && (!process.stdin.isTTY || !process.stdout.isTTY || !process.stderr.isTTY))
|
|
52
|
+
return notStarted("native Pi view requires a real pane terminal on stdin, stdout and stderr");
|
|
49
53
|
let helper: string, root: string, hash: string;
|
|
50
54
|
const nonce = randomUUID();
|
|
51
55
|
let binary: import("node:fs/promises").FileHandle | undefined;
|
|
@@ -95,13 +99,26 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
95
99
|
{
|
|
96
100
|
cwd: root,
|
|
97
101
|
env: request.env,
|
|
98
|
-
stdio:
|
|
99
|
-
|
|
102
|
+
stdio: request.terminalUi
|
|
103
|
+
? ["inherit", "inherit", "inherit", "pipe", "pipe", binary!.fd, "pipe"]
|
|
104
|
+
: ["ignore", "pipe", "pipe", "pipe", "pipe", binary!.fd],
|
|
105
|
+
// The UI shares the pane's foreground process group for raw input and SIGWINCH.
|
|
106
|
+
// Cleanup still targets owned descendants by pidfd, never by process group.
|
|
107
|
+
detached: !request.terminalUi,
|
|
100
108
|
},
|
|
101
109
|
);
|
|
102
110
|
void binary!.close();
|
|
103
111
|
const control = child.stdio[3] as import("node:stream").Duplex;
|
|
104
112
|
const status = child.stdio[4] as import("node:stream").Readable;
|
|
113
|
+
const streams = {
|
|
114
|
+
stdout: request.terminalUi ? (child.stdio.at(6) as import("node:stream").Readable) : child.stdout,
|
|
115
|
+
stderr: child.stderr,
|
|
116
|
+
};
|
|
117
|
+
const closeObservations = () => {
|
|
118
|
+
streams.stdout?.destroy();
|
|
119
|
+
streams.stderr?.destroy();
|
|
120
|
+
status.destroy();
|
|
121
|
+
};
|
|
105
122
|
let identity: CapturedWorkerIdentity | undefined;
|
|
106
123
|
let text = "",
|
|
107
124
|
count = 0,
|
|
@@ -146,9 +163,7 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
146
163
|
stopWatchdog = setTimeout(() => {
|
|
147
164
|
failure ??= "worker helper failed to establish cleanup before its bound";
|
|
148
165
|
child.kill("SIGKILL");
|
|
149
|
-
|
|
150
|
-
child.stderr?.destroy();
|
|
151
|
-
status.destroy();
|
|
166
|
+
closeObservations();
|
|
152
167
|
}, cleanupCeiling + 1000);
|
|
153
168
|
};
|
|
154
169
|
function armIdle() {
|
|
@@ -160,10 +175,15 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
160
175
|
stop();
|
|
161
176
|
}, request.idleTimeoutMs);
|
|
162
177
|
}
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
streams.stdout?.on("data", (bytes: Buffer) => observe("stdout", bytes));
|
|
179
|
+
streams.stderr?.on("data", (bytes: Buffer) => observe("stderr", bytes));
|
|
180
|
+
for (const stream of Object.values(streams))
|
|
181
|
+
stream?.on("error", (error) => {
|
|
182
|
+
failure ??= "child event channel failed: " + String(error);
|
|
183
|
+
stop();
|
|
184
|
+
});
|
|
165
185
|
for (const stream of ["stdout", "stderr"] as const)
|
|
166
|
-
|
|
186
|
+
streams[stream]?.on("end", () => {
|
|
167
187
|
const tail = decoders[stream].end();
|
|
168
188
|
if (tail && !(stream === "stdout" && request.captureStdout === false)) {
|
|
169
189
|
const kept = takeBytes(tail, Math.max(0, (request.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES) - count));
|
|
@@ -276,9 +296,7 @@ export async function runOwnedChild(request: OwnedChildRunRequest): Promise<Owne
|
|
|
276
296
|
void (async () => {
|
|
277
297
|
if (!identity || !(await readCapturedWorkerReceipt(identity))) {
|
|
278
298
|
failure ??= "helper exited without an independent settlement receipt";
|
|
279
|
-
|
|
280
|
-
child.stderr?.destroy();
|
|
281
|
-
status.destroy();
|
|
299
|
+
closeObservations();
|
|
282
300
|
}
|
|
283
301
|
})();
|
|
284
302
|
});
|
|
@@ -80,7 +80,7 @@ export interface PromptOutcome {
|
|
|
80
80
|
|
|
81
81
|
export interface ApprovalGateOptions {
|
|
82
82
|
ui: ApprovalUI;
|
|
83
|
-
/**
|
|
83
|
+
/** Whether a human can answer here. False in print/json mode and view-only child tabs. */
|
|
84
84
|
hasUI: boolean;
|
|
85
85
|
/** pi's `ctx.mode`, quoted back in the refusal so an operator can see why. */
|
|
86
86
|
mode: string;
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,8 @@ export interface SelectedCommand {
|
|
|
12
12
|
name: string;
|
|
13
13
|
sourceInfo?: { path?: string };
|
|
14
14
|
}
|
|
15
|
-
const
|
|
15
|
+
const requiredPhases = ["plan", "build", "review", "debug", "investigate"];
|
|
16
|
+
const phases = [...requiredPhases, "test-review"];
|
|
16
17
|
const hash = (value: string) => createHash("sha256").update(value, "utf8").digest("hex");
|
|
17
18
|
const object = (x: unknown): x is Record<string, unknown> => !!x && typeof x === "object" && !Array.isArray(x);
|
|
18
19
|
const exact = (x: Record<string, unknown>, keys: string[]) =>
|
|
@@ -57,10 +58,10 @@ async function principalBinding(
|
|
|
57
58
|
manifest.version !== 1 ||
|
|
58
59
|
manifest.package !== "principal-pi-skills" ||
|
|
59
60
|
!object(manifest.bindings) ||
|
|
60
|
-
!exact(manifest.bindings, phases)
|
|
61
|
+
!exact(manifest.bindings, Object.hasOwn(manifest.bindings, "test-review") ? phases : requiredPhases)
|
|
61
62
|
)
|
|
62
63
|
throw Error("invalid Principal binding manifest");
|
|
63
|
-
for (const name of
|
|
64
|
+
for (const name of Object.keys(manifest.bindings)) {
|
|
64
65
|
const row = manifest.bindings[name];
|
|
65
66
|
if (
|
|
66
67
|
!object(row) ||
|
|
@@ -74,6 +75,7 @@ async function principalBinding(
|
|
|
74
75
|
)
|
|
75
76
|
throw Error("invalid Principal binding row");
|
|
76
77
|
}
|
|
78
|
+
if (!Object.hasOwn(manifest.bindings, phase)) throw Error("selected Principal phase has no binding row");
|
|
77
79
|
const row = manifest.bindings[phase] as Record<string, string>;
|
|
78
80
|
const agentPath = join(root, row.agent);
|
|
79
81
|
if (
|
|
@@ -376,6 +376,17 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
376
376
|
notice = (await connection.change({ action: "set", edits: trimmed.slice(2).trim() }))
|
|
377
377
|
? "Session models updated"
|
|
378
378
|
: connection.state.error || "Session controls unavailable";
|
|
379
|
+
} else if (trimmed.startsWith("limit ")) {
|
|
380
|
+
const edit = /^limit (wall|idle) ([0-9]+)$/.exec(trimmed);
|
|
381
|
+
if (!edit) notice = "Use limit wall <seconds> or limit idle <seconds>; 0 restores default";
|
|
382
|
+
else
|
|
383
|
+
notice = (await connection.change({
|
|
384
|
+
action: "set-limit",
|
|
385
|
+
key: edit[1] as "wall" | "idle",
|
|
386
|
+
seconds: Number(edit[2]),
|
|
387
|
+
}))
|
|
388
|
+
? "Session limit updated for future children; running children unchanged"
|
|
389
|
+
: connection.state.error || "Settings unavailable";
|
|
379
390
|
} else notice = display.input(line) ? "" : `Unknown command: ${trimmed}. Press ? for help.`;
|
|
380
391
|
redraw();
|
|
381
392
|
};
|
|
@@ -397,7 +408,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
397
408
|
redraw();
|
|
398
409
|
return;
|
|
399
410
|
}
|
|
400
|
-
|
|
411
|
+
let action = display.key(
|
|
401
412
|
key.name === "return"
|
|
402
413
|
? "return"
|
|
403
414
|
: ["up", "down", "escape", "tab"].includes(key.name || "")
|
|
@@ -409,6 +420,32 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
409
420
|
return;
|
|
410
421
|
}
|
|
411
422
|
if (action === "command") command = "";
|
|
423
|
+
if (action === "setting") {
|
|
424
|
+
const selected = display.screen.settingKey ?? "auto";
|
|
425
|
+
const settings = connection.state.snapshot?.settings;
|
|
426
|
+
if (!settings?.editable) notice = "Settings can only be changed in the connected owning parent";
|
|
427
|
+
else if (selected === "auto") action = "auto";
|
|
428
|
+
else if (selected === "wall" || selected === "idle") command = `limit ${selected} `;
|
|
429
|
+
else if (selected === "descendants") notice = "Startup only: set PI_DADDY_FANOUT before restarting Pi";
|
|
430
|
+
else if (selected === "depth") notice = "Startup only: set PI_DADDY_MAX_DEPTH before restarting Pi";
|
|
431
|
+
else if (selected === "perCall") notice = "Children per call is a fixed safety bound";
|
|
432
|
+
else if (!settings.jev.available)
|
|
433
|
+
notice = "JEV controls unavailable; load skill-harness in the parent Pi session";
|
|
434
|
+
else {
|
|
435
|
+
void connection
|
|
436
|
+
.change({ action: "set-jev", enabled: !(settings.jev.enabled || settings.jev.pending) })
|
|
437
|
+
.then((changed) => {
|
|
438
|
+
const jev = connection.state.snapshot?.settings?.jev;
|
|
439
|
+
notice = !changed
|
|
440
|
+
? connection.state.error || "JEV change already pending"
|
|
441
|
+
: jev?.pending
|
|
442
|
+
? "Complete JEV paid-call and LoRA-storage choices in the parent Pi session"
|
|
443
|
+
: jev?.error ||
|
|
444
|
+
`JEV ${jev?.enabled ? "enabled" : "disabled"}; ${jev?.availability ?? "status unknown"}`;
|
|
445
|
+
redraw();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
412
449
|
if (action === "auto") {
|
|
413
450
|
const snapshot = connection.state.snapshot;
|
|
414
451
|
if (!snapshot) notice = "Daddy permission controls unavailable";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DASHBOARD_SETTING_KEYS } from "./dashboard-settings.ts";
|
|
1
2
|
/** Local display controls; these never dispatch actions to an owner or change ledger state. */
|
|
2
3
|
import { dashboardScreenState } from "./dashboard-screen.ts";
|
|
3
4
|
import { ActivityTimelineAliases } from "./activity-timeline.ts";
|
|
@@ -16,16 +17,17 @@ export function createDashboardDisplayControls(details: boolean, ledger: boolean
|
|
|
16
17
|
return {
|
|
17
18
|
state,
|
|
18
19
|
screen,
|
|
19
|
-
key(key: string): "auto" | "quit" | "command" | undefined {
|
|
20
|
+
key(key: string): "auto" | "quit" | "command" | "setting" | undefined {
|
|
20
21
|
if (key === "a") return "auto";
|
|
21
22
|
if (key === "q" || key === "ctrl-c") return "quit";
|
|
22
23
|
if (key === ":") return "command";
|
|
24
|
+
if (screen.view === "settings" && (key === "return" || key === "d")) return "setting";
|
|
23
25
|
if (key === "escape") {
|
|
24
26
|
screen.view = screen.view === "version-details" ? "versions" : "main";
|
|
25
27
|
state.activityDetail = undefined;
|
|
26
28
|
screen.offset = 0;
|
|
27
|
-
} else if (key === "?" || key === "m" || key === "v") {
|
|
28
|
-
screen.view = key === "?" ? "help" : key === "v" ? "versions" : "models";
|
|
29
|
+
} else if (key === "?" || key === "m" || key === "v" || key === "s") {
|
|
30
|
+
screen.view = key === "?" ? "help" : key === "v" ? "versions" : key === "s" ? "settings" : "models";
|
|
29
31
|
state.activityDetail = undefined;
|
|
30
32
|
screen.offset = 0;
|
|
31
33
|
} else if (key === "return" || key === "d") {
|
|
@@ -33,7 +35,11 @@ export function createDashboardDisplayControls(details: boolean, ledger: boolean
|
|
|
33
35
|
screen.offset = 0;
|
|
34
36
|
} else if (["up", "down", "j", "k"].includes(key)) {
|
|
35
37
|
const delta = key === "up" || key === "k" ? -1 : 1;
|
|
36
|
-
if (screen.view === "
|
|
38
|
+
if (screen.view === "settings") {
|
|
39
|
+
const index = DASHBOARD_SETTING_KEYS.indexOf(screen.settingKey ?? "auto");
|
|
40
|
+
screen.settingKey =
|
|
41
|
+
DASHBOARD_SETTING_KEYS[Math.max(0, Math.min(DASHBOARD_SETTING_KEYS.length - 1, index + delta))];
|
|
42
|
+
} else if (screen.view === "versions") {
|
|
37
43
|
const rows = screen.versionRows ?? [],
|
|
38
44
|
index = rows.findIndex((row) => row.id === screen.versionId);
|
|
39
45
|
screen.versionId = rows[Math.max(0, Math.min(rows.length - 1, index + delta))]?.id;
|