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,352 @@
1
+ // apps/daemon/src/task-validation.ts
2
+ import { realpath } from "node:fs/promises";
3
+ import {
4
+ captureWorktreeState,
5
+ isGitServiceError,
6
+ isValidationRunnerError,
7
+ startValidationRun
8
+ } from "../../../packages/git/src/index.js";
9
+ import { sanitizeApprovalDisplayText } from "../../../packages/pi-bridge/src/index.js";
10
+ import {
11
+ PROTOCOL_VERSION,
12
+ TASK_VALIDATION_MAX_OUTPUT_CHARS,
13
+ taskValidationSchema
14
+ } from "../../../packages/protocol/src/index.js";
15
+ var TaskValidationOperationError = class extends Error {
16
+ name = "TaskValidationOperationError";
17
+ code;
18
+ status;
19
+ constructor(code, message, status, options) {
20
+ super(message, options);
21
+ this.code = code;
22
+ this.status = status;
23
+ }
24
+ };
25
+ var WORKTREE_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
26
+ "GIT_PATH_NOT_FOUND",
27
+ "GIT_PATH_NOT_DIRECTORY",
28
+ "GIT_NOT_A_REPOSITORY",
29
+ "GIT_BARE_REPOSITORY",
30
+ "GIT_NOT_A_WORK_TREE",
31
+ "GIT_HEAD_UNRESOLVED"
32
+ ]);
33
+ var WORKTREE_UNAVAILABLE_MESSAGE = "The task worktree is missing or is no longer a healthy Git worktree. Recover it before running validation.";
34
+ var WORKTREE_MISMATCH_MESSAGE = "The recorded worktree no longer matches the task's repository. Recover it before running validation.";
35
+ function createTaskValidationService(options) {
36
+ const capture = options.captureWorktreeState ?? captureWorktreeState;
37
+ const startRun = options.startValidationRun ?? startValidationRun;
38
+ const now = options.now ?? Date.now;
39
+ const activeRuns = /* @__PURE__ */ new Map();
40
+ let disposing = false;
41
+ async function resolveTask(taskId) {
42
+ let task;
43
+ try {
44
+ task = await options.getTask(taskId);
45
+ } catch (error) {
46
+ throw new TaskValidationOperationError(
47
+ "validation-failed",
48
+ "The task could not be resolved from the local workspace.",
49
+ 503,
50
+ { cause: error }
51
+ );
52
+ }
53
+ if (task === null) {
54
+ throw new TaskValidationOperationError(
55
+ "task-not-found",
56
+ "The selected task no longer exists.",
57
+ 404
58
+ );
59
+ }
60
+ if (task.status !== "ready") {
61
+ throw new TaskValidationOperationError(
62
+ "task-not-ready",
63
+ `Only a ready task can be validated; this task is ${task.status}.`,
64
+ 409
65
+ );
66
+ }
67
+ return task;
68
+ }
69
+ async function resolveRepository(task) {
70
+ let repository;
71
+ try {
72
+ repository = await options.getRepository(task.repositoryId);
73
+ } catch (error) {
74
+ throw new TaskValidationOperationError(
75
+ "validation-failed",
76
+ "The task's repository could not be resolved from the local workspace.",
77
+ 503,
78
+ { cause: error }
79
+ );
80
+ }
81
+ if (repository === null) {
82
+ throw new TaskValidationOperationError(
83
+ "task-not-ready",
84
+ "The task's repository is no longer registered.",
85
+ 409
86
+ );
87
+ }
88
+ return repository;
89
+ }
90
+ async function captureVerifiedState(task, repository) {
91
+ let canonicalWorktree;
92
+ try {
93
+ canonicalWorktree = await realpath(task.worktreePath);
94
+ } catch (error) {
95
+ throw new TaskValidationOperationError("task-not-ready", WORKTREE_UNAVAILABLE_MESSAGE, 409, {
96
+ cause: error
97
+ });
98
+ }
99
+ let state;
100
+ try {
101
+ state = await capture({ worktreePath: canonicalWorktree });
102
+ } catch (error) {
103
+ if (isGitServiceError(error) && WORKTREE_UNAVAILABLE_CODES.has(error.code)) {
104
+ throw new TaskValidationOperationError(
105
+ "task-not-ready",
106
+ WORKTREE_UNAVAILABLE_MESSAGE,
107
+ 409,
108
+ { cause: error }
109
+ );
110
+ }
111
+ if (isGitServiceError(error, "GIT_STATE_TOO_LARGE") || isGitServiceError(error, "GIT_STATE_UNFINGERPRINTABLE")) {
112
+ throw new TaskValidationOperationError(
113
+ "validation-failed",
114
+ "The worktree has changed content that is too large or cannot be fingerprinted safely yet.",
115
+ 422,
116
+ { cause: error }
117
+ );
118
+ }
119
+ if (isGitServiceError(error, "GIT_REPOSITORY_CHANGED_DURING_CAPTURE") || isGitServiceError(error, "GIT_STATE_CHANGED_DURING_CAPTURE") || isGitServiceError(error, "GIT_STATE_DEADLINE_EXCEEDED")) {
120
+ throw new TaskValidationOperationError(
121
+ "validation-failed",
122
+ "The worktree state changed or could not be captured in time. Try again.",
123
+ 409,
124
+ { cause: error }
125
+ );
126
+ }
127
+ throw new TaskValidationOperationError(
128
+ "validation-failed",
129
+ "Git could not capture the worktree state for this task. Check the daemon output for details.",
130
+ 502,
131
+ { cause: error }
132
+ );
133
+ }
134
+ if (state.worktree.root !== canonicalWorktree || state.worktree.commonDir !== repository.commonDir || state.worktree.branch !== task.branch) {
135
+ throw new TaskValidationOperationError("task-not-ready", WORKTREE_MISMATCH_MESSAGE, 409);
136
+ }
137
+ return { canonicalWorktree, state };
138
+ }
139
+ function boundedOutput(raw, alreadyOmitted) {
140
+ const safe = sanitizeApprovalDisplayText(raw);
141
+ if (safe.length <= TASK_VALIDATION_MAX_OUTPUT_CHARS) {
142
+ return { text: safe, omitted: alreadyOmitted };
143
+ }
144
+ return {
145
+ text: safe.slice(0, TASK_VALIDATION_MAX_OUTPUT_CHARS),
146
+ omitted: alreadyOmitted + (safe.length - TASK_VALIDATION_MAX_OUTPUT_CHARS)
147
+ };
148
+ }
149
+ function payload(taskId, result, running, stale) {
150
+ return taskValidationSchema.parse({
151
+ protocol: PROTOCOL_VERSION,
152
+ type: "task-validation",
153
+ taskId,
154
+ result,
155
+ running,
156
+ stale,
157
+ capturedAtMs: now()
158
+ });
159
+ }
160
+ async function status(taskId) {
161
+ const task = await resolveTask(taskId);
162
+ const repository = await resolveRepository(task);
163
+ const record = task.status === "ready" ? task.validation ?? null : null;
164
+ const running = activeRuns.has(taskId);
165
+ let stale = false;
166
+ if (record !== null) {
167
+ stale = true;
168
+ try {
169
+ const { state } = await captureVerifiedState(task, repository);
170
+ stale = state.stateFingerprint !== record.stateFingerprint;
171
+ } catch {
172
+ stale = true;
173
+ }
174
+ }
175
+ return payload(task.id, record, running, stale);
176
+ }
177
+ async function run(taskId, command) {
178
+ if (disposing) {
179
+ throw new TaskValidationOperationError(
180
+ "validation-failed",
181
+ "The daemon is shutting down and cannot start a validation run.",
182
+ 503
183
+ );
184
+ }
185
+ if (activeRuns.has(taskId)) {
186
+ throw new TaskValidationOperationError(
187
+ "validation-running",
188
+ "A validation is already running for this task. Wait for it to finish.",
189
+ 409
190
+ );
191
+ }
192
+ const active = { run: null };
193
+ activeRuns.set(taskId, active);
194
+ try {
195
+ let errorResult2 = function(message) {
196
+ return {
197
+ command,
198
+ outcome: "error",
199
+ exitCode: null,
200
+ signal: null,
201
+ output: message,
202
+ outputTruncated: false,
203
+ omittedOutputChars: 0,
204
+ startedAtMs: attemptStartedAtMs,
205
+ finishedAtMs: Math.max(now(), attemptStartedAtMs),
206
+ stateFingerprint: preState.stateFingerprint
207
+ };
208
+ };
209
+ var errorResult = errorResult2;
210
+ if (options.isArchiveActive?.(taskId) === true) {
211
+ throw new TaskValidationOperationError(
212
+ "archive-in-progress",
213
+ "This task is being archived or restored. Wait for that operation to finish.",
214
+ 409
215
+ );
216
+ }
217
+ if (options.isCommitActive?.(taskId) === true) {
218
+ throw new TaskValidationOperationError(
219
+ "commit-in-progress",
220
+ "A commit preflight or execution is active for this task. Wait for it to finish before validating.",
221
+ 409
222
+ );
223
+ }
224
+ if (options.isMergeActive?.(taskId) === true) {
225
+ throw new TaskValidationOperationError(
226
+ "merge-in-progress",
227
+ "A merge preflight or execution is active for this task. Wait for it to finish before validating.",
228
+ 409
229
+ );
230
+ }
231
+ if (options.isAgentWorking !== void 0) {
232
+ let working;
233
+ try {
234
+ working = await options.isAgentWorking(taskId);
235
+ } catch (error) {
236
+ throw new TaskValidationOperationError(
237
+ "validation-failed",
238
+ "The task's runtime state could not be verified. Check the daemon output for details.",
239
+ 503,
240
+ { cause: error }
241
+ );
242
+ }
243
+ if (working) {
244
+ throw new TaskValidationOperationError(
245
+ "agent-working",
246
+ "The task's agent is still working. Wait for the turn to settle before validating.",
247
+ 409
248
+ );
249
+ }
250
+ }
251
+ const task = await resolveTask(taskId);
252
+ const repository = await resolveRepository(task);
253
+ const { canonicalWorktree, state: preState } = await captureVerifiedState(task, repository);
254
+ const attemptStartedAtMs = now();
255
+ async function recordCompletedResult(result2) {
256
+ try {
257
+ await options.recordValidation(taskId, result2);
258
+ } catch (error) {
259
+ throw new TaskValidationOperationError(
260
+ "validation-failed",
261
+ "The validation finished but its result could not be recorded. Check the daemon output for details.",
262
+ 503,
263
+ { cause: error }
264
+ );
265
+ }
266
+ let stale = true;
267
+ try {
268
+ const post = await capture({ worktreePath: canonicalWorktree });
269
+ stale = post.stateFingerprint !== preState.stateFingerprint;
270
+ } catch {
271
+ stale = true;
272
+ }
273
+ return payload(task.id, result2, false, stale);
274
+ }
275
+ if (disposing) {
276
+ throw new TaskValidationOperationError(
277
+ "validation-failed",
278
+ "The daemon is shutting down and cannot start a validation run.",
279
+ 503
280
+ );
281
+ }
282
+ let validationRun;
283
+ try {
284
+ validationRun = startRun({
285
+ worktreePath: canonicalWorktree,
286
+ command,
287
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
288
+ now
289
+ });
290
+ } catch (error) {
291
+ if (isValidationRunnerError(error, "VALIDATION_COMMAND_INVALID")) {
292
+ throw new TaskValidationOperationError(
293
+ "invalid-request",
294
+ "Provide a non-empty validation command within Webdesk's length bound and without unsafe invisible characters.",
295
+ 400,
296
+ { cause: error }
297
+ );
298
+ }
299
+ return recordCompletedResult(errorResult2("The validation process could not be started."));
300
+ }
301
+ active.run = validationRun;
302
+ let raw;
303
+ try {
304
+ raw = await validationRun.result;
305
+ } catch (error) {
306
+ if (isValidationRunnerError(error, "VALIDATION_DISPOSED")) {
307
+ throw new TaskValidationOperationError(
308
+ "validation-failed",
309
+ "The validation run was stopped because the daemon is shutting down.",
310
+ 503,
311
+ { cause: error }
312
+ );
313
+ }
314
+ return recordCompletedResult(
315
+ errorResult2("The validation process failed before producing a result.")
316
+ );
317
+ }
318
+ const display = boundedOutput(raw.output, raw.omittedOutputChars);
319
+ const result = {
320
+ command,
321
+ outcome: raw.outcome,
322
+ exitCode: raw.exitCode,
323
+ signal: raw.signal,
324
+ output: display.text,
325
+ outputTruncated: display.omitted > 0,
326
+ omittedOutputChars: display.omitted,
327
+ startedAtMs: raw.startedAtMs,
328
+ finishedAtMs: raw.finishedAtMs,
329
+ stateFingerprint: preState.stateFingerprint
330
+ };
331
+ return recordCompletedResult(result);
332
+ } finally {
333
+ activeRuns.delete(taskId);
334
+ }
335
+ }
336
+ async function disposeAll() {
337
+ disposing = true;
338
+ await Promise.all(
339
+ [...activeRuns.values()].map((active) => active.run?.dispose() ?? Promise.resolve())
340
+ );
341
+ }
342
+ return {
343
+ status,
344
+ run,
345
+ isRunning: (taskId) => activeRuns.has(taskId),
346
+ disposeAll
347
+ };
348
+ }
349
+ export {
350
+ TaskValidationOperationError,
351
+ createTaskValidationService
352
+ };
@@ -0,0 +1,140 @@
1
+ // apps/daemon/src/workspace-store.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, open, readFile, readdir, rename, unlink } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import {
6
+ WORKSPACE_SCHEMA_VERSION,
7
+ workspaceStateV1Schema,
8
+ workspaceStateV2Schema,
9
+ workspaceStateV3Schema,
10
+ workspaceStateV4Schema,
11
+ workspaceStateSchema
12
+ } from "../../../packages/protocol/src/index.js";
13
+ var WORKSPACE_STATE_FILE = "workspace-v1.json";
14
+ var STALE_TEMP_FILE = /^\.workspace-v1\.json\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/i;
15
+ var WorkspaceStateError = class extends Error {
16
+ name = "WorkspaceStateError";
17
+ };
18
+ function emptyState() {
19
+ return {
20
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
21
+ repositories: [],
22
+ tasks: []
23
+ };
24
+ }
25
+ function errorCode(error) {
26
+ return error !== null && typeof error === "object" && "code" in error ? String(error.code) : void 0;
27
+ }
28
+ function createWorkspaceStore(options) {
29
+ const stateDir = path.resolve(options.stateDir);
30
+ const filePath = path.join(stateDir, WORKSPACE_STATE_FILE);
31
+ let state = null;
32
+ let initialization = null;
33
+ let mutationQueue = Promise.resolve();
34
+ async function persist(next) {
35
+ await mkdir(stateDir, { recursive: true, mode: 448 });
36
+ const tempPath = path.join(
37
+ stateDir,
38
+ `.${WORKSPACE_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`
39
+ );
40
+ let handle = null;
41
+ try {
42
+ handle = await open(tempPath, "wx", 384);
43
+ await handle.writeFile(`${JSON.stringify(next, null, 2)}
44
+ `, "utf8");
45
+ await handle.sync();
46
+ await handle.close();
47
+ handle = null;
48
+ await rename(tempPath, filePath);
49
+ let directory = null;
50
+ try {
51
+ directory = await open(stateDir, "r");
52
+ await directory.sync();
53
+ } catch {
54
+ } finally {
55
+ await directory?.close().catch(() => void 0);
56
+ }
57
+ } catch (error) {
58
+ await handle?.close().catch(() => void 0);
59
+ await unlink(tempPath).catch(() => void 0);
60
+ throw new WorkspaceStateError(
61
+ `Could not atomically write Webdesk workspace metadata at ${filePath}: ${errorCode(error) ?? "write failed"}`
62
+ );
63
+ }
64
+ }
65
+ async function load() {
66
+ await mkdir(stateDir, { recursive: true, mode: 448 });
67
+ for (const name of await readdir(stateDir)) {
68
+ if (STALE_TEMP_FILE.test(name)) {
69
+ await unlink(path.join(stateDir, name)).catch(() => void 0);
70
+ }
71
+ }
72
+ let raw;
73
+ try {
74
+ raw = await readFile(filePath, "utf8");
75
+ } catch (error) {
76
+ if (errorCode(error) === "ENOENT") {
77
+ const initial = emptyState();
78
+ await persist(initial);
79
+ state = initial;
80
+ return;
81
+ }
82
+ throw new WorkspaceStateError(
83
+ `Could not read Webdesk workspace metadata at ${filePath}: ${errorCode(error) ?? "read failed"}`
84
+ );
85
+ }
86
+ try {
87
+ const decoded = JSON.parse(raw);
88
+ const current = workspaceStateSchema.safeParse(decoded);
89
+ if (current.success) {
90
+ state = current.data;
91
+ return;
92
+ }
93
+ const v4 = workspaceStateV4Schema.safeParse(decoded);
94
+ const v3 = v4.success ? v4 : workspaceStateV3Schema.safeParse(decoded);
95
+ const v2 = v3.success ? v3 : workspaceStateV2Schema.safeParse(decoded);
96
+ const legacy = v2.success ? v2 : workspaceStateV1Schema.safeParse(decoded);
97
+ if (!legacy.success) throw new Error("unsupported workspace schema");
98
+ const migrated = workspaceStateSchema.parse({
99
+ ...legacy.data,
100
+ schemaVersion: WORKSPACE_SCHEMA_VERSION
101
+ });
102
+ await persist(migrated);
103
+ state = migrated;
104
+ } catch (error) {
105
+ if (error instanceof WorkspaceStateError) throw error;
106
+ throw new WorkspaceStateError(
107
+ `Webdesk workspace metadata at ${filePath} is invalid. The file was preserved for recovery.`
108
+ );
109
+ }
110
+ }
111
+ function initialize() {
112
+ initialization ??= load();
113
+ return initialization;
114
+ }
115
+ async function read() {
116
+ await initialize();
117
+ return workspaceStateSchema.parse(state);
118
+ }
119
+ async function update(mutate) {
120
+ const operation = mutationQueue.then(async () => {
121
+ await initialize();
122
+ const current = workspaceStateSchema.parse(state);
123
+ const next = workspaceStateSchema.parse(await mutate(current));
124
+ await persist(next);
125
+ state = next;
126
+ return workspaceStateSchema.parse(next);
127
+ });
128
+ mutationQueue = operation.then(
129
+ () => void 0,
130
+ () => void 0
131
+ );
132
+ return operation;
133
+ }
134
+ return { filePath, initialize, read, update };
135
+ }
136
+ export {
137
+ WORKSPACE_STATE_FILE,
138
+ WorkspaceStateError,
139
+ createWorkspaceStore
140
+ };