@bermudi/pi-delegate 0.1.17 → 0.1.19

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/types.ts CHANGED
@@ -119,6 +119,9 @@ export interface AsyncTicket {
119
119
  workersSettled?: boolean;
120
120
  /** Batch-level warning attached to this dispatch, not to any one task. */
121
121
  dispatchWarning?: string;
122
+ /** Batch-level notice that overlapping shared writers were serialized into
123
+ * task order instead of rejected. Informational, not a warning. */
124
+ serializedNotice?: string;
122
125
  /** Immutable dispatch-scoped delegate.json snapshot used by async workers and
123
126
  * later result formatting. */
124
127
  config?: import("./config.ts").DelegateConfig;
@@ -291,6 +294,9 @@ export interface DelegateDetails {
291
294
  overlapWarning?: string;
292
295
  /** Warning that applies to the whole dispatch rather than an individual task. */
293
296
  dispatchWarning?: string;
297
+ /** Notice that overlapping shared writers in this dispatch were serialized
298
+ * into task order instead of rejected. Informational, not a warning. */
299
+ serializedNotice?: string;
294
300
  }
295
301
 
296
302
  export interface TaskResult {
@@ -342,6 +348,7 @@ export interface TaskResult {
342
348
  export type TaskIntegrationStatus =
343
349
  | "applied_unverified"
344
350
  | "no_changes"
351
+ | "retained"
345
352
  | "conflict"
346
353
  | "discarded"
347
354
  | "apply_failed";
@@ -381,6 +388,15 @@ export type TaskIntegration =
381
388
  * paths. They do not turn the discarded proposal into an apply failure. */
382
389
  classificationIssues?: Array<{ path: string; reason: string }>;
383
390
  })
391
+ | (TaskIntegrationFiles & {
392
+ status: "retained";
393
+ reason: string;
394
+ baselineRef: string;
395
+ proposalRef: string;
396
+ patchPath: string;
397
+ worktreePath?: string;
398
+ conflicts?: never;
399
+ })
384
400
  | (TaskIntegrationFiles & {
385
401
  status: "conflict";
386
402
  conflicts: Array<{ path: string; reason: string }>;
@@ -444,6 +460,9 @@ export interface TaskRunEnv {
444
460
  * use this instead of the live singleton so retry/stall/output/provider
445
461
  * settings stay stable for the ticket's lifetime. */
446
462
  config?: import("./config.ts").DelegateConfig;
463
+ /** Injectable runtime context. When omitted, task execution falls back to the
464
+ * module-level default runtime for backward compatibility. */
465
+ runtime?: import("./runtime.ts").DelegateRuntime;
447
466
  }
448
467
 
449
468
  /** Structural subset of Pi's `ExtensionContext` used by delegate's
package/workspace.ts CHANGED
@@ -280,9 +280,10 @@ async function assertGitMetadataContained(
280
280
  root: string,
281
281
  signal: AbortSignal,
282
282
  ): Promise<void> {
283
- const hasGitDir = await fs.promises
284
- .stat(path.join(root, ".git"))
285
- .then((stat) => stat.isDirectory(), () => false);
283
+ const hasGitDir = await fs.promises.stat(path.join(root, ".git")).then(
284
+ (stat) => stat.isDirectory(),
285
+ () => false,
286
+ );
286
287
  if (!hasGitDir) return;
287
288
  const effectiveWorktree = path.resolve(
288
289
  (
@@ -749,6 +750,57 @@ async function sweepStaleScratchLeases(
749
750
  }
750
751
  }
751
752
 
753
+ /** Read-only pre-flight every scratch creation runs before anything exists:
754
+ * canonical cwd, safe project-root discovery, and the shared tree/git-metadata
755
+ * rule. Fast-fails on blockers that are knowable before a container, lease, or
756
+ * copy is created — a rejection here costs milliseconds instead of a doomed
757
+ * reflink copy. The post-copy validation stays the authority for trees that
758
+ * change mid-copy. Throws `ScratchSetupError` on any deterministic blocker. */
759
+ async function resolveScratchSource(
760
+ cwd: string,
761
+ signal: AbortSignal,
762
+ parentSignal: AbortSignal | undefined,
763
+ ): Promise<{ sourceCwd: string; sourceRoot: string }> {
764
+ throwIfSetupCancelled(signal, parentSignal);
765
+ const sourceCwd = await fs.promises.realpath(cwd);
766
+ throwIfSetupCancelled(signal, parentSignal);
767
+ const sourceRoot = await findCopyRoot(sourceCwd, signal);
768
+ throwIfSetupCancelled(signal, parentSignal);
769
+ if (!isWithin(sourceRoot, sourceCwd)) {
770
+ throw new ScratchSetupError(
771
+ "Scratch workspace could not map the task cwd into its project root.",
772
+ );
773
+ }
774
+ await validateScratchTree(
775
+ sourceRoot,
776
+ sourceRoot,
777
+ false,
778
+ signal,
779
+ parentSignal,
780
+ );
781
+ await assertGitMetadataContained(sourceRoot, signal);
782
+ return { sourceCwd, sourceRoot };
783
+ }
784
+
785
+ /** Verify that scratch setup would proceed for `cwd`, without creating
786
+ * anything: the same pre-flight `createScratchWorkspace` runs before any
787
+ * container, lease, or copy exists (one walker, one rule — the post-copy
788
+ * validation stays the authority for trees that change mid-copy). Throws
789
+ * `ScratchSetupError` with the same message setup would produce, so callers
790
+ * can recommend scratch only where it can actually run. */
791
+ export async function checkScratchWorkspaceSupport(
792
+ cwd: string,
793
+ signal?: AbortSignal,
794
+ ): Promise<void> {
795
+ if (process.platform !== "linux" || !fs.existsSync("/proc/self/fd")) {
796
+ throw new ScratchSetupError(
797
+ "Scratch workspaces require Linux with GNU cp and /proc/self/fd available.",
798
+ );
799
+ }
800
+ const active = signal ?? new AbortController().signal;
801
+ await resolveScratchSource(cwd, active, active);
802
+ }
803
+
752
804
  /**
753
805
  * Make an ephemeral, same-filesystem CoW copy of the Git repository containing
754
806
  * cwd (or cwd itself outside Git). This is accidental-write isolation, not a
@@ -790,30 +842,9 @@ export async function createScratchWorkspace(
790
842
  let copiedOwnerStat: fs.Stats | undefined;
791
843
  try {
792
844
  if (signal?.aborted) controller.abort(signal.reason);
793
- throwIfSetupCancelled(controller.signal, signal);
794
- sourceCwd = await fs.promises.realpath(cwd);
795
- throwIfSetupCancelled(controller.signal, signal);
796
- sourceRoot = await findCopyRoot(sourceCwd, controller.signal);
797
- throwIfSetupCancelled(controller.signal, signal);
798
- if (!isWithin(sourceRoot, sourceCwd)) {
799
- throw new ScratchSetupError(
800
- "Scratch workspace could not map the task cwd into its project root.",
801
- );
802
- }
803
-
804
- // Fast-fail on blockers that are knowable before anything is created:
805
- // the same rule the copy validation enforces, run read-only against the
806
- // source. A rejection here costs milliseconds instead of a doomed
807
- // reflink copy, and no lease/container is left behind. The post-copy
808
- // validation below stays the authority — the tree can change mid-copy.
809
- await validateScratchTree(
810
- sourceRoot,
811
- sourceRoot,
812
- false,
813
- controller.signal,
814
- signal,
815
- );
816
- await assertGitMetadataContained(sourceRoot, controller.signal);
845
+ const resolved = await resolveScratchSource(cwd, controller.signal, signal);
846
+ sourceCwd = resolved.sourceCwd;
847
+ sourceRoot = resolved.sourceRoot;
817
848
 
818
849
  containerDir = path.join(path.dirname(sourceRoot), SCRATCH_CONTAINER_NAME);
819
850
  const uid = process.getuid?.();