pi-claude-supervisor 0.2.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 +36 -0
- package/LICENSE +21 -0
- package/README.cn.md +98 -0
- package/README.md +122 -0
- package/docs/architecture.md +122 -0
- package/docs/engineering-plan.md +918 -0
- package/docs/implementation-review.md +62 -0
- package/docs/independent-review.md +332 -0
- package/docs/releasing.md +112 -0
- package/docs/testing.md +109 -0
- package/docs/transport-spike-2026-09-12.md +94 -0
- package/package.json +72 -0
- package/src/config.ts +35 -0
- package/src/decision-session-store.ts +170 -0
- package/src/decision-worker.ts +245 -0
- package/src/events.ts +179 -0
- package/src/index.ts +500 -0
- package/src/notifications.ts +85 -0
- package/src/policy.ts +61 -0
- package/src/state.ts +44 -0
- package/src/supervisor.ts +626 -0
- package/src/types.ts +122 -0
- package/src/verifier.ts +40 -0
- package/src/worker/environment.ts +31 -0
- package/src/worker/process-adapter.ts +604 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export type SupervisorState =
|
|
2
|
+
| "idle"
|
|
3
|
+
| "starting"
|
|
4
|
+
| "running"
|
|
5
|
+
| "waiting"
|
|
6
|
+
| "paused"
|
|
7
|
+
| "verifying"
|
|
8
|
+
| "completed"
|
|
9
|
+
| "failed"
|
|
10
|
+
| "stopped";
|
|
11
|
+
|
|
12
|
+
export type WorkerExitReason = "completed" | "failed" | "stopped" | "crashed" | "unknown";
|
|
13
|
+
|
|
14
|
+
export interface WorkerPermissionRequest {
|
|
15
|
+
requestId: string;
|
|
16
|
+
toolUseId: string;
|
|
17
|
+
toolName: string;
|
|
18
|
+
input: unknown;
|
|
19
|
+
raw: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type WorkerEvent =
|
|
23
|
+
| { type: "output"; handle: WorkerHandle; chunk: WorkerOutputChunk }
|
|
24
|
+
| { type: "jsonl"; handle: WorkerHandle; record: Record<string, unknown> }
|
|
25
|
+
| { type: "turn_completed"; handle: WorkerHandle; result: Record<string, unknown>; sequence: number }
|
|
26
|
+
| { type: "permission_request"; handle: WorkerHandle; request: WorkerPermissionRequest }
|
|
27
|
+
| { type: "exited"; handle: WorkerHandle; exitCode?: number | null; signal?: NodeJS.Signals };
|
|
28
|
+
|
|
29
|
+
export type WorkerEventListener = (event: WorkerEvent) => void | Promise<void>;
|
|
30
|
+
|
|
31
|
+
export interface WorkerStartInput {
|
|
32
|
+
task: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
command: string;
|
|
35
|
+
args?: string[];
|
|
36
|
+
env?: NodeJS.ProcessEnv;
|
|
37
|
+
approval?: { actor: "human"; reason: string };
|
|
38
|
+
eventListener?: WorkerEventListener;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkerHandle {
|
|
42
|
+
id: string;
|
|
43
|
+
pid?: number;
|
|
44
|
+
startedAt: string;
|
|
45
|
+
cwd: string;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkerStatus {
|
|
50
|
+
handle: WorkerHandle;
|
|
51
|
+
running: boolean;
|
|
52
|
+
exitCode?: number | null;
|
|
53
|
+
signal?: NodeJS.Signals;
|
|
54
|
+
lastOutputAt?: string;
|
|
55
|
+
lastInputAt?: string;
|
|
56
|
+
activeRequests?: number;
|
|
57
|
+
exitReason?: WorkerExitReason;
|
|
58
|
+
/** Whether the detached process group or cgroup has been confirmed gone. */
|
|
59
|
+
processGroupCleaned?: boolean;
|
|
60
|
+
/** Whether a Linux cgroup provided descendant cleanup for this worker. */
|
|
61
|
+
cgroupCleaned?: boolean;
|
|
62
|
+
/** cgroup attachment was unavailable and a fallback may have been used. */
|
|
63
|
+
cgroupError?: string;
|
|
64
|
+
/** Cleanup failure is diagnostic and must be treated as a safety failure. */
|
|
65
|
+
cleanupError?: string;
|
|
66
|
+
runtimeError?: string;
|
|
67
|
+
outputTruncated?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PermissionDecision {
|
|
71
|
+
behavior: "allow" | "deny";
|
|
72
|
+
message?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WorkerAdapter {
|
|
76
|
+
capabilities(): WorkerCapabilities;
|
|
77
|
+
start(input: WorkerStartInput): Promise<WorkerHandle>;
|
|
78
|
+
getStatus(handle: WorkerHandle): Promise<WorkerStatus>;
|
|
79
|
+
readOutput(handle: WorkerHandle): Promise<WorkerOutputChunk[]>;
|
|
80
|
+
/** Restore chunks when diagnostic event persistence fails before acknowledgement. */
|
|
81
|
+
restoreOutput?(handle: WorkerHandle, chunks: WorkerOutputChunk[]): Promise<void>;
|
|
82
|
+
/** Subscribe to transport and process lifecycle events without polling. */
|
|
83
|
+
subscribe?(handle: WorkerHandle, listener: WorkerEventListener): () => void;
|
|
84
|
+
/** Respond to Claude Code's stdio permission request. */
|
|
85
|
+
respondPermission?(handle: WorkerHandle, requestId: string, toolUseId: string, decision: PermissionDecision, updatedInput?: unknown): Promise<void>;
|
|
86
|
+
send(handle: WorkerHandle, message: string, idempotencyKey: string): Promise<void>;
|
|
87
|
+
pause(handle: WorkerHandle): Promise<void>;
|
|
88
|
+
resume(handle: WorkerHandle): Promise<void>;
|
|
89
|
+
stop(handle: WorkerHandle, reason: string): Promise<void>;
|
|
90
|
+
killProcessGroup(handle: WorkerHandle, reason: string): Promise<void>;
|
|
91
|
+
resumeSession(sessionId: string): Promise<WorkerHandle>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface WorkerOutputChunk {
|
|
95
|
+
stream: "stdout" | "stderr";
|
|
96
|
+
text: string;
|
|
97
|
+
at: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface WorkerCapabilities {
|
|
101
|
+
transport: "process-pipe" | "pty" | "jsonl";
|
|
102
|
+
interactiveInput: boolean;
|
|
103
|
+
pause: boolean;
|
|
104
|
+
resumeSession: boolean;
|
|
105
|
+
processGroupControl: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TaskContext {
|
|
109
|
+
taskId: string;
|
|
110
|
+
task: string;
|
|
111
|
+
cwd: string;
|
|
112
|
+
maxTurns: number;
|
|
113
|
+
startedAt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface VerificationResult {
|
|
117
|
+
ok: boolean;
|
|
118
|
+
command: string;
|
|
119
|
+
exitCode: number;
|
|
120
|
+
output: string;
|
|
121
|
+
checkedAt: string;
|
|
122
|
+
}
|
package/src/verifier.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { VerificationResult } from "./types.ts";
|
|
4
|
+
import { assertSafeWorkerCommand } from "./policy.ts";
|
|
5
|
+
import { workerEnvironment } from "./worker/environment.ts";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
export interface VerificationCommand {
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function verify(
|
|
15
|
+
cwd: string,
|
|
16
|
+
command: VerificationCommand = { command: "git", args: ["diff", "--check"] },
|
|
17
|
+
timeoutMs = 120_000,
|
|
18
|
+
): Promise<VerificationResult> {
|
|
19
|
+
assertSafeWorkerCommand(command.command, command.args);
|
|
20
|
+
const rendered = [command.command, ...(command.args ?? [])].join(" ");
|
|
21
|
+
try {
|
|
22
|
+
const result = await execFileAsync(command.command, command.args ?? [], {
|
|
23
|
+
cwd,
|
|
24
|
+
timeout: timeoutMs,
|
|
25
|
+
maxBuffer: 256 * 1024,
|
|
26
|
+
env: workerEnvironment(process.env, { GIT_TERMINAL_PROMPT: "0" }),
|
|
27
|
+
});
|
|
28
|
+
return { ok: true, command: rendered, exitCode: 0, output: `${result.stdout}${result.stderr}`, checkedAt: new Date().toISOString() };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const failure = error as { code?: number | string; stdout?: string; stderr?: string; message?: string };
|
|
31
|
+
const exitCode = typeof failure.code === "number" ? failure.code : 1;
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
command: rendered,
|
|
35
|
+
exitCode,
|
|
36
|
+
output: `${failure.stdout ?? ""}${failure.stderr ?? ""}${failure.message ?? ""}`.slice(-256 * 1024),
|
|
37
|
+
checkedAt: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const inheritedNames = [
|
|
2
|
+
"PATH",
|
|
3
|
+
"HOME",
|
|
4
|
+
"USER",
|
|
5
|
+
"LOGNAME",
|
|
6
|
+
"SHELL",
|
|
7
|
+
"TMPDIR",
|
|
8
|
+
"LANG",
|
|
9
|
+
"LC_ALL",
|
|
10
|
+
"LC_CTYPE",
|
|
11
|
+
"TERM",
|
|
12
|
+
"COLORTERM",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a least-privilege worker environment. Credentials and arbitrary host
|
|
17
|
+
* variables are not inherited unless the caller explicitly supplies them.
|
|
18
|
+
*/
|
|
19
|
+
export function workerEnvironment(
|
|
20
|
+
inherited: NodeJS.ProcessEnv = process.env,
|
|
21
|
+
explicit: NodeJS.ProcessEnv = {},
|
|
22
|
+
): NodeJS.ProcessEnv {
|
|
23
|
+
const result: NodeJS.ProcessEnv = {};
|
|
24
|
+
for (const name of inheritedNames) {
|
|
25
|
+
if (inherited[name] !== undefined) result[name] = inherited[name];
|
|
26
|
+
}
|
|
27
|
+
for (const [name, value] of Object.entries(explicit)) {
|
|
28
|
+
if (value !== undefined) result[name] = value;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|