@rallycry/conveyor-agent 10.13.13 → 10.13.14

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.
@@ -4,9 +4,9 @@ import {
4
4
  WorkbenchError,
5
5
  getWorkbenchClient,
6
6
  resetWorkbenchClient
7
- } from "./chunk-YRC57EYG.js";
7
+ } from "./chunk-LZM5OKAC.js";
8
8
  import "./chunk-4VUQ2NPF.js";
9
- import "./chunk-IE7EOKMD.js";
9
+ import "./chunk-VKZ5W2VO.js";
10
10
  export {
11
11
  RemoteProcessHandle,
12
12
  WorkbenchClient,
@@ -14,4 +14,4 @@ export {
14
14
  getWorkbenchClient,
15
15
  resetWorkbenchClient
16
16
  };
17
- //# sourceMappingURL=client-ICIWKSK2.js.map
17
+ //# sourceMappingURL=client-VQIBGPE6.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _project_shared from '@project/shared';
2
- import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus, BootstrapBundle } from '@project/shared';
2
+ import { RunnerMode, AgentSessionServiceMethods, AgentMode, PtyChatEventPayload, WorkspaceDiscoveredPort, AgentQuestion, AgentRunnerStatus } from '@project/shared';
3
3
  export * from '@project/shared';
4
4
  import { ChildProcess } from 'node:child_process';
5
5
 
@@ -88,7 +88,6 @@ declare class AgentConnection {
88
88
  private modeChangeCallback;
89
89
  private apiKeyUpdateCallback;
90
90
  private pullBranchCallback;
91
- private finalizeSnapshotCallback;
92
91
  private runStartCommandCallback;
93
92
  private earlyPullBranches;
94
93
  private spawnReviewCallback;
@@ -164,7 +163,6 @@ declare class AgentConnection {
164
163
  * sessionId is OUR (builder) session — the SEC-9 guard runs on it.
165
164
  */
166
165
  reportSessionSpawnFailure(spawnedSessionId: string, error?: string): void;
167
- onFinalizeSnapshot(callback: () => void): void;
168
166
  onRunStartCommand(callback: () => void): void;
169
167
  /**
170
168
  * Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
@@ -437,6 +435,12 @@ interface SessionRunnerPortDiscovery {
437
435
  start(): Promise<void>;
438
436
  stop(): void;
439
437
  }
438
+ /** Duck-typed handle onto the boot supervisor — just enough surface for the
439
+ * runner to release the app's start command once the core loop is live.
440
+ * Avoids importing the concrete WorkspaceCommandSupervisor class here. */
441
+ interface SessionRunnerWorkspaceCommands {
442
+ notifyLoopReady(): void;
443
+ }
440
444
  interface SessionRunnerDependencies {
441
445
  portDiscovery?: SessionRunnerPortDiscovery;
442
446
  }
@@ -474,10 +478,20 @@ declare class SessionRunner {
474
478
  private readonly portDiscovery;
475
479
  /** Main event-loop lag measurement, shared with the heartbeat worker. */
476
480
  private readonly loopLag;
481
+ /** Boot supervisor handle, set post-construction once it's known (it is
482
+ * started alongside connect(), before the runner itself exists in some
483
+ * call sites) — notified when the core loop goes live so it can release
484
+ * the app's start command. Optional: never set in tests/paths that don't
485
+ * wire a supervisor. */
486
+ private workspaceCommands;
477
487
  constructor(config: SessionRunnerConfig, callbacks: SessionRunnerCallbacks, deps?: SessionRunnerDependencies);
478
488
  get state(): AgentRunnerStatus;
479
489
  get sessionId(): string;
480
490
  get isStopped(): boolean;
491
+ /** Wire the boot supervisor handle post-construction — cli.ts constructs it
492
+ * via startWorkspaceCommandsAfterConnect() (which needs connect() to already
493
+ * be callable), so it can't be a constructor dependency. */
494
+ setWorkspaceCommands(supervisor: SessionRunnerWorkspaceCommands | null): void;
481
495
  /**
482
496
  * Establish the API connection, wire callbacks, and join the session room.
483
497
  * Call this before run() when you need to send events (e.g. setup/start
@@ -558,6 +572,15 @@ declare class SessionRunner {
558
572
  private onPassiveTuiActivity;
559
573
  /** Run queryBridge.executePassive, swallowing abort errors from stop/softStop. */
560
574
  private executePassive;
575
+ /** Shared token-refresh closure for the git-flush call sites (periodic
576
+ * backstop + per-turn flush). Never throws — returns undefined so the
577
+ * caller falls back to its existing remote credentials. */
578
+ private refreshGithubTokenForFlush;
579
+ /** Push uncommitted work to the conveyor-wip ref after a turn. Git-only (no
580
+ * GCS). Primary-branch only — cheap enough to run every turn. Shares the
581
+ * in-flight guard with the periodic backstop so a turn-end flush and a
582
+ * timer tick never overlap. Best-effort, never throws. */
583
+ private flushWipNow;
561
584
  /** Periodic best-effort WIP commit + push during normal agent execution.
562
585
  * Covers ungraceful pod termination (OOMKilled, node crash/eviction) where
563
586
  * the preStop hook + SIGTERM flush don't get a chance to run. No-ops on a
@@ -570,9 +593,6 @@ declare class SessionRunner {
570
593
  * `selectBestKey` rotation honest. Best-effort — never throws, no-op when the
571
594
  * pod has no OAuth token (e.g. API-key projects). */
572
595
  private sampleAndReportKeyUsage;
573
- /** PUT the WorkPreservation snapshot tar to the bundle's capability URL
574
- * when this pod is v3 (CONVEYOR_SNAPSHOT_UPLOAD_URL set). Never throws. */
575
- private uploadGcsSnapshotIfConfigured;
576
596
  /** Best-effort WIP commit + push on shutdown so in-flight work isn't lost
577
597
  * when a claudespace pod is killed. Must be called BEFORE stop() so the
578
598
  * connection is still alive for token refresh. Never throws. */
@@ -582,14 +602,6 @@ declare class SessionRunner {
582
602
  private buildFullContext;
583
603
  private createQueryBridge;
584
604
  private wireConnectionCallbacks;
585
- /** Eager finalize snapshot triggered by the reconciler's sleep signal
586
- * (session:finalizeSnapshot). Runs a full workspace capture and uploads it
587
- * NOW so the sleep confirms on this snapshot instead of waiting for the
588
- * ~2min periodic flush. Sidecar DB state is intentionally NOT captured — it
589
- * is no longer durable across sleep/wake (#2623); only the agent's
590
- * /workspace rides the snapshot. Best-effort: on failure the
591
- * periodic/shutdown path stays the fallback. No-op on non-v3 pods. */
592
- private finalizeSnapshotNow;
593
605
  /** Proactively refresh the GitHub token before the 1-hour expiry. */
594
606
  private refreshGithubToken;
595
607
  /** Re-fetch task context to pick up a newly created branch and check it out. */
@@ -659,67 +671,6 @@ declare function detachWorktreeBranch(projectDir: string, branch: string): Promi
659
671
  */
660
672
  declare function removeWorktree(projectDir: string, taskId: string): Promise<void>;
661
673
 
662
- interface CaptureContext {
663
- cwd: string;
664
- branch: string;
665
- snapshotUploadUrl?: string;
666
- refreshToken?: () => Promise<string | undefined>;
667
- wipMessage?: string;
668
- /** Force the GCS PUT even when the tar is empty (sizeBytes === 0). Used by the
669
- * sleep FINALIZE path so `snapshotAt` is stamped and the reconciler's
670
- * teardown confirm lands immediately — an empty workspace is a valid (empty)
671
- * snapshot; on wake the restore's manifest check falls back to a clean
672
- * checkout. The periodic path leaves this false so a racing empty capture
673
- * never clobbers a good snapshot. */
674
- forceUpload?: boolean;
675
- }
676
- interface SnapshotArtifact {
677
- fileCount: number;
678
- deletionCount: number;
679
- sizeBytes: number;
680
- gcsUploaded: boolean;
681
- wipPushed: boolean;
682
- }
683
- interface RestoreResult {
684
- source: "gcs" | "wip" | "clean";
685
- fileCount?: number;
686
- }
687
- interface GcsUploadResult {
688
- uploaded: boolean;
689
- fileCount: number;
690
- deletionCount: number;
691
- sizeBytes: number;
692
- }
693
- /**
694
- * The GCS sink on its own: assemble the snapshot tar (the single capture
695
- * path) and PUT it to the capability URL. Exposed so a runner whose git-tier
696
- * flush is driven elsewhere (SessionRunner's periodic flushAllPendingWork)
697
- * can add the GCS sink to the SAME trigger instead of running a second,
698
- * racing snapshotter. Calls are serialized: a later capture always PUTs
699
- * after any earlier in-flight one has settled.
700
- */
701
- declare function uploadSnapshotToGcs(cwd: string, uploadUrl: string | undefined, opts?: {
702
- forceUpload?: boolean;
703
- }): Promise<GcsUploadResult>;
704
- declare function captureSnapshot(ctx: CaptureContext): Promise<SnapshotArtifact>;
705
- declare function startPeriodic(intervalMs: number, ctx: CaptureContext): void;
706
- declare function stop(): void;
707
- declare function finalizeForSleep(ctx: CaptureContext): Promise<SnapshotArtifact>;
708
- /**
709
- * Restore precedence at boot: GCS snapshot (tracked+untracked+deletions),
710
- * else the conveyor-wip ref, else a clean checkout. Accepts the narrow slice
711
- * of the bundle it reads so v3 pods can call it from env-derived values
712
- * (CONVEYOR_SNAPSHOT_URL + the task branch) as well as from a full bundle.
713
- *
714
- * GCS is only authoritative when the archive's manifest HEAD matches the
715
- * current clone (checked BEFORE any tree mutation) — a stale or invalid
716
- * snapshot leaves the tree untouched and falls through to the wip ref, whose
717
- * own parent-HEAD guard handles staleness.
718
- */
719
- declare function restoreOnBoot(bundle: Pick<BootstrapBundle, "snapshotUrl"> & {
720
- gitPlan: Pick<BootstrapBundle["gitPlan"], "branch">;
721
- }, cwd: string): Promise<RestoreResult>;
722
-
723
674
  interface ConveyorConfig {
724
675
  setupCommand?: string;
725
676
  startCommand?: string;
@@ -742,4 +693,4 @@ declare function runStartCommand(cmd: string, cwd: string, onOutput: (stream: "s
742
693
  /** Fetch full git history if the repo was cloned with --depth=1. */
743
694
  declare function unshallowRepo(workspaceDir: string): void;
744
695
 
745
- 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 };
696
+ export { AgentConnection, type AgentConnectionConfig, type AgentRunnerCallbacks, type AgentRunnerConfig, type ApiKeyUpdateData, type ConveyorConfig, type IncomingMessage, type LifecycleCallbacks, type LifecycleConfig, type ModeAction, type ModeTaskContext, SessionRunner, type SessionRunnerCallbacks, type SessionRunnerConfig, type SetModeData, detachWorktreeBranch, ensureWorktree, flushPendingChanges, getCurrentBranch, hasUncommittedChanges, hasUnpushedCommits, loadConveyorConfig, loadForwardPorts, pushToOrigin, removeWorktree, runAuthTokenCommand, runSetupCommand, runStartCommand, stageAndCommit, unshallowRepo, updateRemoteToken };
package/dist/index.js CHANGED
@@ -2,8 +2,6 @@ import {
2
2
  AgentConnection,
3
3
  GIT_TIMEOUT_MS,
4
4
  SessionRunner,
5
- captureSnapshot,
6
- finalizeForSleep,
7
5
  flushPendingChanges,
8
6
  getCurrentBranch,
9
7
  hasUncommittedChanges,
@@ -11,19 +9,15 @@ import {
11
9
  loadConveyorConfig,
12
10
  loadForwardPorts,
13
11
  pushToOrigin,
14
- restoreOnBoot,
15
12
  stageAndCommit,
16
- startPeriodic,
17
- stop,
18
13
  unshallowRepo,
19
14
  updateRemoteToken,
20
- uploadSnapshotToGcs,
21
15
  workspacePathExists
22
- } from "./chunk-TB5SQIGX.js";
16
+ } from "./chunk-J4UH5TOO.js";
23
17
  import "./chunk-7TQO4ZF4.js";
24
18
  import {
25
19
  getWorkbenchClient
26
- } from "./chunk-YRC57EYG.js";
20
+ } from "./chunk-LZM5OKAC.js";
27
21
  import {
28
22
  workbenchEnabled
29
23
  } from "./chunk-4VUQ2NPF.js";
@@ -31,8 +25,8 @@ import {
31
25
  runAuthTokenCommand,
32
26
  runSetupCommand,
33
27
  runStartCommand
34
- } from "./chunk-2K6RRTQC.js";
35
- import "./chunk-IE7EOKMD.js";
28
+ } from "./chunk-RSIW2UCR.js";
29
+ import "./chunk-VKZ5W2VO.js";
36
30
 
37
31
  // src/runner/worktree.ts
38
32
  import { execFile } from "child_process";
@@ -102,10 +96,8 @@ async function removeWorktree(projectDir, taskId) {
102
96
  export {
103
97
  AgentConnection,
104
98
  SessionRunner,
105
- captureSnapshot,
106
99
  detachWorktreeBranch,
107
100
  ensureWorktree,
108
- finalizeForSleep,
109
101
  flushPendingChanges,
110
102
  getCurrentBranch,
111
103
  hasUncommittedChanges,
@@ -114,15 +106,11 @@ export {
114
106
  loadForwardPorts,
115
107
  pushToOrigin,
116
108
  removeWorktree,
117
- restoreOnBoot,
118
109
  runAuthTokenCommand,
119
110
  runSetupCommand,
120
111
  runStartCommand,
121
112
  stageAndCommit,
122
- startPeriodic as startWorkPreservation,
123
- stop as stopWorkPreservation,
124
113
  unshallowRepo,
125
- updateRemoteToken,
126
- uploadSnapshotToGcs
114
+ updateRemoteToken
127
115
  };
128
116
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { GIT_TIMEOUT_MS, hasUncommittedChanges } from \"./git-utils.js\";\nimport { getWorkbenchClient } from \"../workbench/client.js\";\nimport { workbenchEnabled } from \"../workbench/mode.js\";\nimport { workspacePathExists } from \"../workbench/fs.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.\n// Split-mode pods route over the launcher (the repo lives in the workbench).\nconst execFileAsync = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<string> {\n if (workbenchEnabled()) {\n const { stdout } = await getWorkbenchClient().execFile(\"git\", args, {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n });\n return stdout;\n }\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 (await workspacePathExists(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 (!(await workspacePathExists(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,YAAY;AACrB,SAAS,iBAAiB;AAS1B,IAAM,gBAAgB,UAAU,QAAQ;AAExC,eAAe,IAAI,KAAa,MAAiC;AAC/D,MAAI,iBAAiB,GAAG;AACtB,UAAM,EAAE,QAAAA,QAAO,IAAI,MAAM,mBAAmB,EAAE,SAAS,OAAO,MAAM;AAAA,MAClE;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,WAAOA;AAAA,EACT;AACA,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,MAAM,oBAAoB,YAAY,GAAG;AAC3C,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,CAAE,MAAM,oBAAoB,YAAY,EAAI;AAChD,MAAI;AACF,UAAM,IAAI,YAAY,CAAC,YAAY,UAAU,cAAc,SAAS,CAAC;AAAA,EACvE,QAAQ;AAAA,EAER;AACF;","names":["stdout"]}
1
+ {"version":3,"sources":["../src/runner/worktree.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { GIT_TIMEOUT_MS, hasUncommittedChanges } from \"./git-utils.js\";\nimport { getWorkbenchClient } from \"../workbench/client.js\";\nimport { workbenchEnabled } from \"../workbench/mode.js\";\nimport { workspacePathExists } from \"../workbench/fs.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.\n// Split-mode pods route over the launcher (the repo lives in the workbench).\nconst execFileAsync = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<string> {\n if (workbenchEnabled()) {\n const { stdout } = await getWorkbenchClient().execFile(\"git\", args, {\n cwd,\n timeout: GIT_TIMEOUT_MS,\n });\n return stdout;\n }\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 (await workspacePathExists(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 (!(await workspacePathExists(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,YAAY;AACrB,SAAS,iBAAiB;AAS1B,IAAM,gBAAgB,UAAU,QAAQ;AAExC,eAAe,IAAI,KAAa,MAAiC;AAC/D,MAAI,iBAAiB,GAAG;AACtB,UAAM,EAAE,QAAAA,QAAO,IAAI,MAAM,mBAAmB,EAAE,SAAS,OAAO,MAAM;AAAA,MAClE;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,WAAOA;AAAA,EACT;AACA,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,MAAM,oBAAoB,YAAY,GAAG;AAC3C,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,CAAE,MAAM,oBAAoB,YAAY,EAAI;AAChD,MAAI;AACF,UAAM,IAAI,YAAY,CAAC,YAAY,UAAU,cAAc,SAAS,CAAC;AAAA,EACvE,QAAQ;AAAA,EAER;AACF;","names":["stdout"]}
@@ -4,7 +4,7 @@ import {
4
4
  parseLine,
5
5
  timingSafeTokenEqual,
6
6
  writeFrame
7
- } from "./chunk-IE7EOKMD.js";
7
+ } from "./chunk-VKZ5W2VO.js";
8
8
  export {
9
9
  DEFAULT_WORKBENCH_PORT,
10
10
  FrameReader,
@@ -12,4 +12,4 @@ export {
12
12
  timingSafeTokenEqual,
13
13
  writeFrame
14
14
  };
15
- //# sourceMappingURL=protocol-QJRHTTMQ.js.map
15
+ //# sourceMappingURL=protocol-IHRTO5C4.js.map
@@ -1,23 +1,18 @@
1
1
  import {
2
- buildSnapshotTar,
3
- extractSnapshotTar,
4
2
  loadPtySpawn,
5
3
  terminateProcessGroup
6
- } from "./chunk-2K6RRTQC.js";
4
+ } from "./chunk-RSIW2UCR.js";
7
5
  import {
8
6
  FrameReader,
9
7
  timingSafeTokenEqual,
10
8
  writeFrame
11
- } from "./chunk-IE7EOKMD.js";
9
+ } from "./chunk-VKZ5W2VO.js";
12
10
 
13
11
  // src/workbench/server.ts
14
12
  import { spawn } from "child_process";
15
- import { createReadStream, createWriteStream } from "fs";
16
- import { mkdtemp, readdir, readFile, rm, stat } from "fs/promises";
13
+ import { readdir, readFile, stat } from "fs/promises";
17
14
  import { createServer } from "net";
18
- import { tmpdir } from "os";
19
- import { join } from "path";
20
- var TAR_CHUNK_BYTES = 64 * 1024;
15
+ var READ_CHUNK_BYTES = 64 * 1024;
21
16
  function errCode(err) {
22
17
  return typeof err?.code === "string" ? err.code : void 0;
23
18
  }
@@ -90,12 +85,6 @@ function dispatch(socket, req, sink, opts) {
90
85
  case "pty":
91
86
  void runPty(socket, req, sink);
92
87
  return;
93
- case "snapshotBuild":
94
- void runSnapshotBuild(socket, req);
95
- return;
96
- case "snapshotExtract":
97
- void runSnapshotExtract(socket, req, sink);
98
- return;
99
88
  case "readFile":
100
89
  void runReadFile(socket, req);
101
90
  return;
@@ -203,67 +192,13 @@ async function runPty(socket, req, sink) {
203
192
  }
204
193
  });
205
194
  }
206
- async function runSnapshotBuild(socket, req) {
207
- try {
208
- const result = await buildSnapshotTar(req.cwd);
209
- try {
210
- writeFrame(socket, {
211
- t: "meta",
212
- sizeBytes: result.sizeBytes,
213
- fileCount: result.fileCount,
214
- deletionCount: result.deletionCount,
215
- capturedAt: result.capturedAt
216
- });
217
- await new Promise((resolve, reject) => {
218
- const stream = createReadStream(result.tarPath, { highWaterMark: TAR_CHUNK_BYTES });
219
- stream.on("data", (chunk) => {
220
- writeFrame(socket, { t: "data", d: Buffer.from(chunk).toString("base64") });
221
- });
222
- stream.on("end", () => resolve());
223
- stream.on("error", reject);
224
- socket.on("close", () => stream.destroy());
225
- });
226
- writeFrame(socket, { t: "end" });
227
- } finally {
228
- await result.cleanup();
229
- }
230
- } catch (err) {
231
- sendError(socket, err);
232
- }
233
- socket.end();
234
- }
235
- async function runSnapshotExtract(socket, req, sink) {
236
- const staging = await mkdtemp(join(tmpdir(), "conveyor-workbench-extract-"));
237
- const tarPath = join(staging, "snapshot.tar.gz");
238
- const out = createWriteStream(tarPath);
239
- try {
240
- await new Promise((resolve, reject) => {
241
- sink.setHandler((frame) => {
242
- if (frame.t === "data" && typeof frame.d === "string") {
243
- out.write(Buffer.from(frame.d, "base64"));
244
- } else if (frame.t === "end") {
245
- out.end(() => resolve());
246
- }
247
- });
248
- out.on("error", reject);
249
- socket.on("close", () => reject(new Error("connection closed before end frame")));
250
- });
251
- const value = await extractSnapshotTar(req.cwd, tarPath);
252
- writeFrame(socket, { t: "result", value });
253
- } catch (err) {
254
- sendError(socket, err);
255
- } finally {
256
- await rm(staging, { recursive: true, force: true }).catch(() => void 0);
257
- socket.end();
258
- }
259
- }
260
195
  async function runReadFile(socket, req) {
261
196
  try {
262
197
  const content = await readFile(req.path);
263
- for (let offset = 0; offset < content.length; offset += TAR_CHUNK_BYTES) {
198
+ for (let offset = 0; offset < content.length; offset += READ_CHUNK_BYTES) {
264
199
  writeFrame(socket, {
265
200
  t: "data",
266
- d: content.subarray(offset, offset + TAR_CHUNK_BYTES).toString("base64")
201
+ d: content.subarray(offset, offset + READ_CHUNK_BYTES).toString("base64")
267
202
  });
268
203
  }
269
204
  writeFrame(socket, { t: "end" });
@@ -310,4 +245,4 @@ async function runReaddir(socket, req) {
310
245
  export {
311
246
  startWorkbenchServer
312
247
  };
313
- //# sourceMappingURL=server-UGE67KIZ.js.map
248
+ //# sourceMappingURL=server-TVIXWCAI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workbench/server.ts"],"sourcesContent":["/**\n * The workbench launcher — PID 1 of the workbench container in split-mode\n * pods (CONVEYOR_MODE=workbench). A token-authed, loopback-only exec daemon:\n * the agent container drives every workspace-touching operation through it\n * (shell commands, the claude PTY, git, file reads) while staying insulated\n * from workload OOMs in its own container cgroup.\n *\n * One connection = one operation. A dropped connection kills the operation's\n * process (group) — the agent dying, restarting, or timing out must never\n * leave orphans accumulating against the workbench memory budget.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { createServer, type Socket } from \"node:net\";\nimport { terminateProcessGroup } from \"../setup/commands.js\";\nimport { loadPtySpawn, type PtyProcess } from \"../harness/pty/pty-support.js\";\nimport {\n FrameReader,\n timingSafeTokenEqual,\n writeFrame,\n type WorkbenchFrame,\n type WorkbenchRequest,\n} from \"./protocol.js\";\n\nconst READ_CHUNK_BYTES = 64 * 1024;\n\nexport interface WorkbenchServerOptions {\n port: number;\n token: string;\n version: string;\n}\n\nexport interface WorkbenchServerHandle {\n port: number;\n close(): Promise<void>;\n}\n\nfunction errCode(err: unknown): string | undefined {\n return typeof (err as { code?: unknown })?.code === \"string\"\n ? (err as { code: string }).code\n : undefined;\n}\n\nfunction sendError(socket: Socket, err: unknown): void {\n const frame: WorkbenchFrame = {\n t: \"error\",\n message: err instanceof Error ? err.message : String(err),\n ...(errCode(err) ? { code: errCode(err) } : {}),\n };\n writeFrame(socket, frame);\n}\n\n/** Merge request env over the daemon's own (mirrors the local spawn paths,\n * which inherit process.env and let callers override). */\nfunction mergedEnv(overrides?: Record<string, string>): NodeJS.ProcessEnv {\n return overrides ? { ...process.env, ...overrides } : { ...process.env };\n}\n\n/**\n * Post-dispatch inbound frames for the connection's operation. Handlers\n * assign `onNext`; frames arriving before assignment are queued so an input\n * racing the (async) pty spawn is never dropped.\n */\nclass FrameSink {\n private pending: WorkbenchFrame[] = [];\n private handler: ((frame: WorkbenchFrame) => void) | null = null;\n\n push(frame: WorkbenchFrame): void {\n if (this.handler) this.handler(frame);\n else this.pending.push(frame);\n }\n\n setHandler(handler: (frame: WorkbenchFrame) => void): void {\n this.handler = handler;\n for (const frame of this.pending.splice(0)) handler(frame);\n }\n}\n\nexport async function startWorkbenchServer(\n opts: WorkbenchServerOptions,\n): Promise<WorkbenchServerHandle> {\n const server = createServer((socket) => handleConnection(socket, opts));\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(opts.port, \"127.0.0.1\", () => resolve());\n });\n const address = server.address();\n const port = typeof address === \"object\" && address ? address.port : opts.port;\n return {\n port,\n close: () =>\n new Promise<void>((resolve) => {\n server.close(() => resolve());\n }),\n };\n}\n\nfunction handleConnection(socket: Socket, opts: WorkbenchServerOptions): void {\n socket.on(\"error\", () => undefined);\n const sink = new FrameSink();\n let dispatched = false;\n const reader = new FrameReader((frame) => {\n if (dispatched) {\n sink.push(frame as unknown as WorkbenchFrame);\n return;\n }\n dispatched = true;\n dispatch(socket, frame as unknown as WorkbenchRequest, sink, opts);\n });\n socket.on(\"data\", (chunk: Buffer) => reader.push(chunk));\n}\n\nfunction dispatch(\n socket: Socket,\n req: WorkbenchRequest,\n sink: FrameSink,\n opts: WorkbenchServerOptions,\n): void {\n if (!req || typeof req !== \"object\" || !timingSafeTokenEqual(req.token ?? \"\", opts.token)) {\n writeFrame(socket, { t: \"error\", message: \"unauthorized\", code: \"unauthorized\" });\n socket.destroy();\n return;\n }\n switch (req.op) {\n case \"ping\":\n writeFrame(socket, { t: \"pong\", version: opts.version });\n socket.end();\n return;\n case \"exec\":\n runExec(socket, req, sink);\n return;\n case \"pty\":\n void runPty(socket, req, sink);\n return;\n case \"readFile\":\n void runReadFile(socket, req);\n return;\n case \"stat\":\n void runStat(socket, req);\n return;\n case \"readdir\":\n void runReaddir(socket, req);\n return;\n default:\n writeFrame(socket, { t: \"error\", message: \"unknown op\", code: \"bad_request\" });\n socket.end();\n }\n}\n\nfunction runExec(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"exec\" }>,\n sink: FrameSink,\n): void {\n let child: ChildProcess;\n try {\n // Argv form (git and friends) spawns directly with no shell; shell form\n // (setup/start commands) mirrors the local `spawn(\"sh\", [\"-c\", cmd])`.\n const [argvFile, ...argvRest] = req.argv ?? [];\n child = argvFile\n ? spawn(argvFile, argvRest, {\n cwd: req.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: mergedEnv(req.env),\n })\n : spawn(\"sh\", [\"-c\", req.command ?? \"\"], {\n cwd: req.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n env: mergedEnv(req.env),\n });\n } catch (err) {\n sendError(socket, err);\n socket.end();\n return;\n }\n\n let settled = false;\n sink.setHandler((frame) => {\n if (frame.t === \"signal\" && frame.mode === \"term-group\" && !settled) {\n void terminateProcessGroup(child);\n }\n });\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n writeFrame(socket, { t: \"out\", s: \"stdout\", d: chunk.toString(\"base64\") });\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n writeFrame(socket, { t: \"out\", s: \"stderr\", d: chunk.toString(\"base64\") });\n });\n child.on(\"close\", (code, signal) => {\n settled = true;\n writeFrame(socket, { t: \"exit\", code, signal });\n socket.end();\n });\n child.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n sendError(socket, err);\n socket.end();\n });\n // Agent gone (crash, timeout-abort, container death) → no orphans.\n socket.on(\"close\", () => {\n if (!settled) void terminateProcessGroup(child);\n });\n}\n\nasync function runPty(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"pty\" }>,\n sink: FrameSink,\n): Promise<void> {\n let pty: PtyProcess;\n try {\n const ptySpawn = await loadPtySpawn();\n pty = ptySpawn(req.file, req.args, {\n name: \"xterm-color\",\n cols: req.cols,\n rows: req.rows,\n cwd: req.cwd,\n env: req.env,\n });\n } catch (err) {\n sendError(socket, err);\n socket.end();\n return;\n }\n\n let exited = false;\n sink.setHandler((frame) => {\n try {\n if (frame.t === \"input\" && typeof frame.d === \"string\") {\n pty.write(Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (\n frame.t === \"resize\" &&\n typeof frame.cols === \"number\" &&\n typeof frame.rows === \"number\"\n ) {\n pty.resize(frame.cols, frame.rows);\n } else if (frame.t === \"kill\") {\n pty.kill(typeof frame.sig === \"string\" ? frame.sig : undefined);\n }\n } catch {\n /* pty already dead — exit handler owns teardown */\n }\n });\n pty.onData((data) => {\n writeFrame(socket, { t: \"data\", d: Buffer.from(data, \"utf8\").toString(\"base64\") });\n });\n pty.onExit((event) => {\n exited = true;\n writeFrame(socket, { t: \"exit\", code: event.exitCode, signal: null });\n socket.end();\n });\n socket.on(\"close\", () => {\n if (!exited) {\n try {\n pty.kill();\n } catch {\n /* already dead */\n }\n }\n });\n}\n\nasync function runReadFile(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"readFile\" }>,\n): Promise<void> {\n try {\n const content = await readFile(req.path);\n for (let offset = 0; offset < content.length; offset += READ_CHUNK_BYTES) {\n writeFrame(socket, {\n t: \"data\",\n d: content.subarray(offset, offset + READ_CHUNK_BYTES).toString(\"base64\"),\n });\n }\n writeFrame(socket, { t: \"end\" });\n } catch (err) {\n sendError(socket, err);\n }\n socket.end();\n}\n\nasync function runStat(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"stat\" }>,\n): Promise<void> {\n try {\n const s = await stat(req.path);\n writeFrame(socket, {\n t: \"stat\",\n exists: true,\n isFile: s.isFile(),\n isDirectory: s.isDirectory(),\n size: s.size,\n mtimeMs: s.mtimeMs,\n });\n } catch (err) {\n if (errCode(err) === \"ENOENT\") {\n writeFrame(socket, {\n t: \"stat\",\n exists: false,\n isFile: false,\n isDirectory: false,\n size: 0,\n mtimeMs: 0,\n });\n } else {\n sendError(socket, err);\n }\n }\n socket.end();\n}\n\nasync function runReaddir(\n socket: Socket,\n req: Extract<WorkbenchRequest, { op: \"readdir\" }>,\n): Promise<void> {\n try {\n writeFrame(socket, { t: \"entries\", names: await readdir(req.path) });\n } catch (err) {\n sendError(socket, err);\n }\n socket.end();\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "10.13.13",
3
+ "version": "10.13.14",
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",
@@ -334,13 +334,6 @@ boot_mark "env injected — entering user-home wiring"
334
334
 
335
335
  export ANTHROPIC_API_KEY=$(echo "${BOOTSTRAP_JSON}" | jq -r '.anthropicKey // empty')
336
336
  export CLOUDSDK_AUTH_ACCESS_TOKEN=$(echo "${BOOTSTRAP_JSON}" | jq -r '.gcpToken // empty')
337
- # WorkPreservation GCS tier (spec §Durability & snapshots): the upload URL is
338
- # long-lived (used for the pod's whole lifetime of periodic captures + sleep
339
- # finalize); the download URL is present only when a snapshot exists and is
340
- # consumed once by restoreOnBoot.
341
- CONVEYOR_SNAPSHOT_UPLOAD_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUploadUrl // empty')
342
- CONVEYOR_SNAPSHOT_URL=$(echo "${BOOTSTRAP_JSON}" | jq -r '.snapshotUrl // empty')
343
- export CONVEYOR_SNAPSHOT_UPLOAD_URL CONVEYOR_SNAPSHOT_URL
344
337
  unset BOOTSTRAP_JSON
345
338
  # POD_BOOTSTRAP_TOKEN is deliberately NOT unset: it is the credential-refresh
346
339
  # key. The agent re-polls GET /api/v3/pods/bootstrap with it to swap in a
@@ -688,96 +681,29 @@ reset_tracked_repo_changes_before_assignment_checkout() {
688
681
  }
689
682
 
690
683
  # Shared by both the pod-image branch and the repo-present-non-pod-image branch
691
- # of prepare_workspace_git: refresh the remote token, fetch the base branch,
692
- # fetch/checkout the task branch (creating it from base if it doesn't exist on
693
- # origin yet), and merge latest base into it. On success the repo is left
694
- # checked out on the task branch, up to date with base. On any failure it
695
- # writes GIT_FAILED_MARKER and returns 1 callers must not fall through to
696
- # `: > "$GIT_READY_MARKER"` in that case.
684
+ # of prepare_workspace_git: refresh the remote token and warm origin/<base>.
685
+ # The agent (ensureOnTaskBranch, Tasks 4-5) now owns the task-branch
686
+ # fetch/checkout/create-from-base and the merge of base into the task branch
687
+ # this function no longer touches BRANCH or CHECKOUT_REF at all. On success the
688
+ # repo is left on a clean tree with an up-to-date origin/<base> remote-tracking
689
+ # ref. On any failure it writes GIT_FAILED_MARKER and returns 1 — callers must
690
+ # not fall through to `: > "$GIT_READY_MARKER"` in that case.
697
691
  sync_task_branch_to_repo() {
698
692
  # Guarded so a set-url failure writes the failed marker instead of aborting
699
693
  # the backgrounded subshell (which would leave the agent waiting forever).
700
694
  if ! git -C repo remote set-url origin "https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" 2>/dev/null; then
701
- echo "[pool] ERROR: remote set-url failed for pod-image repo" >&2
702
- printf '%s' "pod-image remote set-url failed" > "$GIT_FAILED_MARKER"
695
+ echo "[pool] ERROR: remote set-url failed" >&2
696
+ printf '%s' "remote set-url failed" > "$GIT_FAILED_MARKER"
703
697
  return 1
704
698
  fi
705
- # Always fetch latest base so downstream steps (and the agent's
706
- # syncWithBaseBranch) have an up-to-date origin/<base> to merge from.
707
- DEV_BRANCH="${BASE_BRANCH}"
708
- if ! _fetch_dev_out=$(git -C repo fetch origin "+refs/heads/${DEV_BRANCH}:refs/remotes/origin/${DEV_BRANCH}" 2>&1); then
709
- echo "[pool] WARN: fetch origin/${DEV_BRANCH} failed: ${_fetch_dev_out}" >&2
699
+ # Warm origin/<base> so the agent's checkout/fetch is a fast-forward.
700
+ if ! _fetch_out=$(git -C repo fetch origin "+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>&1); then
701
+ echo "[pool] WARN: fetch origin/${BASE_BRANCH} failed: ${_fetch_out}" >&2
710
702
  fi
711
-
712
- if ! reset_tracked_repo_changes_before_assignment_checkout; then
713
- return 1
714
- fi
715
-
716
- if [ -n "${CHECKOUT_REF}" ]; then
717
- echo "[pool] Fetching checkout ref ${CHECKOUT_REF} for review branch ${BRANCH}..."
718
- if ! _fetch_checkout_out=$(git -C repo fetch origin "+${CHECKOUT_REF}:refs/remotes/origin/pr-checkout" 2>&1); then
719
- echo "[pool] ERROR: Repo fetch failed for checkout ref '${CHECKOUT_REF}': ${_fetch_checkout_out}" >&2
720
- printf '%s' "fetch checkout ref ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
721
- return 1
722
- fi
723
- if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "refs/remotes/origin/pr-checkout" 2>&1); then
724
- echo "[pool] ERROR: Repo checkout of ${CHECKOUT_REF} failed: ${_checkout_out}" >&2
725
- printf '%s' "checkout of ${CHECKOUT_REF} failed" > "$GIT_FAILED_MARKER"
726
- return 1
727
- fi
728
- else
729
- # Fetch the task branch into a local ref explicitly so we can check it out.
730
- # If the branch doesn't exist on origin, create it from the base branch.
731
- # Note: if BRANCH equals DEV_BRANCH (deferred-branch case), the fetch above
732
- # already populated the ref and this is a no-op.
733
- if ! _fetch_out=$(git -C repo fetch origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>&1); then
734
- # Check if the error is specifically due to the remote ref not existing
735
- if echo "${_fetch_out}" | grep -q "couldn't find remote ref"; then
736
- echo "[pool] Branch missing on origin — creating from base '${DEV_BRANCH}'"
737
- # Create the branch locally from the base branch
738
- if ! _create_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${DEV_BRANCH}" 2>&1); then
739
- echo "[pool] ERROR: Failed to create branch '${BRANCH}' from '${DEV_BRANCH}': ${_create_out}" >&2
740
- printf '%s' "create branch ${BRANCH} from ${DEV_BRANCH} failed" > "$GIT_FAILED_MARKER"
741
- return 1
742
- fi
743
- # Push the new branch to origin with -u to set up tracking
744
- if ! _push_out=$(git -C repo push -u origin "${BRANCH}" 2>&1); then
745
- echo "[pool] ERROR: Failed to push new branch '${BRANCH}': ${_push_out}" >&2
746
- printf '%s' "push new branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
747
- return 1
748
- fi
749
- echo "[pool] Created and pushed new branch '${BRANCH}' from '${DEV_BRANCH}'"
750
- else
751
- # Other git error (auth, network, etc.) — fail loud as before
752
- echo "[pool] ERROR: Repo fetch failed for branch '${BRANCH}': ${_fetch_out}" >&2
753
- printf '%s' "fetch branch ${BRANCH} failed" > "$GIT_FAILED_MARKER"
754
- return 1
755
- fi
756
- fi
757
- # Use `checkout -B` to create or reset the local branch tracking origin.
758
- # This leaves HEAD attached to a real branch (not detached) so `git status`,
759
- # `git push`, and any tool that keys off branch state work correctly.
760
- if ! _checkout_out=$(git -C repo checkout -f -B "${BRANCH}" "origin/${BRANCH}" 2>&1); then
761
- echo "[pool] ERROR: Repo checkout of ${BRANCH} failed: ${_checkout_out}" >&2
762
- printf '%s' "checkout of ${BRANCH} failed" > "$GIT_FAILED_MARKER"
763
- return 1
764
- fi
765
- fi
766
-
767
- # Merge latest dev into the task branch so the agent starts on up-to-date
768
- # base code. Skip if the branch already contains origin/dev (fast-path)
769
- # or if BRANCH is the dev branch itself.
770
- if [ "${BRANCH}" != "${DEV_BRANCH}" ] && git -C repo rev-parse "origin/${DEV_BRANCH}" >/dev/null 2>&1; then
771
- if git -C repo merge-base --is-ancestor "origin/${DEV_BRANCH}" HEAD; then
772
- echo "[pool] Branch already up-to-date with origin/${DEV_BRANCH}"
773
- elif ! _merge_out=$(git -C repo merge "origin/${DEV_BRANCH}" --no-edit 2>&1); then
774
- echo "[pool] WARN: merge origin/${DEV_BRANCH} failed, aborting: ${_merge_out}" >&2
775
- git -C repo merge --abort 2>/dev/null || true
776
- else
777
- echo "[pool] Merged origin/${DEV_BRANCH} into ${BRANCH}"
778
- fi
779
- fi
780
- echo "[pool] Repo updated to ${BRANCH}@$(git -C repo rev-parse --short HEAD)"
703
+ # Leave the tree clean; the agent (ensureOnTaskBranch) does the authoritative
704
+ # checkout + conveyor-wip restore. No task-branch checkout, no dev merge here.
705
+ reset_tracked_repo_changes_before_assignment_checkout || return 1
706
+ echo "[pool] Repo remote ready; agent will checkout ${BRANCH}"
781
707
  return 0
782
708
  }
783
709
 
@@ -803,11 +729,10 @@ prepare_workspace_git() {
803
729
  mark_git_ready
804
730
  elif [ -d "repo/.git" ] && [ -n "${CONVEYOR_GITHUB_TOKEN}" ]; then
805
731
  # Non-image but repo already present (e.g. a baked image running without
806
- # CONVEYOR_POD_IMAGE set). Must NOT settle for a bare remote-URL refresh
807
- # that would leave the agent running on whatever branch the repo snapshot
808
- # happened to be on (e.g. dev), silently committing/pushing to the wrong
809
- # branch. Run the exact same fetch/checkout/merge flow as the pod-image
810
- # branch so this path ends up in the same checked-out-on-task-branch state.
732
+ # CONVEYOR_POD_IMAGE set). Run the exact same remote-refresh + base-warm
733
+ # flow as the pod-image branch so this path ends up in the same
734
+ # clean-tree, remote-ready state; the agent (ensureOnTaskBranch) does the
735
+ # actual task-branch checkout regardless of which branch got it here.
811
736
  echo "[pool] Repo present (non-pod-image) — updating repo to latest (branch=${BRANCH})..."
812
737
  if ! sync_task_branch_to_repo; then
813
738
  return 1
@@ -840,9 +765,10 @@ prepare_workspace_git() {
840
765
  # branch. A naive `clone --branch <task>` dies with "Remote branch not
841
766
  # found" when the task branch has never been pushed to origin, which
842
767
  # crash-loops the pod on every fresh task. sync_task_branch_to_repo below
843
- # resolves/creates ${BRANCH} from the clone identically to the pod-image
844
- # path. Full depth (no --depth 1) so its base-merge never hits "refusing
845
- # to merge unrelated histories".
768
+ # only refreshes the remote and warms origin/<base> from this clone; the
769
+ # agent (ensureOnTaskBranch) resolves/creates ${BRANCH} from it. Full
770
+ # depth (no --depth 1) so the agent's base-merge never hits "refusing to
771
+ # merge unrelated histories".
846
772
  if ! _clone_out=$(git clone --single-branch --branch "${BASE_BRANCH}" \
847
773
  "https://x-access-token:${CONVEYOR_GITHUB_TOKEN}@github.com/${REPO_OWNER}/${REPO_NAME}.git" \
848
774
  repo 2>&1); then