@rallycry/conveyor-agent 10.2.1 → 10.2.4

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _project_shared from '@project/shared';
2
- import { RunnerMode, AgentSessionServiceMethods, AgentMode, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
2
+ import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
3
3
  export * from '@project/shared';
4
4
  import { ChildProcess } from 'node:child_process';
5
5
 
@@ -32,6 +32,20 @@ interface ApiKeyUpdateData {
32
32
  apiKey: string;
33
33
  isSubscription?: boolean;
34
34
  }
35
+ /**
36
+ * Server push asking the builder pod to spawn a same-pod review child: a
37
+ * second conveyor-agent process bound to a fresh review WorkspaceSession
38
+ * (sessionId + sessionJwt). No taskId on the wire — the child inherits the
39
+ * parent's CONVEYOR_TASK_ID and the server derives it from the JWT (SEC-9).
40
+ */
41
+ interface SpawnReviewData {
42
+ sessionId: string;
43
+ sessionJwt: string;
44
+ mode: "code-review";
45
+ branch?: string | null;
46
+ prNumber?: number | null;
47
+ checkoutRef?: string | null;
48
+ }
35
49
  declare class AgentConnection {
36
50
  private socket;
37
51
  private readonly config;
@@ -59,6 +73,8 @@ declare class AgentConnection {
59
73
  private finalizeSnapshotCallback;
60
74
  private runStartCommandCallback;
61
75
  private earlyPullBranches;
76
+ private spawnReviewCallback;
77
+ private earlySpawnReviews;
62
78
  private ptyInputCallback;
63
79
  private ptyResizeCallback;
64
80
  constructor(config: AgentConnectionConfig);
@@ -108,6 +124,14 @@ declare class AgentConnection {
108
124
  onPullBranch(callback: (data: {
109
125
  branch: string;
110
126
  }) => void): void;
127
+ onSpawnReview(callback: (data: SpawnReviewData) => void): void;
128
+ /**
129
+ * Report that a same-pod review child failed to spawn (fire-and-forget).
130
+ * The server Ends the orphaned review session and falls back to a dedicated
131
+ * review pod. sessionId is OUR (builder) session — the SEC-9 guard runs on
132
+ * it; the review session is identified separately.
133
+ */
134
+ reportReviewSpawnFailure(reviewSessionId: string, error?: string): void;
111
135
  onFinalizeSnapshot(callback: () => void): void;
112
136
  onRunStartCommand(callback: () => void): void;
113
137
  /**
@@ -119,6 +143,12 @@ declare class AgentConnection {
119
143
  cols: number;
120
144
  rows: number;
121
145
  }): void;
146
+ /**
147
+ * Forward one compact chat-proxy event derived from the transcript JSONL to
148
+ * the relay (fire-and-forget). Feeds the experimental chat PTY proxy ring.
149
+ * Old servers that don't know the method reject harmlessly.
150
+ */
151
+ sendPtyChatEvent(event: PtyChatEventPayload): void;
122
152
  /**
123
153
  * Report that the interactive CLI process for this session has died and no
124
154
  * respawn is imminent (fire-and-forget). The server clears the scrollback
@@ -140,7 +170,10 @@ declare class AgentConnection {
140
170
  duplicate: true;
141
171
  matchedMessagePreview: string;
142
172
  };
143
- sendHeartbeat(): void;
173
+ sendHeartbeat(loopLagMs?: number): void;
174
+ private heartbeatWorker;
175
+ startHeartbeatWorker(sharedBuffer: SharedArrayBuffer, intervalMs?: number): void;
176
+ stopHeartbeatWorker(): void;
144
177
  emitModeChanged(agentMode?: AgentMode | null): void;
145
178
  updateTaskFields(fields: {
146
179
  plan?: string;
@@ -385,8 +418,14 @@ declare class SessionRunner {
385
418
  private agentSpokeThisTurn;
386
419
  /** Guards overlapping runs of the periodic git flush. */
387
420
  private periodicFlushInFlight;
421
+ /** Consecutive flush ticks skipped because a heavy gate was running. */
422
+ private gateSkipCount;
423
+ /** Max consecutive gate-deferred ticks (~2 min each) before flushing anyway. */
424
+ private static readonly MAX_GATE_SKIPS;
388
425
  /** Runtime preview-port poller (v3 dynamic port discovery). */
389
426
  private portDiscovery;
427
+ /** Main event-loop lag measurement, shared with the heartbeat worker. */
428
+ private readonly loopLag;
390
429
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks);
391
430
  get state(): AgentRunnerStatus;
392
431
  get sessionId(): string;
@@ -484,16 +523,27 @@ declare class SessionRunner {
484
523
  stop(): void;
485
524
  softStop(): void;
486
525
  private static readonly MAX_STUCK_NUDGES;
487
- /** The prompt delivered into the live TUI when an auto agent looks stuck. It
488
- * must offer BOTH escape routes so the agent never just goes idle. */
526
+ /** The build-phase nudge (In Progress, no PR). Offers BOTH escape routes so
527
+ * the agent never just goes idle. */
489
528
  private static readonly STUCK_PROMPT;
529
+ /** The planning-phase nudge (still Planning, identification/plan unfinished).
530
+ * Same two-escape-route shape, aimed at finishing discovery instead of a PR. */
531
+ private static readonly STUCK_PROMPT_PLANNING;
490
532
  /**
491
- * An auto task is "stuck" when its last turn ended (the call sites run
492
- * post-turn, so the TUI is no longer actively working), it has no open PR,
493
- * AND the agent did not communicate in chat this turn. If the agent posted a
494
- * message (explained itself / asked for help) it is waiting on a human, not
495
- * silently stuck, so we leave it attached instead of nudging.
533
+ * Classify how an auto task is "stuck" after its turn ended (call sites run
534
+ * post-turn, so the TUI is no longer working), or null if it isn't. Shared
535
+ * guards: only auto, not rate-limited, under the nudge cap, and the agent
536
+ * stayed silent this turn if it posted to chat (explained itself / asked for
537
+ * help) it's waiting on a human, not silently stuck.
538
+ *
539
+ * - "pr": In Progress with no open PR (build finished without shipping).
540
+ * - "planning": still Planning without (identified + saved plan). A fresh auto
541
+ * agent that finishes its plan turn WITHOUT ExitPlanMode would otherwise idle
542
+ * → clean-exit → its session Ends → the workspace is reaped "agent_gone"
543
+ * before a plan ever landed. Nudging (and, on exhaustion, going dormant)
544
+ * keeps the session Active so the pod is never prematurely slept.
496
545
  */
546
+ private autoStuckKind;
497
547
  private isAutoStuck;
498
548
  /**
499
549
  * Stop driving the agent and route the core loop into dormant idle —
@@ -504,15 +554,18 @@ declare class SessionRunner {
504
554
  */
505
555
  private enterDormantForHumanTakeover;
506
556
  private refreshTaskContext;
557
+ /** Prompt + chat-message label for a stuck-nudge of the given kind. */
558
+ private stuckNudgeContent;
507
559
  private maybeHandleStuckAuto;
508
560
  private buildFullContext;
509
561
  private createQueryBridge;
510
562
  private wireConnectionCallbacks;
511
563
  /** Eager finalize snapshot triggered by the reconciler's sleep signal
512
- * (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
564
+ * (session:finalizeSnapshot). Runs a full workspace capture and uploads it
513
565
  * NOW so the sleep confirms on this snapshot instead of waiting for the
514
- * ~2min periodic flush and so sidecar DB state (which ONLY finalizeForSleep
515
- * captures via pg_dump) actually lands. Best-effort: on failure the
566
+ * ~2min periodic flush. Sidecar DB state is intentionally NOT captured it
567
+ * is no longer durable across sleep/wake (#2623); only the agent's
568
+ * /workspace rides the snapshot. Best-effort: on failure the
516
569
  * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
517
570
  private finalizeSnapshotNow;
518
571
  /** Proactively refresh the GitHub token before the 1-hour expiry. */
@@ -545,29 +598,16 @@ interface AgentRunnerCallbacks {
545
598
  onStatusChange: (status: string) => void | Promise<void>;
546
599
  }
547
600
 
548
- declare class PlanSync {
549
- private planFileSnapshot;
550
- private lockedPlanFile;
551
- private workspaceDir;
552
- private connection;
553
- constructor(workspaceDir: string, connection: AgentConnection);
554
- updateWorkspaceDir(workspaceDir: string): void;
555
- private getPlanDirs;
556
- snapshotPlanFiles(): void;
557
- private findNewestPlanFile;
558
- syncPlanFile(): Promise<void>;
559
- }
560
-
561
601
  /** Returns true if the working tree has uncommitted changes (staged or unstaged). */
562
- declare function hasUncommittedChanges(cwd: string): boolean;
602
+ declare function hasUncommittedChanges(cwd: string): Promise<boolean>;
563
603
  /** Returns the current branch name, or null if on detached HEAD. */
564
- declare function getCurrentBranch(cwd: string): string | null;
604
+ declare function getCurrentBranch(cwd: string): Promise<string | null>;
565
605
  /** Returns true if there are committed changes that haven't been pushed to origin. */
566
- declare function hasUnpushedCommits(cwd: string): boolean;
606
+ declare function hasUnpushedCommits(cwd: string): Promise<boolean>;
567
607
  /** Stage all changes and create a commit. Returns the commit hash if successful. */
568
- declare function stageAndCommit(cwd: string, message: string): string | null;
608
+ declare function stageAndCommit(cwd: string, message: string): Promise<string | null>;
569
609
  /** Update the git remote URL with a fresh token. */
570
- declare function updateRemoteToken(cwd: string, token: string): void;
610
+ declare function updateRemoteToken(cwd: string, token: string): Promise<void>;
571
611
  /** Best-effort flush of pending work: pushes any real (intentional) commits to
572
612
  * the task branch, and captures uncommitted changes as a WIP snapshot on the
573
613
  * `conveyor-wip/<branch>` ref — never as commits on the branch itself. When
@@ -584,18 +624,18 @@ declare function flushPendingChanges(cwd: string, opts?: {
584
624
  * attempting the push. On auth failure, refreshes again and retries. */
585
625
  declare function pushToOrigin(cwd: string, refreshToken?: () => Promise<string | undefined>, skipVerify?: boolean): Promise<boolean>;
586
626
 
587
- declare function ensureWorktree(projectDir: string, taskId: string, branch?: string): string;
627
+ declare function ensureWorktree(projectDir: string, taskId: string, branch?: string): Promise<string>;
588
628
  /**
589
629
  * Detach any task worktree that has `branch` checked out, releasing the branch lock
590
630
  * so the main project directory can check it out. Only touches worktrees inside
591
631
  * `.worktrees/` — never the main project directory.
592
632
  */
593
- declare function detachWorktreeBranch(projectDir: string, branch: string): void;
633
+ declare function detachWorktreeBranch(projectDir: string, branch: string): Promise<void>;
594
634
  /**
595
635
  * Force-remove is intentional: this runs after task completion or explicit stop.
596
636
  * Any uncommitted changes at this point are scratch artifacts, not valuable work.
597
637
  */
598
- declare function removeWorktree(projectDir: string, taskId: string): void;
638
+ declare function removeWorktree(projectDir: string, taskId: string): Promise<void>;
599
639
 
600
640
  interface CaptureContext {
601
641
  cwd: string;
@@ -603,9 +643,6 @@ interface CaptureContext {
603
643
  snapshotUploadUrl?: string;
604
644
  refreshToken?: () => Promise<string | undefined>;
605
645
  wipMessage?: string;
606
- /** Path to a pre-built postgres dump OUTSIDE cwd (see pg-dump.ts) that the
607
- * GCS tar should carry under its fixed in-archive name. */
608
- pgDumpPath?: string;
609
646
  /** Force the GCS PUT even when the tar is empty (sizeBytes === 0). Used by the
610
647
  * sleep FINALIZE path so `snapshotAt` is stamped and the reconciler's
611
648
  * teardown confirm lands immediately — an empty workspace is a valid (empty)
@@ -620,8 +657,6 @@ interface SnapshotArtifact {
620
657
  sizeBytes: number;
621
658
  gcsUploaded: boolean;
622
659
  wipPushed: boolean;
623
- /** Whether this capture bundled a postgres dump into the GCS artifact. */
624
- pgDumped: boolean;
625
660
  }
626
661
  interface RestoreResult {
627
662
  source: "gcs" | "wip" | "clean";
@@ -632,7 +667,6 @@ interface GcsUploadResult {
632
667
  fileCount: number;
633
668
  deletionCount: number;
634
669
  sizeBytes: number;
635
- pgDumped: boolean;
636
670
  }
637
671
  /**
638
672
  * The GCS sink on its own: assemble the snapshot tar (the single capture
@@ -643,7 +677,6 @@ interface GcsUploadResult {
643
677
  * after any earlier in-flight one has settled.
644
678
  */
645
679
  declare function uploadSnapshotToGcs(cwd: string, uploadUrl: string | undefined, opts?: {
646
- pgDumpPath?: string;
647
680
  forceUpload?: boolean;
648
681
  }): Promise<GcsUploadResult>;
649
682
  declare function captureSnapshot(ctx: CaptureContext): Promise<SnapshotArtifact>;
@@ -668,7 +701,6 @@ declare function restoreOnBoot(bundle: Pick<BootstrapBundle, "snapshotUrl"> & {
668
701
  interface ConveyorConfig {
669
702
  setupCommand?: string;
670
703
  startCommand?: string;
671
- previewPort?: number;
672
704
  }
673
705
  interface ForwardPortsResult {
674
706
  ports: number[];
@@ -688,4 +720,4 @@ declare function runStartCommand(cmd: string, cwd: string, onOutput: (stream: "s
688
720
  /** Fetch full git history if the repo was cloned with --depth=1. */
689
721
  declare function unshallowRepo(workspaceDir: string): void;
690
722
 
691
- export { AgentConnection, type AgentConnectionConfig, type AgentRunnerCallbacks, type AgentRunnerConfig, type ApiKeyUpdateData, type CaptureContext, type ConveyorConfig, type GcsUploadResult, type IncomingMessage, type LifecycleCallbacks, type LifecycleConfig, type ModeAction, type ModeTaskContext, PlanSync, type RestoreResult, SessionRunner, type SessionRunnerCallbacks, type SessionRunnerConfig, type SetModeData, type SnapshotArtifact, captureSnapshot, detachWorktreeBranch, ensureWorktree, finalizeForSleep, flushPendingChanges, getCurrentBranch, hasUncommittedChanges, hasUnpushedCommits, loadConveyorConfig, loadForwardPorts, pushToOrigin, removeWorktree, restoreOnBoot, runAuthTokenCommand, runSetupCommand, runStartCommand, stageAndCommit, startPeriodic as startWorkPreservation, stop as stopWorkPreservation, unshallowRepo, updateRemoteToken, uploadSnapshotToGcs };
723
+ export { AgentConnection, type AgentConnectionConfig, type AgentRunnerCallbacks, type AgentRunnerConfig, type ApiKeyUpdateData, type CaptureContext, type ConveyorConfig, type GcsUploadResult, type IncomingMessage, type LifecycleCallbacks, type LifecycleConfig, type ModeAction, type ModeTaskContext, type RestoreResult, SessionRunner, type SessionRunnerCallbacks, type SessionRunnerConfig, type SetModeData, type SnapshotArtifact, captureSnapshot, detachWorktreeBranch, ensureWorktree, finalizeForSleep, flushPendingChanges, getCurrentBranch, hasUncommittedChanges, hasUnpushedCommits, loadConveyorConfig, loadForwardPorts, pushToOrigin, removeWorktree, restoreOnBoot, runAuthTokenCommand, runSetupCommand, runStartCommand, stageAndCommit, startPeriodic as startWorkPreservation, stop as stopWorkPreservation, unshallowRepo, updateRemoteToken, uploadSnapshotToGcs };
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  AgentConnection,
3
- PlanSync,
4
3
  SessionRunner,
5
4
  captureSnapshot,
6
5
  finalizeForSleep,
@@ -21,46 +20,45 @@ import {
21
20
  unshallowRepo,
22
21
  updateRemoteToken,
23
22
  uploadSnapshotToGcs
24
- } from "./chunk-YGLTLUGA.js";
23
+ } from "./chunk-DEMRCBJN.js";
24
+ import "./chunk-7TQO4ZF4.js";
25
25
 
26
26
  // src/runner/worktree.ts
27
- import { execSync } from "child_process";
27
+ import { execFile } from "child_process";
28
28
  import { existsSync } from "fs";
29
29
  import { join } from "path";
30
+ import { promisify } from "util";
31
+ var execFileAsync = promisify(execFile);
32
+ var GIT_TIMEOUT_MS = 6e4;
33
+ async function git(cwd, args) {
34
+ const { stdout } = await execFileAsync("git", args, { cwd, timeout: GIT_TIMEOUT_MS });
35
+ return stdout.toString();
36
+ }
30
37
  var WORKTREE_DIR = ".worktrees";
31
- function ensureWorktree(projectDir, taskId, branch) {
38
+ async function ensureWorktree(projectDir, taskId, branch) {
32
39
  if (projectDir.includes(`/${WORKTREE_DIR}/`)) {
33
40
  return projectDir;
34
41
  }
35
42
  const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
36
43
  if (existsSync(worktreePath)) {
37
44
  if (branch) {
38
- if (hasUncommittedChanges(worktreePath)) {
45
+ if (await hasUncommittedChanges(worktreePath)) {
39
46
  return worktreePath;
40
47
  }
41
48
  try {
42
- execSync(`git checkout --detach origin/${branch}`, {
43
- cwd: worktreePath,
44
- stdio: "ignore"
45
- });
49
+ await git(worktreePath, ["checkout", "--detach", `origin/${branch}`]);
46
50
  } catch {
47
51
  }
48
52
  }
49
53
  return worktreePath;
50
54
  }
51
55
  const ref = branch ? `origin/${branch}` : "HEAD";
52
- execSync(`git worktree add --detach "${worktreePath}" ${ref}`, {
53
- cwd: projectDir,
54
- stdio: "ignore"
55
- });
56
+ await git(projectDir, ["worktree", "add", "--detach", worktreePath, ref]);
56
57
  return worktreePath;
57
58
  }
58
- function detachWorktreeBranch(projectDir, branch) {
59
+ async function detachWorktreeBranch(projectDir, branch) {
59
60
  try {
60
- const output = execSync("git worktree list --porcelain", {
61
- cwd: projectDir,
62
- encoding: "utf-8"
63
- });
61
+ const output = await git(projectDir, ["worktree", "list", "--porcelain"]);
64
62
  const entries = output.split("\n\n");
65
63
  for (const entry of entries) {
66
64
  const lines = entry.trim().split("\n");
@@ -70,27 +68,23 @@ function detachWorktreeBranch(projectDir, branch) {
70
68
  const worktreePath = worktreeLine.replace("worktree ", "");
71
69
  if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;
72
70
  try {
73
- execSync("git checkout --detach", { cwd: worktreePath, stdio: "ignore" });
71
+ await git(worktreePath, ["checkout", "--detach"]);
74
72
  } catch {
75
73
  }
76
74
  }
77
75
  } catch {
78
76
  }
79
77
  }
80
- function removeWorktree(projectDir, taskId) {
78
+ async function removeWorktree(projectDir, taskId) {
81
79
  const worktreePath = join(projectDir, WORKTREE_DIR, taskId);
82
80
  if (!existsSync(worktreePath)) return;
83
81
  try {
84
- execSync(`git worktree remove "${worktreePath}" --force`, {
85
- cwd: projectDir,
86
- stdio: "ignore"
87
- });
82
+ await git(projectDir, ["worktree", "remove", worktreePath, "--force"]);
88
83
  } catch {
89
84
  }
90
85
  }
91
86
  export {
92
87
  AgentConnection,
93
- PlanSync,
94
88
  SessionRunner,
95
89
  captureSnapshot,
96
90
  detachWorktreeBranch,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { hasUncommittedChanges } from \"./git-utils.js\";\n\nconst WORKTREE_DIR = \".worktrees\";\n\nexport function ensureWorktree(projectDir: string, taskId: string, branch?: string): string {\n if (projectDir.includes(`/${WORKTREE_DIR}/`)) {\n return projectDir;\n }\n\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n\n if (existsSync(worktreePath)) {\n if (branch) {\n if (hasUncommittedChanges(worktreePath)) {\n return worktreePath;\n }\n try {\n execSync(`git checkout --detach origin/${branch}`, {\n cwd: worktreePath,\n stdio: \"ignore\",\n });\n } catch {\n /* branch doesn't exist on remote yet */\n }\n }\n return worktreePath;\n }\n\n const ref = branch ? `origin/${branch}` : \"HEAD\";\n execSync(`git worktree add --detach \"${worktreePath}\" ${ref}`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n\n return worktreePath;\n}\n\n/**\n * Detach any task worktree that has `branch` checked out, releasing the branch lock\n * so the main project directory can check it out. Only touches worktrees inside\n * `.worktrees/` — never the main project directory.\n */\nexport function detachWorktreeBranch(projectDir: string, branch: string): void {\n try {\n const output = execSync(\"git worktree list --porcelain\", {\n cwd: projectDir,\n encoding: \"utf-8\",\n });\n const entries = output.split(\"\\n\\n\");\n for (const entry of entries) {\n const lines = entry.trim().split(\"\\n\");\n const worktreeLine = lines.find((l) => l.startsWith(\"worktree \"));\n const branchLine = lines.find((l) => l.startsWith(\"branch \"));\n if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;\n\n const worktreePath = worktreeLine.replace(\"worktree \", \"\");\n if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;\n\n try {\n execSync(\"git checkout --detach\", { cwd: worktreePath, stdio: \"ignore\" });\n } catch {\n /* best effort */\n }\n }\n } catch {\n /* best effort */\n }\n}\n\n/**\n * Force-remove is intentional: this runs after task completion or explicit stop.\n * Any uncommitted changes at this point are scratch artifacts, not valuable work.\n */\nexport function removeWorktree(projectDir: string, taskId: string): void {\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n if (!existsSync(worktreePath)) return;\n try {\n execSync(`git worktree remove \"${worktreePath}\" --force`, {\n cwd: projectDir,\n stdio: \"ignore\",\n });\n } catch {\n /* best effort */\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAGrB,IAAM,eAAe;AAEd,SAAS,eAAe,YAAoB,QAAgB,QAAyB;AAC1F,MAAI,WAAW,SAAS,IAAI,YAAY,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAE1D,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI,QAAQ;AACV,UAAI,sBAAsB,YAAY,GAAG;AACvC,eAAO;AAAA,MACT;AACA,UAAI;AACF,iBAAS,gCAAgC,MAAM,IAAI;AAAA,UACjD,KAAK;AAAA,UACL,OAAO;AAAA,QACT,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,SAAS,UAAU,MAAM,KAAK;AAC1C,WAAS,8BAA8B,YAAY,KAAK,GAAG,IAAI;AAAA,IAC7D,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AAED,SAAO;AACT;AAOO,SAAS,qBAAqB,YAAoB,QAAsB;AAC7E,MAAI;AACF,UAAM,SAAS,SAAS,iCAAiC;AAAA,MACvD,KAAK;AAAA,MACL,UAAU;AAAA,IACZ,CAAC;AACD,UAAM,UAAU,OAAO,MAAM,MAAM;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,IAAI;AACrC,YAAM,eAAe,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,CAAC;AAChE,YAAM,aAAa,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AAC5D,UAAI,CAAC,gBAAgB,eAAe,qBAAqB,MAAM,GAAI;AAEnE,YAAM,eAAe,aAAa,QAAQ,aAAa,EAAE;AACzD,UAAI,CAAC,aAAa,SAAS,IAAI,YAAY,GAAG,EAAG;AAEjD,UAAI;AACF,iBAAS,yBAAyB,EAAE,KAAK,cAAc,OAAO,SAAS,CAAC;AAAA,MAC1E,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,eAAe,YAAoB,QAAsB;AACvE,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAC1D,MAAI,CAAC,WAAW,YAAY,EAAG;AAC/B,MAAI;AACF,aAAS,wBAAwB,YAAY,aAAa;AAAA,MACxD,KAAK;AAAA,MACL,OAAO;AAAA,IACT,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { hasUncommittedChanges } from \"./git-utils.js\";\n\n// Async git (execFile, no shell) — synchronous child processes freeze the\n// agent's event loop and starve the socket heartbeat; see git-utils.ts.\nconst execFileAsync = promisify(execFile);\nconst GIT_TIMEOUT_MS = 60_000;\n\nasync function git(cwd: string, args: string[]): Promise<string> {\n const { stdout } = await execFileAsync(\"git\", args, { cwd, timeout: GIT_TIMEOUT_MS });\n return stdout.toString();\n}\n\nconst WORKTREE_DIR = \".worktrees\";\n\nexport async function ensureWorktree(\n projectDir: string,\n taskId: string,\n branch?: string,\n): Promise<string> {\n if (projectDir.includes(`/${WORKTREE_DIR}/`)) {\n return projectDir;\n }\n\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n\n if (existsSync(worktreePath)) {\n if (branch) {\n if (await hasUncommittedChanges(worktreePath)) {\n return worktreePath;\n }\n try {\n await git(worktreePath, [\"checkout\", \"--detach\", `origin/${branch}`]);\n } catch {\n /* branch doesn't exist on remote yet */\n }\n }\n return worktreePath;\n }\n\n const ref = branch ? `origin/${branch}` : \"HEAD\";\n await git(projectDir, [\"worktree\", \"add\", \"--detach\", worktreePath, ref]);\n\n return worktreePath;\n}\n\n/**\n * Detach any task worktree that has `branch` checked out, releasing the branch lock\n * so the main project directory can check it out. Only touches worktrees inside\n * `.worktrees/` — never the main project directory.\n */\nexport async function detachWorktreeBranch(projectDir: string, branch: string): Promise<void> {\n try {\n const output = await git(projectDir, [\"worktree\", \"list\", \"--porcelain\"]);\n const entries = output.split(\"\\n\\n\");\n for (const entry of entries) {\n const lines = entry.trim().split(\"\\n\");\n const worktreeLine = lines.find((l) => l.startsWith(\"worktree \"));\n const branchLine = lines.find((l) => l.startsWith(\"branch \"));\n if (!worktreeLine || branchLine !== `branch refs/heads/${branch}`) continue;\n\n const worktreePath = worktreeLine.replace(\"worktree \", \"\");\n if (!worktreePath.includes(`/${WORKTREE_DIR}/`)) continue;\n\n try {\n await git(worktreePath, [\"checkout\", \"--detach\"]);\n } catch {\n /* best effort */\n }\n }\n } catch {\n /* best effort */\n }\n}\n\n/**\n * Force-remove is intentional: this runs after task completion or explicit stop.\n * Any uncommitted changes at this point are scratch artifacts, not valuable work.\n */\nexport async function removeWorktree(projectDir: string, taskId: string): Promise<void> {\n const worktreePath = join(projectDir, WORKTREE_DIR, taskId);\n if (!existsSync(worktreePath)) return;\n try {\n await git(projectDir, [\"worktree\", \"remove\", worktreePath, \"--force\"]);\n } catch {\n /* best effort */\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,iBAAiB;AAK1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,iBAAiB;AAEvB,eAAe,IAAI,KAAa,MAAiC;AAC/D,QAAM,EAAE,OAAO,IAAI,MAAM,cAAc,OAAO,MAAM,EAAE,KAAK,SAAS,eAAe,CAAC;AACpF,SAAO,OAAO,SAAS;AACzB;AAEA,IAAM,eAAe;AAErB,eAAsB,eACpB,YACA,QACA,QACiB;AACjB,MAAI,WAAW,SAAS,IAAI,YAAY,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAE1D,MAAI,WAAW,YAAY,GAAG;AAC5B,QAAI,QAAQ;AACV,UAAI,MAAM,sBAAsB,YAAY,GAAG;AAC7C,eAAO;AAAA,MACT;AACA,UAAI;AACF,cAAM,IAAI,cAAc,CAAC,YAAY,YAAY,UAAU,MAAM,EAAE,CAAC;AAAA,MACtE,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,SAAS,UAAU,MAAM,KAAK;AAC1C,QAAM,IAAI,YAAY,CAAC,YAAY,OAAO,YAAY,cAAc,GAAG,CAAC;AAExE,SAAO;AACT;AAOA,eAAsB,qBAAqB,YAAoB,QAA+B;AAC5F,MAAI;AACF,UAAM,SAAS,MAAM,IAAI,YAAY,CAAC,YAAY,QAAQ,aAAa,CAAC;AACxE,UAAM,UAAU,OAAO,MAAM,MAAM;AACnC,eAAW,SAAS,SAAS;AAC3B,YAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,IAAI;AACrC,YAAM,eAAe,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,CAAC;AAChE,YAAM,aAAa,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AAC5D,UAAI,CAAC,gBAAgB,eAAe,qBAAqB,MAAM,GAAI;AAEnE,YAAM,eAAe,aAAa,QAAQ,aAAa,EAAE;AACzD,UAAI,CAAC,aAAa,SAAS,IAAI,YAAY,GAAG,EAAG;AAEjD,UAAI;AACF,cAAM,IAAI,cAAc,CAAC,YAAY,UAAU,CAAC;AAAA,MAClD,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAMA,eAAsB,eAAe,YAAoB,QAA+B;AACtF,QAAM,eAAe,KAAK,YAAY,cAAc,MAAM;AAC1D,MAAI,CAAC,WAAW,YAAY,EAAG;AAC/B,MAAI;AACF,UAAM,IAAI,YAAY,CAAC,YAAY,UAAU,cAAc,SAAS,CAAC;AAAA,EACvE,QAAQ;AAAA,EAER;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "10.2.1",
3
+ "version": "10.2.4",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",