pi-webdesk 0.1.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.
Files changed (64) hide show
  1. package/README.md +111 -0
  2. package/dist/apps/daemon/src/appearance-preferences.js +218 -0
  3. package/dist/apps/daemon/src/auth.js +88 -0
  4. package/dist/apps/daemon/src/bin.js +123 -0
  5. package/dist/apps/daemon/src/cli.js +48 -0
  6. package/dist/apps/daemon/src/event-hub.js +155 -0
  7. package/dist/apps/daemon/src/index.js +102 -0
  8. package/dist/apps/daemon/src/launcher-control.js +114 -0
  9. package/dist/apps/daemon/src/launcher.js +73 -0
  10. package/dist/apps/daemon/src/pi-auth.js +290 -0
  11. package/dist/apps/daemon/src/pi-resources.js +182 -0
  12. package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
  13. package/dist/apps/daemon/src/pi-sessions.js +265 -0
  14. package/dist/apps/daemon/src/runtime-process.js +241 -0
  15. package/dist/apps/daemon/src/secret.js +71 -0
  16. package/dist/apps/daemon/src/server.js +1662 -0
  17. package/dist/apps/daemon/src/session-projection.js +117 -0
  18. package/dist/apps/daemon/src/state-lock.js +31 -0
  19. package/dist/apps/daemon/src/static-web.js +53 -0
  20. package/dist/apps/daemon/src/task-archive.js +152 -0
  21. package/dist/apps/daemon/src/task-commit.js +503 -0
  22. package/dist/apps/daemon/src/task-merge.js +912 -0
  23. package/dist/apps/daemon/src/task-review.js +204 -0
  24. package/dist/apps/daemon/src/task-runtime.js +1124 -0
  25. package/dist/apps/daemon/src/task-validation.js +352 -0
  26. package/dist/apps/daemon/src/workspace-store.js +140 -0
  27. package/dist/apps/daemon/src/workspace.js +795 -0
  28. package/dist/extensions/webdesk.js +34 -0
  29. package/dist/packages/git/src/commit.js +675 -0
  30. package/dist/packages/git/src/errors.js +55 -0
  31. package/dist/packages/git/src/fingerprint.js +286 -0
  32. package/dist/packages/git/src/index.js +123 -0
  33. package/dist/packages/git/src/merge.js +1008 -0
  34. package/dist/packages/git/src/paths.js +58 -0
  35. package/dist/packages/git/src/repository.js +77 -0
  36. package/dist/packages/git/src/review.js +396 -0
  37. package/dist/packages/git/src/runner.js +110 -0
  38. package/dist/packages/git/src/validation.js +263 -0
  39. package/dist/packages/git/src/worktree.js +233 -0
  40. package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
  41. package/dist/packages/pi-bridge/src/auth.js +80 -0
  42. package/dist/packages/pi-bridge/src/errors.js +19 -0
  43. package/dist/packages/pi-bridge/src/handshake.js +43 -0
  44. package/dist/packages/pi-bridge/src/index.js +76 -0
  45. package/dist/packages/pi-bridge/src/jsonl.js +105 -0
  46. package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
  47. package/dist/packages/pi-bridge/src/resolve.js +59 -0
  48. package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
  49. package/dist/packages/pi-bridge/src/resources.js +481 -0
  50. package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
  51. package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
  52. package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
  53. package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
  54. package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
  55. package/dist/packages/pi-bridge/src/runtime.js +0 -0
  56. package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
  57. package/dist/packages/pi-bridge/src/sessions.js +314 -0
  58. package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
  59. package/dist/packages/protocol/src/index.js +1863 -0
  60. package/dist/web/assets/index-BOw_fhvO.css +2 -0
  61. package/dist/web/assets/index-oXs7yAAo.js +119 -0
  62. package/dist/web/index.html +14 -0
  63. package/package.json +69 -0
  64. package/scripts/prepare.mjs +7 -0
@@ -0,0 +1,263 @@
1
+ // packages/git/src/validation.ts
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ var DEFAULT_VALIDATION_TIMEOUT_MS = 6e5;
6
+ var MAX_VALIDATION_TIMEOUT_MS = 6e5;
7
+ var DEFAULT_VALIDATION_MAX_OUTPUT_CHARS = 1e5;
8
+ var MAX_VALIDATION_COMMAND_CHARS = 4096;
9
+ var DEFAULT_TERM_GRACE_MS = 5e3;
10
+ var MAX_TERM_GRACE_MS = 1e4;
11
+ var SETTLE_FAILSAFE_SLACK_MS = 1e3;
12
+ var VALIDATION_SUPERVISOR_SCRIPT = String.raw`
13
+ validation_command=$1
14
+ term_grace_seconds=$2
15
+ process_group=$$
16
+ exec 3<&0
17
+ (
18
+ trap 'exit 0' TERM HUP INT
19
+ if ! IFS= read -r _ <&3; then
20
+ trap '' TERM HUP INT
21
+ kill -TERM "-$process_group" 2>/dev/null || true
22
+ sleep "$term_grace_seconds"
23
+ kill -KILL "-$process_group" 2>/dev/null || true
24
+ fi
25
+ ) &
26
+ watchdog=$!
27
+ /bin/sh -lc "$validation_command" </dev/null
28
+ command_status=$?
29
+ kill -KILL "$watchdog" 2>/dev/null || true
30
+ wait "$watchdog" 2>/dev/null || true
31
+ exit "$command_status"
32
+ `;
33
+ var ValidationRunnerError = class extends Error {
34
+ name = "ValidationRunnerError";
35
+ code;
36
+ constructor(code, message, options) {
37
+ super(message, options);
38
+ this.code = code;
39
+ }
40
+ };
41
+ function isValidationRunnerError(value, code) {
42
+ if (!(value instanceof ValidationRunnerError)) return false;
43
+ return code === void 0 || value.code === code;
44
+ }
45
+ function boundedNumber(value, min, max) {
46
+ if (!Number.isSafeInteger(value) || value < min) return min;
47
+ return Math.min(value, max);
48
+ }
49
+ function commandCharacterIsSafe(character) {
50
+ return character === "\n" || character === " " || !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/u.test(character) && (!new RegExp("\\p{Zs}", "u").test(character) || character === " ") && !/[\u115F\u1160\u2800\u3164\uFFA0]/u.test(character);
51
+ }
52
+ function startValidationRun(options) {
53
+ const command = options.command;
54
+ if (typeof command !== "string" || command.trim().length === 0 || command.length > MAX_VALIDATION_COMMAND_CHARS || ![...command].every(commandCharacterIsSafe)) {
55
+ throw new ValidationRunnerError(
56
+ "VALIDATION_COMMAND_INVALID",
57
+ `Validation commands must be non-empty, at most ${MAX_VALIDATION_COMMAND_CHARS} characters, and free of unsafe invisible characters`
58
+ );
59
+ }
60
+ if (!path.isAbsolute(options.worktreePath)) {
61
+ throw new ValidationRunnerError(
62
+ "VALIDATION_WORKTREE_INVALID",
63
+ "Validation runs require an absolute, caller-verified worktree path"
64
+ );
65
+ }
66
+ const timeoutMs = boundedNumber(
67
+ options.timeoutMs ?? DEFAULT_VALIDATION_TIMEOUT_MS,
68
+ 1,
69
+ MAX_VALIDATION_TIMEOUT_MS
70
+ );
71
+ const maxOutputChars = boundedNumber(
72
+ options.maxOutputChars ?? DEFAULT_VALIDATION_MAX_OUTPUT_CHARS,
73
+ 1,
74
+ DEFAULT_VALIDATION_MAX_OUTPUT_CHARS
75
+ );
76
+ const termGraceMs = boundedNumber(
77
+ options.termGraceMs ?? DEFAULT_TERM_GRACE_MS,
78
+ 1,
79
+ MAX_TERM_GRACE_MS
80
+ );
81
+ const now = options.now ?? Date.now;
82
+ const spawnImpl = options.spawnImpl ?? spawn;
83
+ const startedAtMs = now();
84
+ let child;
85
+ try {
86
+ child = spawnImpl(
87
+ "/bin/sh",
88
+ [
89
+ "-c",
90
+ VALIDATION_SUPERVISOR_SCRIPT,
91
+ "pita-validation-supervisor",
92
+ command,
93
+ String(termGraceMs / 1e3)
94
+ ],
95
+ {
96
+ cwd: options.worktreePath,
97
+ env: { ...process.env },
98
+ detached: true,
99
+ stdio: ["pipe", "pipe", "pipe"]
100
+ }
101
+ );
102
+ } catch (error) {
103
+ throw new ValidationRunnerError(
104
+ "VALIDATION_SPAWN_FAILED",
105
+ "The validation shell could not be started",
106
+ { cause: error }
107
+ );
108
+ }
109
+ let output = "";
110
+ let omittedOutputChars = 0;
111
+ let timedOut = false;
112
+ let disposed = false;
113
+ let settled = false;
114
+ let exitRecord = null;
115
+ const timers = [];
116
+ function append(text) {
117
+ if (text.length === 0) return;
118
+ const capacity = maxOutputChars - output.length;
119
+ if (capacity <= 0) {
120
+ omittedOutputChars += text.length;
121
+ return;
122
+ }
123
+ if (text.length <= capacity) {
124
+ output += text;
125
+ return;
126
+ }
127
+ output += text.slice(0, capacity);
128
+ omittedOutputChars += text.length - capacity;
129
+ }
130
+ const decoders = [];
131
+ for (const stream of [child.stdout, child.stderr]) {
132
+ if (stream === null) continue;
133
+ const decoder = new StringDecoder("utf8");
134
+ const state = { decoder, ended: false };
135
+ decoders.push(state);
136
+ stream.on("data", (chunk) => append(decoder.write(chunk)));
137
+ stream.on("end", () => {
138
+ if (state.ended) return;
139
+ state.ended = true;
140
+ append(decoder.end());
141
+ });
142
+ stream.on("error", () => void 0);
143
+ }
144
+ function flushDecoders() {
145
+ for (const state of decoders) {
146
+ if (state.ended) continue;
147
+ state.ended = true;
148
+ append(state.decoder.end());
149
+ }
150
+ }
151
+ function signalGroup(signal) {
152
+ const pid = child.pid;
153
+ if (pid === void 0) return;
154
+ try {
155
+ process.kill(-pid, signal);
156
+ } catch {
157
+ }
158
+ }
159
+ function groupIsAlive() {
160
+ const pid = child.pid;
161
+ if (pid === void 0) return false;
162
+ try {
163
+ process.kill(-pid, 0);
164
+ return true;
165
+ } catch (error) {
166
+ return !(error !== null && typeof error === "object" && "code" in error && error.code === "ESRCH");
167
+ }
168
+ }
169
+ function scheduleTimer(callback, delayMs) {
170
+ const timer = setTimeout(callback, delayMs);
171
+ timer.unref?.();
172
+ timers.push(timer);
173
+ }
174
+ function escalate() {
175
+ if (groupIsAlive()) signalGroup("SIGTERM");
176
+ scheduleTimer(() => {
177
+ if (groupIsAlive()) signalGroup("SIGKILL");
178
+ }, termGraceMs);
179
+ }
180
+ let settle = () => void 0;
181
+ let fail = () => void 0;
182
+ const result = new Promise((resolve, reject) => {
183
+ settle = () => {
184
+ if (settled) return;
185
+ flushDecoders();
186
+ settled = true;
187
+ for (const timer of timers) clearTimeout(timer);
188
+ if (disposed) {
189
+ reject(
190
+ new ValidationRunnerError(
191
+ "VALIDATION_DISPOSED",
192
+ "The validation run was disposed before completion"
193
+ )
194
+ );
195
+ return;
196
+ }
197
+ const exitCode = exitRecord?.code ?? null;
198
+ const signal = exitRecord?.signal ?? null;
199
+ resolve({
200
+ outcome: timedOut ? "timed-out" : exitCode === 0 && signal === null ? "passed" : "failed",
201
+ exitCode,
202
+ signal,
203
+ output,
204
+ outputTruncated: omittedOutputChars > 0,
205
+ omittedOutputChars,
206
+ startedAtMs,
207
+ finishedAtMs: Math.max(now(), startedAtMs)
208
+ });
209
+ };
210
+ fail = (error) => {
211
+ if (settled) return;
212
+ settled = true;
213
+ for (const timer of timers) clearTimeout(timer);
214
+ reject(error);
215
+ };
216
+ });
217
+ result.catch(() => void 0);
218
+ scheduleTimer(() => {
219
+ if (exitRecord !== null) return;
220
+ timedOut = true;
221
+ escalate();
222
+ scheduleTimer(settle, termGraceMs + SETTLE_FAILSAFE_SLACK_MS);
223
+ }, timeoutMs);
224
+ child.on("error", (error) => {
225
+ fail(
226
+ new ValidationRunnerError(
227
+ "VALIDATION_SPAWN_FAILED",
228
+ "The validation shell could not be started",
229
+ { cause: error }
230
+ )
231
+ );
232
+ });
233
+ child.on("exit", (code, signal) => {
234
+ exitRecord = { code, signal };
235
+ if (!timedOut && !disposed) {
236
+ escalate();
237
+ }
238
+ scheduleTimer(settle, termGraceMs + SETTLE_FAILSAFE_SLACK_MS);
239
+ });
240
+ child.on("close", () => {
241
+ if (!groupIsAlive()) settle();
242
+ });
243
+ return {
244
+ result,
245
+ async dispose() {
246
+ if (!settled) {
247
+ disposed = true;
248
+ escalate();
249
+ scheduleTimer(settle, termGraceMs + SETTLE_FAILSAFE_SLACK_MS);
250
+ }
251
+ await result.catch(() => void 0);
252
+ }
253
+ };
254
+ }
255
+ export {
256
+ DEFAULT_VALIDATION_MAX_OUTPUT_CHARS,
257
+ DEFAULT_VALIDATION_TIMEOUT_MS,
258
+ MAX_VALIDATION_COMMAND_CHARS,
259
+ MAX_VALIDATION_TIMEOUT_MS,
260
+ ValidationRunnerError,
261
+ isValidationRunnerError,
262
+ startValidationRun
263
+ };
@@ -0,0 +1,233 @@
1
+ // packages/git/src/worktree.ts
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ GitServiceError,
6
+ GitWorktreeIncompleteError,
7
+ boundedDetailText
8
+ } from "./errors.js";
9
+ import { canonicalPathForms, requireAbsolutePath } from "./paths.js";
10
+ import {
11
+ COMMIT_SHA_PATTERN,
12
+ inspectRepository
13
+ } from "./repository.js";
14
+ import {
15
+ defaultGitRunner,
16
+ runGitExpectingSuccess
17
+ } from "./runner.js";
18
+ function containsControlCharacters(value) {
19
+ for (const character of value) {
20
+ const code = character.codePointAt(0) ?? 0;
21
+ if (code < 32 || code === 127) return true;
22
+ }
23
+ return false;
24
+ }
25
+ function validateBranchInput(branch) {
26
+ const reject = (reason) => {
27
+ throw new GitServiceError("GIT_INVALID_BRANCH_NAME", `Invalid branch name: ${reason}`, {
28
+ details: { branch: boundedDetailText(branch, 120) }
29
+ });
30
+ };
31
+ if (branch.length === 0) reject("name is empty");
32
+ if (branch.startsWith("-")) reject("name must not start with '-'");
33
+ if (containsControlCharacters(branch)) reject("name contains control characters");
34
+ }
35
+ function validateBaseRefInput(baseRef) {
36
+ const reject = (reason) => {
37
+ throw new GitServiceError("GIT_INVALID_BASE_REF", `Invalid base ref: ${reason}`, {
38
+ details: { baseRef: boundedDetailText(baseRef, 120) }
39
+ });
40
+ };
41
+ if (baseRef.length === 0) reject("ref is empty");
42
+ if (baseRef.startsWith("-")) reject("ref must not start with '-'");
43
+ if (containsControlCharacters(baseRef)) reject("ref contains control characters");
44
+ }
45
+ function validateWorktreeTargetPath(worktreePath) {
46
+ requireAbsolutePath(worktreePath, "worktree path");
47
+ if (path.resolve(worktreePath) !== worktreePath) {
48
+ throw new GitServiceError(
49
+ "GIT_PATH_NOT_NORMALIZED",
50
+ "Worktree path must be normalized (no '.', '..', repeated or trailing separators)",
51
+ { details: { path: worktreePath } }
52
+ );
53
+ }
54
+ return worktreePath;
55
+ }
56
+ function parseWorktreeListPaths(porcelainZ) {
57
+ return porcelainZ.split("\0").filter((token) => token.startsWith("worktree ")).map((token) => token.slice("worktree ".length));
58
+ }
59
+ async function isWorktreePathRegistered(runner, repositoryRoot, worktreePath) {
60
+ const list = await runGitExpectingSuccess(runner, ["worktree", "list", "--porcelain", "-z"], {
61
+ cwd: repositoryRoot
62
+ });
63
+ const targetForms = new Set(await canonicalPathForms(worktreePath));
64
+ for (const listed of parseWorktreeListPaths(list.stdout)) {
65
+ for (const form of await canonicalPathForms(listed)) {
66
+ if (targetForms.has(form)) return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ async function branchExists(runner, repositoryRoot, branch) {
72
+ const result = await runner(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
73
+ cwd: repositoryRoot
74
+ });
75
+ if (result.exitCode === 0) return true;
76
+ if (result.exitCode === 1) return false;
77
+ throw new GitServiceError("GIT_COMMAND_FAILED", "git show-ref exited unexpectedly", {
78
+ details: { exitCode: result.exitCode, stderr: boundedDetailText(result.stderr) }
79
+ });
80
+ }
81
+ async function preflightTaskWorktree(options) {
82
+ const runner = options.runner ?? defaultGitRunner;
83
+ validateBranchInput(options.branch);
84
+ validateBaseRefInput(options.baseRef);
85
+ const worktreePath = validateWorktreeTargetPath(options.worktreePath);
86
+ const repository = await inspectRepository(options.repositoryPath, { runner });
87
+ const root = repository.root;
88
+ const format = await runner(["check-ref-format", `refs/heads/${options.branch}`], { cwd: root });
89
+ if (format.exitCode !== 0) {
90
+ throw new GitServiceError("GIT_INVALID_BRANCH_NAME", "Branch name is not a valid Git ref name", {
91
+ details: { branch: boundedDetailText(options.branch, 120) }
92
+ });
93
+ }
94
+ const resolved = await runner(
95
+ ["rev-parse", "--verify", "--quiet", "--end-of-options", `${options.baseRef}^{commit}`],
96
+ { cwd: root }
97
+ );
98
+ if (resolved.exitCode !== 0) {
99
+ throw new GitServiceError(
100
+ "GIT_BASE_REF_NOT_FOUND",
101
+ "Base ref does not resolve to a commit in this repository",
102
+ { details: { baseRef: boundedDetailText(options.baseRef, 120) } }
103
+ );
104
+ }
105
+ const baseCommit = resolved.stdout.endsWith("\n") ? resolved.stdout.slice(0, -1) : resolved.stdout;
106
+ if (!COMMIT_SHA_PATTERN.test(baseCommit)) {
107
+ throw new GitServiceError("GIT_COMMAND_FAILED", "git rev-parse returned an unexpected commit value", {
108
+ details: { value: boundedDetailText(baseCommit, 80) }
109
+ });
110
+ }
111
+ if (await branchExists(runner, root, options.branch)) {
112
+ throw new GitServiceError("GIT_BRANCH_EXISTS", "A local branch with this name already exists", {
113
+ details: { branch: options.branch }
114
+ });
115
+ }
116
+ let targetOnDisk = false;
117
+ try {
118
+ await lstat(worktreePath);
119
+ targetOnDisk = true;
120
+ } catch (error) {
121
+ const errno = error;
122
+ if (errno.code !== "ENOENT" && errno.code !== "ENOTDIR") {
123
+ throw new GitServiceError("GIT_FILESYSTEM_ERROR", "Worktree target path could not be checked", {
124
+ cause: error,
125
+ details: { path: worktreePath }
126
+ });
127
+ }
128
+ }
129
+ if (targetOnDisk) {
130
+ throw new GitServiceError(
131
+ "GIT_WORKTREE_TARGET_EXISTS",
132
+ "Worktree target path already exists on disk",
133
+ { details: { path: worktreePath } }
134
+ );
135
+ }
136
+ if (await isWorktreePathRegistered(runner, root, worktreePath)) {
137
+ throw new GitServiceError(
138
+ "GIT_WORKTREE_TARGET_REGISTERED",
139
+ "Worktree target path is already registered in git worktree list",
140
+ { details: { path: worktreePath } }
141
+ );
142
+ }
143
+ return { repository, branch: options.branch, baseCommit, worktreePath };
144
+ }
145
+ async function collectRecoveryFacts(runner, plan) {
146
+ let branchNowExists = null;
147
+ try {
148
+ branchNowExists = await branchExists(runner, plan.repository.root, plan.branch);
149
+ } catch {
150
+ branchNowExists = null;
151
+ }
152
+ let targetRegisteredAsWorktree = null;
153
+ try {
154
+ targetRegisteredAsWorktree = await isWorktreePathRegistered(
155
+ runner,
156
+ plan.repository.root,
157
+ plan.worktreePath
158
+ );
159
+ } catch {
160
+ targetRegisteredAsWorktree = null;
161
+ }
162
+ let targetExistsOnDisk = null;
163
+ try {
164
+ await lstat(plan.worktreePath);
165
+ targetExistsOnDisk = true;
166
+ } catch (error) {
167
+ const errno = error;
168
+ targetExistsOnDisk = errno.code === "ENOENT" || errno.code === "ENOTDIR" ? false : null;
169
+ }
170
+ return {
171
+ branch: plan.branch,
172
+ worktreePath: plan.worktreePath,
173
+ baseCommit: plan.baseCommit,
174
+ branchExists: branchNowExists,
175
+ targetRegisteredAsWorktree,
176
+ targetExistsOnDisk
177
+ };
178
+ }
179
+ async function createTaskWorktree(options) {
180
+ const runner = options.runner ?? defaultGitRunner;
181
+ const plan = await preflightTaskWorktree({ ...options, runner });
182
+ let addOutcome = null;
183
+ let addFailure = null;
184
+ try {
185
+ addOutcome = await runner(
186
+ ["worktree", "add", "-b", plan.branch, plan.worktreePath, plan.baseCommit],
187
+ { cwd: plan.repository.root }
188
+ );
189
+ } catch (error) {
190
+ addFailure = error;
191
+ }
192
+ if (addOutcome === null || addOutcome.exitCode !== 0) {
193
+ const facts = await collectRecoveryFacts(runner, plan);
194
+ throw new GitWorktreeIncompleteError(
195
+ "git worktree add failed; nothing was cleaned up automatically",
196
+ facts,
197
+ { cause: addFailure ?? void 0 }
198
+ );
199
+ }
200
+ try {
201
+ const worktree = await inspectRepository(plan.worktreePath, { runner });
202
+ const canonicalTarget = await realpath(plan.worktreePath);
203
+ if (worktree.root !== canonicalTarget) {
204
+ throw new Error("created worktree root does not match the requested target path");
205
+ }
206
+ if (worktree.branch !== plan.branch) {
207
+ throw new Error("created worktree is not on the requested branch");
208
+ }
209
+ if (worktree.headCommit !== plan.baseCommit) {
210
+ throw new Error("created worktree HEAD does not match the requested base commit");
211
+ }
212
+ if (worktree.commonDir !== plan.repository.commonDir) {
213
+ throw new Error("created worktree does not belong to the requested repository");
214
+ }
215
+ return {
216
+ repository: plan.repository,
217
+ branch: plan.branch,
218
+ baseCommit: plan.baseCommit,
219
+ worktree
220
+ };
221
+ } catch (error) {
222
+ const facts = await collectRecoveryFacts(runner, plan);
223
+ throw new GitWorktreeIncompleteError(
224
+ "created worktree failed post-verification; nothing was cleaned up automatically",
225
+ facts,
226
+ { cause: error }
227
+ );
228
+ }
229
+ }
230
+ export {
231
+ createTaskWorktree,
232
+ preflightTaskWorktree
233
+ };
@@ -0,0 +1,117 @@
1
+ // packages/pi-bridge/extensions/pita-policy.ts
2
+ import {
3
+ PITA_POLICY_PROTOCOL,
4
+ PITA_POLICY_STATUS_KEY,
5
+ PITA_POLICY_VERSION,
6
+ encodePolicyStatus
7
+ } from "../src/handshake.js";
8
+ import {
9
+ MAX_POLICY_APPROVAL_SUMMARY_CHARS,
10
+ MAX_POLICY_APPROVAL_TIMEOUT_MS,
11
+ PITA_POLICY_APPROVAL_TITLE,
12
+ encodePolicyApproval,
13
+ sanitizeApprovalDisplayText
14
+ } from "../src/policy-approval.js";
15
+ var ROUTINE_READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set(["read", "grep", "find", "ls"]);
16
+ var DEFAULT_CONFIRM_TIMEOUT_MS = 12e4;
17
+ function resolveRuntimeMode(env) {
18
+ return env["PITA_RUNTIME_MODE"] === "trusted" ? "trusted" : "supervised";
19
+ }
20
+ function resolveConfirmTimeoutMs(env) {
21
+ const raw = env["PITA_POLICY_CONFIRM_TIMEOUT_MS"];
22
+ if (raw === void 0) return DEFAULT_CONFIRM_TIMEOUT_MS;
23
+ const parsed = Number(raw);
24
+ return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_POLICY_APPROVAL_TIMEOUT_MS ? parsed : DEFAULT_CONFIRM_TIMEOUT_MS;
25
+ }
26
+ function describeToolCall(toolName, input) {
27
+ if (toolName === "bash" && typeof input === "object" && input !== null) {
28
+ const command = input["command"];
29
+ if (typeof command === "string") {
30
+ return `Run bash command:
31
+ ${sanitizeApprovalDisplayText(command)}`;
32
+ }
33
+ }
34
+ if ((toolName === "write" || toolName === "edit") && typeof input === "object" && input !== null) {
35
+ const args = input;
36
+ const path = args["path"];
37
+ if (typeof path === "string") {
38
+ const heading = `${toolName === "write" ? "Write" : "Edit"} file:
39
+ ${sanitizeApprovalDisplayText(path)}`;
40
+ const lines = (value, prefix) => sanitizeApprovalDisplayText(value).split("\n").map((line) => `${prefix}${line}`).join("\n");
41
+ if (toolName === "write" && typeof args.content === "string") return `${heading}
42
+
43
+ Proposed file contents:
44
+ ${lines(args.content, "+ ")}`;
45
+ const replacements = Array.isArray(args.edits) ? args.edits : [args];
46
+ const previews = replacements.flatMap((edit) => {
47
+ if (!edit || typeof edit !== "object") return [];
48
+ const replacement = edit;
49
+ return typeof replacement.oldText === "string" && typeof replacement.newText === "string" ? [`${lines(replacement.oldText, "- ")}
50
+ ${lines(replacement.newText, "+ ")}`] : [];
51
+ });
52
+ return previews.length ? `${heading}
53
+
54
+ Proposed replacements (\u2212 before, + after):
55
+ ${previews.join("\n\n")}` : heading;
56
+ }
57
+ }
58
+ return `Run tool "${sanitizeApprovalDisplayText(toolName)}"`;
59
+ }
60
+ function pitaPolicyExtension(pi) {
61
+ const mode = resolveRuntimeMode(process.env);
62
+ const confirmTimeoutMs = resolveConfirmTimeoutMs(process.env);
63
+ pi.on("session_start", async (_event, ctx) => {
64
+ ctx.ui.setStatus(
65
+ PITA_POLICY_STATUS_KEY,
66
+ encodePolicyStatus({
67
+ protocol: PITA_POLICY_PROTOCOL,
68
+ version: PITA_POLICY_VERSION,
69
+ mode
70
+ })
71
+ );
72
+ });
73
+ pi.on("tool_call", async (event, ctx) => {
74
+ if (mode === "trusted") return void 0;
75
+ if (ROUTINE_READ_ONLY_TOOL_NAMES.has(event.toolName)) return void 0;
76
+ try {
77
+ const summary = describeToolCall(event.toolName, event.input);
78
+ const displayToolName = sanitizeApprovalDisplayText(event.toolName);
79
+ if (summary.length > MAX_POLICY_APPROVAL_SUMMARY_CHARS || displayToolName.length > 200) {
80
+ const reason = `Pita blocked ${displayToolName}: the action description is too large for a safe approval. Split it into smaller tool calls.`;
81
+ ctx.ui.notify(reason, "warning");
82
+ return { block: true, reason };
83
+ }
84
+ const requestedAtMs = Date.now();
85
+ const confirmed = await ctx.ui.confirm(
86
+ PITA_POLICY_APPROVAL_TITLE,
87
+ encodePolicyApproval({
88
+ toolCallId: event.toolCallId,
89
+ toolName: displayToolName,
90
+ summary,
91
+ requestedAtMs,
92
+ timeoutMs: confirmTimeoutMs
93
+ }),
94
+ { timeout: confirmTimeoutMs }
95
+ );
96
+ if (confirmed !== true) {
97
+ return {
98
+ block: true,
99
+ reason: `Pita blocked ${event.toolName}: approval was denied, cancelled, or timed out`
100
+ };
101
+ }
102
+ return void 0;
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ return {
106
+ block: true,
107
+ reason: `Pita blocked ${event.toolName}: approval UI failed (${message})`
108
+ };
109
+ }
110
+ });
111
+ }
112
+ export {
113
+ pitaPolicyExtension as default,
114
+ describeToolCall,
115
+ resolveConfirmTimeoutMs,
116
+ resolveRuntimeMode
117
+ };
@@ -0,0 +1,80 @@
1
+ // packages/pi-bridge/src/auth.ts
2
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
3
+ function createPiAuthManager(options = {}) {
4
+ let runtimePromise = null;
5
+ function runtime() {
6
+ if (runtimePromise === null) {
7
+ const created = options.createModelRuntime?.() ?? ModelRuntime.create({ refreshOnCreate: false });
8
+ runtimePromise = created;
9
+ void created.catch(() => {
10
+ if (runtimePromise === created) runtimePromise = null;
11
+ });
12
+ }
13
+ return runtimePromise;
14
+ }
15
+ return {
16
+ async listProviders() {
17
+ const modelRuntime = await runtime();
18
+ const providers = await Promise.all(
19
+ modelRuntime.getProviders().map(async (provider) => {
20
+ const methods = [];
21
+ if (typeof provider.auth.apiKey?.login === "function") {
22
+ methods.push({ type: "api_key", label: provider.auth.apiKey.name });
23
+ }
24
+ if (provider.auth.oauth !== void 0) {
25
+ methods.push({
26
+ type: "oauth",
27
+ label: provider.auth.oauth.loginLabel ?? provider.auth.oauth.name
28
+ });
29
+ }
30
+ try {
31
+ const auth = await modelRuntime.checkAuth(provider.id);
32
+ if (auth === void 0 && methods.length === 0) return null;
33
+ const models = auth === void 0 ? [] : (await modelRuntime.getAvailable(provider.id)).map((model) => ({
34
+ id: model.id,
35
+ provider: model.provider,
36
+ name: model.name,
37
+ reasoning: model.reasoning
38
+ }));
39
+ return {
40
+ id: provider.id,
41
+ name: provider.name,
42
+ authenticated: auth !== void 0,
43
+ ...auth?.source === void 0 ? {} : { source: auth.source },
44
+ ...auth === void 0 ? {} : { authType: auth.type },
45
+ methods,
46
+ models
47
+ };
48
+ } catch {
49
+ return methods.length === 0 ? null : {
50
+ id: provider.id,
51
+ name: provider.name,
52
+ authenticated: false,
53
+ methods,
54
+ models: []
55
+ };
56
+ }
57
+ })
58
+ );
59
+ return providers.filter((provider) => provider !== null).sort((left, right) => left.name.localeCompare(right.name));
60
+ },
61
+ async hasAuth(providerId) {
62
+ return await (await runtime()).checkAuth(providerId) !== void 0;
63
+ },
64
+ async login(providerId, method, interaction) {
65
+ const modelRuntime = await runtime();
66
+ const provider = modelRuntime.getProviders().find((candidate) => candidate.id === providerId);
67
+ if (provider === void 0) {
68
+ throw new Error(`Pi provider ${providerId} is not installed.`);
69
+ }
70
+ const supported = method === "api_key" ? typeof provider.auth.apiKey?.login === "function" : provider.auth.oauth !== void 0;
71
+ if (!supported) {
72
+ throw new Error(`Pi provider ${providerId} does not support ${method} login.`);
73
+ }
74
+ await modelRuntime.login(providerId, method, interaction);
75
+ }
76
+ };
77
+ }
78
+ export {
79
+ createPiAuthManager
80
+ };