@rallycry/conveyor-agent 10.2.2 → 10.2.5

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
 
@@ -143,6 +143,12 @@ declare class AgentConnection {
143
143
  cols: number;
144
144
  rows: number;
145
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;
146
152
  /**
147
153
  * Report that the interactive CLI process for this session has died and no
148
154
  * respawn is imminent (fire-and-forget). The server clears the scrollback
@@ -164,7 +170,10 @@ declare class AgentConnection {
164
170
  duplicate: true;
165
171
  matchedMessagePreview: string;
166
172
  };
167
- sendHeartbeat(): void;
173
+ sendHeartbeat(loopLagMs?: number): void;
174
+ private heartbeatWorker;
175
+ startHeartbeatWorker(sharedBuffer: SharedArrayBuffer, intervalMs?: number): void;
176
+ stopHeartbeatWorker(): void;
168
177
  emitModeChanged(agentMode?: AgentMode | null): void;
169
178
  updateTaskFields(fields: {
170
179
  plan?: string;
@@ -409,8 +418,14 @@ declare class SessionRunner {
409
418
  private agentSpokeThisTurn;
410
419
  /** Guards overlapping runs of the periodic git flush. */
411
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;
412
425
  /** Runtime preview-port poller (v3 dynamic port discovery). */
413
426
  private portDiscovery;
427
+ /** Main event-loop lag measurement, shared with the heartbeat worker. */
428
+ private readonly loopLag;
414
429
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks);
415
430
  get state(): AgentRunnerStatus;
416
431
  get sessionId(): string;
@@ -508,16 +523,27 @@ declare class SessionRunner {
508
523
  stop(): void;
509
524
  softStop(): void;
510
525
  private static readonly MAX_STUCK_NUDGES;
511
- /** The prompt delivered into the live TUI when an auto agent looks stuck. It
512
- * 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. */
513
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;
514
532
  /**
515
- * An auto task is "stuck" when its last turn ended (the call sites run
516
- * post-turn, so the TUI is no longer actively working), it has no open PR,
517
- * AND the agent did not communicate in chat this turn. If the agent posted a
518
- * message (explained itself / asked for help) it is waiting on a human, not
519
- * 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.
520
545
  */
546
+ private autoStuckKind;
521
547
  private isAutoStuck;
522
548
  /**
523
549
  * Stop driving the agent and route the core loop into dormant idle —
@@ -528,15 +554,18 @@ declare class SessionRunner {
528
554
  */
529
555
  private enterDormantForHumanTakeover;
530
556
  private refreshTaskContext;
557
+ /** Prompt + chat-message label for a stuck-nudge of the given kind. */
558
+ private stuckNudgeContent;
531
559
  private maybeHandleStuckAuto;
532
560
  private buildFullContext;
533
561
  private createQueryBridge;
534
562
  private wireConnectionCallbacks;
535
563
  /** Eager finalize snapshot triggered by the reconciler's sleep signal
536
- * (session:finalizeSnapshot). Runs pg_dump + a full capture and uploads it
564
+ * (session:finalizeSnapshot). Runs a full workspace capture and uploads it
537
565
  * NOW so the sleep confirms on this snapshot instead of waiting for the
538
- * ~2min periodic flush and so sidecar DB state (which ONLY finalizeForSleep
539
- * 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
540
569
  * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
541
570
  private finalizeSnapshotNow;
542
571
  /** Proactively refresh the GitHub token before the 1-hour expiry. */
@@ -569,29 +598,16 @@ interface AgentRunnerCallbacks {
569
598
  onStatusChange: (status: string) => void | Promise<void>;
570
599
  }
571
600
 
572
- declare class PlanSync {
573
- private planFileSnapshot;
574
- private lockedPlanFile;
575
- private workspaceDir;
576
- private connection;
577
- constructor(workspaceDir: string, connection: AgentConnection);
578
- updateWorkspaceDir(workspaceDir: string): void;
579
- private getPlanDirs;
580
- snapshotPlanFiles(): void;
581
- private findNewestPlanFile;
582
- syncPlanFile(): Promise<void>;
583
- }
584
-
585
601
  /** Returns true if the working tree has uncommitted changes (staged or unstaged). */
586
- declare function hasUncommittedChanges(cwd: string): boolean;
602
+ declare function hasUncommittedChanges(cwd: string): Promise<boolean>;
587
603
  /** Returns the current branch name, or null if on detached HEAD. */
588
- declare function getCurrentBranch(cwd: string): string | null;
604
+ declare function getCurrentBranch(cwd: string): Promise<string | null>;
589
605
  /** Returns true if there are committed changes that haven't been pushed to origin. */
590
- declare function hasUnpushedCommits(cwd: string): boolean;
606
+ declare function hasUnpushedCommits(cwd: string): Promise<boolean>;
591
607
  /** Stage all changes and create a commit. Returns the commit hash if successful. */
592
- declare function stageAndCommit(cwd: string, message: string): string | null;
608
+ declare function stageAndCommit(cwd: string, message: string): Promise<string | null>;
593
609
  /** Update the git remote URL with a fresh token. */
594
- declare function updateRemoteToken(cwd: string, token: string): void;
610
+ declare function updateRemoteToken(cwd: string, token: string): Promise<void>;
595
611
  /** Best-effort flush of pending work: pushes any real (intentional) commits to
596
612
  * the task branch, and captures uncommitted changes as a WIP snapshot on the
597
613
  * `conveyor-wip/<branch>` ref — never as commits on the branch itself. When
@@ -608,18 +624,18 @@ declare function flushPendingChanges(cwd: string, opts?: {
608
624
  * attempting the push. On auth failure, refreshes again and retries. */
609
625
  declare function pushToOrigin(cwd: string, refreshToken?: () => Promise<string | undefined>, skipVerify?: boolean): Promise<boolean>;
610
626
 
611
- declare function ensureWorktree(projectDir: string, taskId: string, branch?: string): string;
627
+ declare function ensureWorktree(projectDir: string, taskId: string, branch?: string): Promise<string>;
612
628
  /**
613
629
  * Detach any task worktree that has `branch` checked out, releasing the branch lock
614
630
  * so the main project directory can check it out. Only touches worktrees inside
615
631
  * `.worktrees/` — never the main project directory.
616
632
  */
617
- declare function detachWorktreeBranch(projectDir: string, branch: string): void;
633
+ declare function detachWorktreeBranch(projectDir: string, branch: string): Promise<void>;
618
634
  /**
619
635
  * Force-remove is intentional: this runs after task completion or explicit stop.
620
636
  * Any uncommitted changes at this point are scratch artifacts, not valuable work.
621
637
  */
622
- declare function removeWorktree(projectDir: string, taskId: string): void;
638
+ declare function removeWorktree(projectDir: string, taskId: string): Promise<void>;
623
639
 
624
640
  interface CaptureContext {
625
641
  cwd: string;
@@ -627,9 +643,6 @@ interface CaptureContext {
627
643
  snapshotUploadUrl?: string;
628
644
  refreshToken?: () => Promise<string | undefined>;
629
645
  wipMessage?: string;
630
- /** Path to a pre-built postgres dump OUTSIDE cwd (see pg-dump.ts) that the
631
- * GCS tar should carry under its fixed in-archive name. */
632
- pgDumpPath?: string;
633
646
  /** Force the GCS PUT even when the tar is empty (sizeBytes === 0). Used by the
634
647
  * sleep FINALIZE path so `snapshotAt` is stamped and the reconciler's
635
648
  * teardown confirm lands immediately — an empty workspace is a valid (empty)
@@ -644,8 +657,6 @@ interface SnapshotArtifact {
644
657
  sizeBytes: number;
645
658
  gcsUploaded: boolean;
646
659
  wipPushed: boolean;
647
- /** Whether this capture bundled a postgres dump into the GCS artifact. */
648
- pgDumped: boolean;
649
660
  }
650
661
  interface RestoreResult {
651
662
  source: "gcs" | "wip" | "clean";
@@ -656,7 +667,6 @@ interface GcsUploadResult {
656
667
  fileCount: number;
657
668
  deletionCount: number;
658
669
  sizeBytes: number;
659
- pgDumped: boolean;
660
670
  }
661
671
  /**
662
672
  * The GCS sink on its own: assemble the snapshot tar (the single capture
@@ -667,7 +677,6 @@ interface GcsUploadResult {
667
677
  * after any earlier in-flight one has settled.
668
678
  */
669
679
  declare function uploadSnapshotToGcs(cwd: string, uploadUrl: string | undefined, opts?: {
670
- pgDumpPath?: string;
671
680
  forceUpload?: boolean;
672
681
  }): Promise<GcsUploadResult>;
673
682
  declare function captureSnapshot(ctx: CaptureContext): Promise<SnapshotArtifact>;
@@ -692,7 +701,6 @@ declare function restoreOnBoot(bundle: Pick<BootstrapBundle, "snapshotUrl"> & {
692
701
  interface ConveyorConfig {
693
702
  setupCommand?: string;
694
703
  startCommand?: string;
695
- previewPort?: number;
696
704
  }
697
705
  interface ForwardPortsResult {
698
706
  ports: number[];
@@ -712,4 +720,4 @@ declare function runStartCommand(cmd: string, cwd: string, onOutput: (stream: "s
712
720
  /** Fetch full git history if the repo was cloned with --depth=1. */
713
721
  declare function unshallowRepo(workspaceDir: string): void;
714
722
 
715
- 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-53ZLFIAI.js";
23
+ } from "./chunk-CFELRV35.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.2",
3
+ "version": "10.2.5",
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",
@@ -324,6 +324,13 @@ if [ -n "${CONVEYOR_USER_ID}" ] && [ -n "${USER_HOME_PROJECT_ID}" ] && [ -d "${U
324
324
  ln -sfn "${USER_HOME_ROOT}/.claude" /home/conveyor/.claude
325
325
  ln -sfn "${USER_HOME_ROOT}/.claude.json" /home/conveyor/.claude.json
326
326
  ln -sfn "${USER_HOME_ROOT}/.config/claude" /home/conveyor/.config/claude
327
+
328
+ # OpenCode state (sessions + config) persists the same way as ~/.claude.
329
+ mkdir -p "${USER_HOME_ROOT}/.local/share/opencode" "${USER_HOME_ROOT}/.config/opencode" 2>/dev/null || true
330
+ rm -rf /home/conveyor/.local/share/opencode /home/conveyor/.config/opencode 2>/dev/null || true
331
+ mkdir -p /home/conveyor/.local/share 2>/dev/null || true
332
+ ln -sfn "${USER_HOME_ROOT}/.local/share/opencode" /home/conveyor/.local/share/opencode
333
+ ln -sfn "${USER_HOME_ROOT}/.config/opencode" /home/conveyor/.config/opencode
327
334
  else
328
335
  echo "[pool] Skipping user-home symlink (userId='${CONVEYOR_USER_ID}', projectId='${USER_HOME_PROJECT_ID}', mount present: $([ -d "${USER_HOME_MOUNT}" ] && echo yes || echo no))"
329
336
  # No persistent home — the CLI reads the pod-local config; seed it there so
@@ -331,6 +338,88 @@ else
331
338
  seed_claude_json /home/conveyor/.claude.json
332
339
  fi
333
340
 
341
+ # ── Wire published graphify bundles from the shared user-home mount ──
342
+ # A locally published graph lives in the same GCS-FUSE user-home bucket as
343
+ # Claude state, under users/_shared/graphify/<repo>/latest. Export the path
344
+ # contract before the agent starts, then bind the files into graphify-out after
345
+ # git has prepared the workspace but before the ready marker is released.
346
+ configure_graphify_env() {
347
+ if [ "${CONVEYOR_GRAPHIFY_DISABLE:-}" = "1" ]; then
348
+ return 0
349
+ fi
350
+
351
+ local slug="${CONVEYOR_GRAPHIFY_SLUG:-${REPO_NAME:-}}"
352
+ if [ -z "${slug}" ]; then
353
+ return 0
354
+ fi
355
+
356
+ local primary_root="${USER_HOME_MOUNT}/users/_shared/graphify"
357
+ local legacy_root="${USER_HOME_MOUNT}/_shared/graphify"
358
+ local shared_root="${CONVEYOR_GRAPHIFY_SHARED_ROOT:-}"
359
+ if [ -z "${shared_root}" ]; then
360
+ if [ -d "${primary_root}" ] || [ ! -d "${legacy_root}" ]; then
361
+ shared_root="${primary_root}"
362
+ else
363
+ shared_root="${legacy_root}"
364
+ fi
365
+ fi
366
+
367
+ export CONVEYOR_GRAPHIFY_SLUG="${slug}"
368
+ export CONVEYOR_GRAPHIFY_SHARED_ROOT="${shared_root}"
369
+ export CONVEYOR_GRAPHIFY_DIR="${CONVEYOR_GRAPHIFY_DIR:-${shared_root}/${slug}/latest}"
370
+ export CONVEYOR_GRAPHIFY_GRAPH="${CONVEYOR_GRAPHIFY_GRAPH:-${CONVEYOR_GRAPHIFY_DIR}/graph.json}"
371
+ }
372
+
373
+ bind_graphify_bundle() {
374
+ if [ "${CONVEYOR_GRAPHIFY_DISABLE:-}" = "1" ]; then
375
+ echo "[pool] Graphify bind disabled."
376
+ return 0
377
+ fi
378
+
379
+ if [ -z "${CONVEYOR_GRAPHIFY_SLUG:-}" ]; then
380
+ return 0
381
+ fi
382
+
383
+ local workspace="${CONVEYOR_WORKSPACE:-/workspaces/repo}"
384
+ local source_dir="${CONVEYOR_GRAPHIFY_DIR:-}"
385
+ local graph_file="${CONVEYOR_GRAPHIFY_GRAPH:-}"
386
+
387
+ if [ -z "${source_dir}" ] || [ -z "${graph_file}" ]; then
388
+ return 0
389
+ fi
390
+
391
+ if [ ! -f "${graph_file}" ]; then
392
+ echo "[pool] Graphify bundle not found for '${CONVEYOR_GRAPHIFY_SLUG}' at ${source_dir}"
393
+ return 0
394
+ fi
395
+
396
+ local target_dir="${workspace}/graphify-out"
397
+ if ! mkdir -p "${target_dir}" 2>/dev/null; then
398
+ echo "[pool] WARN: unable to create graphify-out at ${target_dir}"
399
+ return 0
400
+ fi
401
+
402
+ local rel
403
+ for rel in graph.json GRAPH_REPORT.md manifest.json .graphify_analysis.json .graphify_labels.json publish-manifest.json cost.json; do
404
+ if [ ! -e "${source_dir}/${rel}" ]; then
405
+ continue
406
+ fi
407
+ if [ -e "${target_dir}/${rel}" ] || [ -L "${target_dir}/${rel}" ]; then
408
+ continue
409
+ fi
410
+ ln -s "${source_dir}/${rel}" "${target_dir}/${rel}" 2>/dev/null || true
411
+ done
412
+
413
+ echo "[pool] Bound graphify bundle '${CONVEYOR_GRAPHIFY_SLUG}' into ${target_dir}"
414
+ }
415
+
416
+ mark_git_ready() {
417
+ bind_graphify_bundle
418
+ : > "$GIT_READY_MARKER"
419
+ }
420
+
421
+ configure_graphify_env
422
+
334
423
  # ═══════════════════════════════════════════════════════════════════════════
335
424
  # WORKSPACE GIT — moved OFF the pre-launch critical path (Claudespace v3).
336
425
  #
@@ -489,7 +578,7 @@ prepare_workspace_git() {
489
578
  # Task repo is ready HERE — release the gate. The agent (already launched)
490
579
  # is polling for this marker before it spawns Claude / runs setup. Reference
491
580
  # repos are cloned AFTER this so they never block Claude.
492
- : > "$GIT_READY_MARKER"
581
+ mark_git_ready
493
582
  elif [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
494
583
  # Non-image but repo already present (e.g. a baked image running without
495
584
  # CONVEYOR_POD_IMAGE set). Must NOT settle for a bare remote-URL refresh —
@@ -501,7 +590,7 @@ prepare_workspace_git() {
501
590
  if ! sync_task_branch_to_repo; then
502
591
  return 1
503
592
  fi
504
- : > "$GIT_READY_MARKER"
593
+ mark_git_ready
505
594
  elif [ -n "${CONVEYOR_GITHUB_TOKEN}" ] && [ -n "${REPO_OWNER}" ] && [ -n "${REPO_NAME}" ] && [ -n "${BRANCH}" ]; then
506
595
  echo "[pool] Cloning repo post-assignment (pre-clone was missing)..."
507
596
  # Guard each clone/fetch/checkout: under `set -e` a bare failing clone would
@@ -543,14 +632,14 @@ prepare_workspace_git() {
543
632
  return 1
544
633
  fi
545
634
  fi
546
- : > "$GIT_READY_MARKER"
635
+ mark_git_ready
547
636
  else
548
637
  # No git plan (e.g. task-less pod, or no token/repo). Nothing to prepare —
549
638
  # the repo dir already exists (created before launch) and the agent's own
550
639
  # git-sync fallback (guarded by CONVEYOR_GIT_READY) never runs on this pod
551
640
  # path anyway. Signal ready so the agent doesn't wait out the timeout.
552
641
  echo "[pool] No git plan to prepare — marking git ready."
553
- : > "$GIT_READY_MARKER"
642
+ mark_git_ready
554
643
  fi
555
644
 
556
645
  # ── Clone reference repos into /workspaces/references (best-effort) ──