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,503 @@
1
+ // apps/daemon/src/task-commit.ts
2
+ import { randomUUID } from "node:crypto";
3
+ import { realpath } from "node:fs/promises";
4
+ import {
5
+ captureWorktreeState,
6
+ commitPreparedTree,
7
+ isGitServiceError,
8
+ prepareCommitTree
9
+ } from "../../../packages/git/src/index.js";
10
+ import { sanitizeApprovalDisplayText } from "../../../packages/pi-bridge/src/index.js";
11
+ import {
12
+ PROTOCOL_VERSION,
13
+ TASK_COMMIT_MAX_FILE_DIFF_CHARS,
14
+ TASK_COMMIT_MAX_TOTAL_DIFF_CHARS,
15
+ taskCommitPreflightSchema,
16
+ taskCommitResultSchema
17
+ } from "../../../packages/protocol/src/index.js";
18
+ var COMMIT_PREFLIGHT_TTL_MS = 10 * 60 * 1e3;
19
+ var TaskCommitOperationError = class extends Error {
20
+ name = "TaskCommitOperationError";
21
+ code;
22
+ status;
23
+ constructor(code, message, status, options) {
24
+ super(message, options);
25
+ this.code = code;
26
+ this.status = status;
27
+ }
28
+ };
29
+ var WORKTREE_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
30
+ "GIT_PATH_NOT_FOUND",
31
+ "GIT_PATH_NOT_DIRECTORY",
32
+ "GIT_NOT_A_REPOSITORY",
33
+ "GIT_BARE_REPOSITORY",
34
+ "GIT_NOT_A_WORK_TREE",
35
+ "GIT_HEAD_UNRESOLVED"
36
+ ]);
37
+ var WORKTREE_UNAVAILABLE_MESSAGE = "The task worktree is missing or is no longer a healthy Git worktree. Recover it before committing.";
38
+ var WORKTREE_MISMATCH_MESSAGE = "The recorded worktree no longer matches the task's repository. Recover it before committing.";
39
+ var MAX_PATH_DISPLAY_CHARS = 4096;
40
+ function boundedDisplay(text, maxChars) {
41
+ const clippedInput = text.length > maxChars;
42
+ const safe = sanitizeApprovalDisplayText(text.slice(0, maxChars));
43
+ if (safe.length <= maxChars) return { text: safe, clipped: clippedInput };
44
+ return { text: safe.slice(0, maxChars), clipped: true };
45
+ }
46
+ function boundedPathDisplay(text) {
47
+ const bounded = boundedDisplay(text, MAX_PATH_DISPLAY_CHARS);
48
+ if (!bounded.clipped) return bounded.text;
49
+ return `${bounded.text.slice(0, MAX_PATH_DISPLAY_CHARS - 1)}\u2026`;
50
+ }
51
+ function createTaskCommitService(options) {
52
+ const capture = options.captureWorktreeState ?? captureWorktreeState;
53
+ const prepare = options.prepareCommitTree ?? prepareCommitTree;
54
+ const publish = options.commitPreparedTree ?? commitPreparedTree;
55
+ const preflightTtlMs = options.preflightTtlMs ?? COMMIT_PREFLIGHT_TTL_MS;
56
+ const now = options.now ?? Date.now;
57
+ const createId = options.createId ?? randomUUID;
58
+ const activePhases = /* @__PURE__ */ new Map();
59
+ const pendingPreflights = /* @__PURE__ */ new Map();
60
+ async function resolveTask(taskId) {
61
+ let task;
62
+ try {
63
+ task = await options.getTask(taskId);
64
+ } catch (error) {
65
+ throw new TaskCommitOperationError(
66
+ "commit-failed",
67
+ "The task could not be resolved from the local workspace.",
68
+ 503,
69
+ { cause: error }
70
+ );
71
+ }
72
+ if (task === null) {
73
+ throw new TaskCommitOperationError(
74
+ "task-not-found",
75
+ "The selected task no longer exists.",
76
+ 404
77
+ );
78
+ }
79
+ if (task.status !== "ready") {
80
+ throw new TaskCommitOperationError(
81
+ "task-not-ready",
82
+ `Only a ready task can be committed; this task is ${task.status}.`,
83
+ 409
84
+ );
85
+ }
86
+ return task;
87
+ }
88
+ async function resolveRepository(task) {
89
+ let repository;
90
+ try {
91
+ repository = await options.getRepository(task.repositoryId);
92
+ } catch (error) {
93
+ throw new TaskCommitOperationError(
94
+ "commit-failed",
95
+ "The task's repository could not be resolved from the local workspace.",
96
+ 503,
97
+ { cause: error }
98
+ );
99
+ }
100
+ if (repository === null) {
101
+ throw new TaskCommitOperationError(
102
+ "task-not-ready",
103
+ "The task's repository is no longer registered.",
104
+ 409
105
+ );
106
+ }
107
+ return repository;
108
+ }
109
+ function claimPhase(taskId, phase) {
110
+ if (activePhases.has(taskId)) {
111
+ throw new TaskCommitOperationError(
112
+ "commit-in-progress",
113
+ "A commit preflight or execution is already in progress for this task.",
114
+ 409
115
+ );
116
+ }
117
+ activePhases.set(taskId, phase);
118
+ }
119
+ function refuseRunningValidation(taskId) {
120
+ if (options.isValidationRunning(taskId)) {
121
+ throw new TaskCommitOperationError(
122
+ "validation-running",
123
+ "A validation is running for this task. Wait for it to finish before committing.",
124
+ 409
125
+ );
126
+ }
127
+ }
128
+ function refuseActiveMerge(taskId) {
129
+ if (options.isMergeActive?.(taskId) === true) {
130
+ throw new TaskCommitOperationError(
131
+ "merge-in-progress",
132
+ "A merge preflight or execution is active for this task. Wait for it to finish before committing.",
133
+ 409
134
+ );
135
+ }
136
+ }
137
+ async function refuseWorkingAgent(taskId) {
138
+ let working;
139
+ try {
140
+ working = await options.isAgentWorking(taskId);
141
+ } catch (error) {
142
+ throw new TaskCommitOperationError(
143
+ "commit-failed",
144
+ "The task's runtime state could not be verified. Check the daemon output for details.",
145
+ 503,
146
+ { cause: error }
147
+ );
148
+ }
149
+ if (working) {
150
+ throw new TaskCommitOperationError(
151
+ "agent-working",
152
+ "The task's agent is still working. Wait for the turn to settle before committing.",
153
+ 409
154
+ );
155
+ }
156
+ }
157
+ async function captureVerifiedState(task, repository, phase) {
158
+ let canonicalWorktree;
159
+ try {
160
+ canonicalWorktree = await realpath(task.worktreePath);
161
+ } catch (error) {
162
+ throw new TaskCommitOperationError("task-not-ready", WORKTREE_UNAVAILABLE_MESSAGE, 409, {
163
+ cause: error
164
+ });
165
+ }
166
+ let state;
167
+ try {
168
+ state = await capture({ worktreePath: canonicalWorktree });
169
+ } catch (error) {
170
+ throw mapGitError(error, phase);
171
+ }
172
+ if (state.worktree.root !== canonicalWorktree || state.worktree.commonDir !== repository.commonDir || state.worktree.branch !== task.branch) {
173
+ throw new TaskCommitOperationError("task-not-ready", WORKTREE_MISMATCH_MESSAGE, 409);
174
+ }
175
+ return { canonicalWorktree, state };
176
+ }
177
+ function mapGitError(error, phase) {
178
+ if (isGitServiceError(error)) {
179
+ if (WORKTREE_UNAVAILABLE_CODES.has(error.code)) {
180
+ return new TaskCommitOperationError("task-not-ready", WORKTREE_UNAVAILABLE_MESSAGE, 409, {
181
+ cause: error
182
+ });
183
+ }
184
+ switch (error.code) {
185
+ case "GIT_COMMIT_CONFLICTED":
186
+ return new TaskCommitOperationError(
187
+ "commit-conflicted",
188
+ "The worktree has unresolved merge conflicts. Resolve them before committing.",
189
+ 409,
190
+ { cause: error }
191
+ );
192
+ case "GIT_COMMIT_NOTHING_TO_COMMIT":
193
+ return new TaskCommitOperationError(
194
+ "nothing-to-commit",
195
+ "There are no changes to commit for this task.",
196
+ 409,
197
+ { cause: error }
198
+ );
199
+ case "GIT_COMMIT_IDENTITY_MISSING":
200
+ return new TaskCommitOperationError(
201
+ "identity-missing",
202
+ "Git identity is not configured for this worktree. Set user.name and user.email, then try again.",
203
+ 409,
204
+ { cause: error }
205
+ );
206
+ case "GIT_COMMIT_DETACHED_HEAD":
207
+ case "GIT_COMMIT_BRANCH_MISMATCH":
208
+ return new TaskCommitOperationError("task-not-ready", WORKTREE_MISMATCH_MESSAGE, 409, {
209
+ cause: error
210
+ });
211
+ case "GIT_COMMIT_STATE_UNSUPPORTED":
212
+ case "GIT_STATE_UNFINGERPRINTABLE":
213
+ case "GIT_STATE_TOO_LARGE":
214
+ return new TaskCommitOperationError(
215
+ "state-unsupported",
216
+ "The worktree has changes that are too large or cannot be committed safely by Webdesk yet.",
217
+ 422,
218
+ { cause: error }
219
+ );
220
+ case "GIT_COMMIT_PARENT_MOVED":
221
+ case "GIT_COMMIT_REF_CAS_FAILED":
222
+ return new TaskCommitOperationError(
223
+ "branch-moved",
224
+ "The branch moved since the approved preflight. Review the current state and preflight again.",
225
+ 409,
226
+ { cause: error }
227
+ );
228
+ case "GIT_COMMIT_PUBLICATION_UNKNOWN": {
229
+ const commit = typeof error.details.commit === "string" ? error.details.commit.slice(0, 12) : "unknown";
230
+ return new TaskCommitOperationError(
231
+ "commit-failed",
232
+ `Git may have published commit ${commit}, but Webdesk could not verify the branch. Inspect the repository before retrying.`,
233
+ 502,
234
+ { cause: error }
235
+ );
236
+ }
237
+ case "GIT_COMMIT_SIGNING_FAILED":
238
+ return new TaskCommitOperationError(
239
+ "signing-failed",
240
+ "Commit signing failed or timed out; no commit was published. Check your signing setup.",
241
+ 502,
242
+ { cause: error }
243
+ );
244
+ case "GIT_REPOSITORY_CHANGED_DURING_CAPTURE":
245
+ case "GIT_STATE_CHANGED_DURING_CAPTURE":
246
+ case "GIT_STATE_DEADLINE_EXCEEDED":
247
+ return new TaskCommitOperationError(
248
+ "commit-failed",
249
+ phase === "preflight" ? "The worktree state changed or could not be captured in time. Try again." : "The worktree state changed or could not be re-verified in time. Preflight again.",
250
+ 409,
251
+ { cause: error }
252
+ );
253
+ default:
254
+ break;
255
+ }
256
+ }
257
+ return new TaskCommitOperationError(
258
+ "commit-failed",
259
+ "Git could not complete the commit operation. Check the daemon output for details.",
260
+ 502,
261
+ { cause: error }
262
+ );
263
+ }
264
+ function validationVerdict(task, stateFingerprint) {
265
+ const result = task.validation;
266
+ if (result === void 0) return "none";
267
+ if (result.stateFingerprint !== stateFingerprint) return "stale";
268
+ return result.outcome === "passed" ? "current-passed" : "current-failed";
269
+ }
270
+ function preflightPayload(taskId, pending, prepared, verdict, createdAtMs) {
271
+ let remainingChars = TASK_COMMIT_MAX_TOTAL_DIFF_CHARS;
272
+ let totalDiffTruncated = prepared.totalDiffTruncated;
273
+ const files = prepared.files.map((file) => {
274
+ let diff = null;
275
+ let diffTruncated = file.diffTruncated;
276
+ let diffOmittedReason = file.diffOmittedReason;
277
+ if (file.diff !== null) {
278
+ if (remainingChars <= 0) {
279
+ diffOmittedReason = "total-budget";
280
+ diffTruncated = false;
281
+ totalDiffTruncated = true;
282
+ } else {
283
+ const perFileLimit = Math.min(TASK_COMMIT_MAX_FILE_DIFF_CHARS, remainingChars);
284
+ const bounded = boundedDisplay(file.diff, perFileLimit);
285
+ diff = bounded.text;
286
+ if (bounded.clipped) {
287
+ diffTruncated = true;
288
+ if (perFileLimit < TASK_COMMIT_MAX_FILE_DIFF_CHARS) totalDiffTruncated = true;
289
+ }
290
+ remainingChars -= diff.length;
291
+ }
292
+ }
293
+ return {
294
+ entryId: file.entryId,
295
+ path: boundedPathDisplay(file.path),
296
+ previousPath: file.previousPath === null ? null : boundedPathDisplay(file.previousPath),
297
+ kind: file.kind,
298
+ binary: file.binary,
299
+ diff,
300
+ diffTruncated,
301
+ diffOmittedReason
302
+ };
303
+ });
304
+ return taskCommitPreflightSchema.parse({
305
+ protocol: PROTOCOL_VERSION,
306
+ type: "task-commit-preflight",
307
+ taskId,
308
+ preflightId: pending.preflightId,
309
+ branch: pending.branch,
310
+ parentCommit: pending.parentCommit,
311
+ treeOid: pending.treeOid,
312
+ message: pending.message,
313
+ files,
314
+ totalFiles: prepared.totalFiles,
315
+ omittedFiles: prepared.omittedFiles,
316
+ totalDiffTruncated,
317
+ stagedDivergence: prepared.stagedDivergence,
318
+ validationVerdict: verdict,
319
+ acknowledgementRequired: pending.acknowledgementRequired,
320
+ hooksBypassed: true,
321
+ createdAtMs,
322
+ expiresAtMs: pending.expiresAtMs
323
+ });
324
+ }
325
+ async function preflight(taskId, message) {
326
+ claimPhase(taskId, "preflight");
327
+ pendingPreflights.delete(taskId);
328
+ try {
329
+ if (options.isArchiveActive?.(taskId) === true) {
330
+ throw new TaskCommitOperationError(
331
+ "archive-in-progress",
332
+ "This task is being archived or restored. Wait for that operation to finish.",
333
+ 409
334
+ );
335
+ }
336
+ refuseRunningValidation(taskId);
337
+ refuseActiveMerge(taskId);
338
+ const task = await resolveTask(taskId);
339
+ const repository = await resolveRepository(task);
340
+ await refuseWorkingAgent(taskId);
341
+ const { canonicalWorktree, state: before } = await captureVerifiedState(
342
+ task,
343
+ repository,
344
+ "preflight"
345
+ );
346
+ let prepared;
347
+ try {
348
+ prepared = await prepare({
349
+ worktreePath: canonicalWorktree,
350
+ expectedBranch: task.branch
351
+ });
352
+ } catch (error) {
353
+ throw mapGitError(error, "preflight");
354
+ }
355
+ if (prepared.parentCommit !== before.worktree.headCommit) {
356
+ throw new TaskCommitOperationError(
357
+ "commit-failed",
358
+ "The worktree state changed while the commit was being prepared. Try again.",
359
+ 409
360
+ );
361
+ }
362
+ const { state: after } = await captureVerifiedState(task, repository, "preflight");
363
+ if (after.stateFingerprint !== before.stateFingerprint) {
364
+ throw new TaskCommitOperationError(
365
+ "commit-failed",
366
+ "The worktree changed while the commit was being prepared. Review the new state and try again.",
367
+ 409
368
+ );
369
+ }
370
+ const verdict = validationVerdict(task, before.stateFingerprint);
371
+ const createdAtMs = now();
372
+ const pending = {
373
+ preflightId: createId(),
374
+ message,
375
+ branch: prepared.branch,
376
+ parentCommit: prepared.parentCommit,
377
+ treeOid: prepared.treeOid,
378
+ stagedPaths: prepared.stagedPaths,
379
+ stateFingerprint: before.stateFingerprint,
380
+ validationVerdict: verdict,
381
+ acknowledgementRequired: verdict !== "current-passed",
382
+ expiresAtMs: createdAtMs + preflightTtlMs
383
+ };
384
+ const payload = preflightPayload(taskId, pending, prepared, verdict, createdAtMs);
385
+ pendingPreflights.set(taskId, pending);
386
+ return payload;
387
+ } finally {
388
+ activePhases.delete(taskId);
389
+ }
390
+ }
391
+ async function execute(taskId, request) {
392
+ claimPhase(taskId, "execute");
393
+ try {
394
+ if (options.isArchiveActive?.(taskId) === true) {
395
+ throw new TaskCommitOperationError(
396
+ "archive-in-progress",
397
+ "This task is being archived or restored. Wait for that operation to finish.",
398
+ 409
399
+ );
400
+ }
401
+ refuseRunningValidation(taskId);
402
+ refuseActiveMerge(taskId);
403
+ const task = await resolveTask(taskId);
404
+ const repository = await resolveRepository(task);
405
+ await refuseWorkingAgent(taskId);
406
+ const pending = pendingPreflights.get(taskId);
407
+ if (pending === void 0 || pending.preflightId !== request.preflightId) {
408
+ throw new TaskCommitOperationError(
409
+ "preflight-not-found",
410
+ "This commit preflight is no longer valid. Review the current changes and preflight again.",
411
+ 409
412
+ );
413
+ }
414
+ if (now() >= pending.expiresAtMs) {
415
+ pendingPreflights.delete(taskId);
416
+ throw new TaskCommitOperationError(
417
+ "preflight-not-found",
418
+ "This commit preflight expired. Review the current changes and preflight again.",
419
+ 409
420
+ );
421
+ }
422
+ const currentVerdict = validationVerdict(task, pending.stateFingerprint);
423
+ if (currentVerdict !== pending.validationVerdict) {
424
+ pendingPreflights.delete(taskId);
425
+ throw new TaskCommitOperationError(
426
+ "preflight-outdated",
427
+ "The validation verdict changed after this preflight. Review the current result and preflight again.",
428
+ 409
429
+ );
430
+ }
431
+ if (pending.acknowledgementRequired && request.acknowledgeValidation !== true) {
432
+ throw new TaskCommitOperationError(
433
+ "acknowledgement-required",
434
+ "Acknowledge the validation verdict explicitly before committing this snapshot.",
435
+ 409
436
+ );
437
+ }
438
+ let canonicalWorktree;
439
+ let current;
440
+ try {
441
+ ({ canonicalWorktree, state: current } = await captureVerifiedState(
442
+ task,
443
+ repository,
444
+ "execute"
445
+ ));
446
+ } catch (error) {
447
+ pendingPreflights.delete(taskId);
448
+ throw error;
449
+ }
450
+ if (current.stateFingerprint !== pending.stateFingerprint) {
451
+ pendingPreflights.delete(taskId);
452
+ throw new TaskCommitOperationError(
453
+ "preflight-outdated",
454
+ "The worktree changed after this preflight was approved. Review the new state and preflight again.",
455
+ 409
456
+ );
457
+ }
458
+ pendingPreflights.delete(taskId);
459
+ let publication;
460
+ try {
461
+ publication = await publish({
462
+ worktreePath: canonicalWorktree,
463
+ branch: pending.branch,
464
+ parentCommit: pending.parentCommit,
465
+ treeOid: pending.treeOid,
466
+ message: pending.message,
467
+ stagedPaths: pending.stagedPaths,
468
+ reflogReason: `pita-commit: task ${taskId} preflight ${pending.preflightId}`
469
+ });
470
+ } catch (error) {
471
+ throw mapGitError(error, "execute");
472
+ }
473
+ return taskCommitResultSchema.parse({
474
+ protocol: PROTOCOL_VERSION,
475
+ type: "task-commit-result",
476
+ taskId,
477
+ branch: publication.branch,
478
+ commit: publication.commit,
479
+ treeOid: publication.treeOid,
480
+ parentCommit: publication.parentCommit,
481
+ signed: publication.signed,
482
+ indexSynced: publication.indexSynced,
483
+ headVerified: publication.headVerified,
484
+ capturedAtMs: now()
485
+ });
486
+ } finally {
487
+ activePhases.delete(taskId);
488
+ }
489
+ }
490
+ return {
491
+ preflight,
492
+ execute,
493
+ isBusy: (taskId) => activePhases.has(taskId),
494
+ invalidate(taskId) {
495
+ pendingPreflights.delete(taskId);
496
+ }
497
+ };
498
+ }
499
+ export {
500
+ COMMIT_PREFLIGHT_TTL_MS,
501
+ TaskCommitOperationError,
502
+ createTaskCommitService
503
+ };