@rallycry/conveyor-agent 10.13.72 → 11.0.1

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.
@@ -11,7 +11,7 @@ import {
11
11
  ensureDir,
12
12
  redactToken,
13
13
  reportBootMilestone
14
- } from "./chunk-WMMBAKPE.js";
14
+ } from "./chunk-GL2DIQEQ.js";
15
15
  import {
16
16
  workbenchPort
17
17
  } from "./chunk-KMB3BU4S.js";
@@ -101,6 +101,7 @@ function decodeJwtClaims(jwt) {
101
101
  function resolveModeEnv(claims) {
102
102
  if (claims.mode === "review") return { CONVEYOR_MODE: "code-review" };
103
103
  if (claims.mode === "pack") return { CONVEYOR_MODE: "pack" };
104
+ if (claims.mode === "plan") return { CONVEYOR_MODE: "plan" };
104
105
  if (claims.mode === "serving") return { CONVEYOR_MODE: "serving" };
105
106
  if (claims.mode === "adhoc") return { CONVEYOR_MODE: "adhoc" };
106
107
  if (!claims.taskId && claims.projectId) return { CONVEYOR_MODE: "pm" };
@@ -853,6 +854,7 @@ async function startWorkspaceSshd(exec, log, port = process.env.CONVEYOR_WORKSPA
853
854
  // src/boot/workspace-extras.ts
854
855
  import {
855
856
  appendFileSync as appendFileSync3,
857
+ cpSync,
856
858
  existsSync as existsSync3,
857
859
  lstatSync as lstatSync2,
858
860
  mkdirSync as mkdirSync3,
@@ -863,7 +865,8 @@ import {
863
865
  statSync as statSync3,
864
866
  symlinkSync as symlinkSync2
865
867
  } from "fs";
866
- import { join as join5 } from "path";
868
+ import { dirname as dirname2, join as join5 } from "path";
869
+ import { fileURLToPath } from "url";
867
870
  function errText(err) {
868
871
  return redactToken(err instanceof Error ? err.message : String(err));
869
872
  }
@@ -1017,6 +1020,49 @@ function linkGrimoireSkills(workspace, log) {
1017
1020
  excludeLinkedSkills(workspace, linkedNames, log);
1018
1021
  log.info(`[boot] Linked ${linkedNames.length} grimoire skills into ${targetDir}`);
1019
1022
  }
1023
+ function bundledSkillsDir(moduleUrl = import.meta.url) {
1024
+ try {
1025
+ const dir = join5(dirname2(fileURLToPath(moduleUrl)), "..", "skills");
1026
+ return isDir2(dir) ? dir : null;
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function materializeBundledSkills(workspace, log, sourceDirOverride) {
1032
+ const sourceDir = sourceDirOverride === void 0 ? bundledSkillsDir() : sourceDirOverride;
1033
+ if (!sourceDir) {
1034
+ log.warn(
1035
+ "[boot] WARN: no bundled skills payload in this agent build. Cloud modes that invoke a skill will have no workflow to follow. This is a packaging fault (tsup copySkillsPayload did not run), not a workspace problem."
1036
+ );
1037
+ return;
1038
+ }
1039
+ const targetDir = join5(workspace, ".claude", "skills");
1040
+ try {
1041
+ mkdirSync3(targetDir, { recursive: true });
1042
+ } catch {
1043
+ log.warn(`[boot] WARN: unable to create ${targetDir}; skipping bundled skills`);
1044
+ return;
1045
+ }
1046
+ const copied = [];
1047
+ let names = [];
1048
+ try {
1049
+ names = readdirSync2(sourceDir);
1050
+ } catch {
1051
+ return;
1052
+ }
1053
+ for (const name of names) {
1054
+ if (!isDir2(join5(sourceDir, name))) continue;
1055
+ const target = join5(targetDir, name);
1056
+ if (existsSync3(target) || isSymlink(target)) continue;
1057
+ try {
1058
+ cpSync(join5(sourceDir, name), target, { recursive: true, dereference: true });
1059
+ copied.push(name);
1060
+ } catch {
1061
+ }
1062
+ }
1063
+ excludeLinkedSkills(workspace, copied, log);
1064
+ log.info(`[boot] Materialized ${copied.length} bundled skills into ${targetDir}`);
1065
+ }
1020
1066
  async function neutralizeGrimoirePointer(workspace, git, log) {
1021
1067
  if (!existsSync3(join5(workspace, ".claude", "grimoire"))) return;
1022
1068
  try {
@@ -1058,6 +1104,7 @@ async function runPreReadyBinds(workspace, bundle, git, log) {
1058
1104
  bindGraphifyBundle(workspace, graphifyEnv, log);
1059
1105
  await ensureGrimoireSubmodule(workspace, bundle.githubToken ?? "", git, log);
1060
1106
  linkGrimoireSkills(workspace, log);
1107
+ materializeBundledSkills(workspace, log);
1061
1108
  await neutralizeGrimoirePointer(workspace, git, log);
1062
1109
  }
1063
1110
  async function cloneReferenceRepos(refsJson, git, log, referencesDir = "/workspaces/references") {
@@ -6,15 +6,112 @@ import {
6
6
 
7
7
  // src/boot/git-prep.ts
8
8
  import { execFile } from "child_process";
9
- import { existsSync, mkdirSync } from "fs";
10
- import { join } from "path";
9
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
10
+ import { join as join2 } from "path";
11
11
  import { promisify } from "util";
12
+
13
+ // src/boot/commit-trailers.ts
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { dirname, isAbsolute, join } from "path";
16
+ var HOOK_MARKER = "conveyor-commit-trailers";
17
+ var realFs = {
18
+ exists: (path) => existsSync(path),
19
+ read: (path) => readFileSync(path, "utf8"),
20
+ write: (path, content) => writeFileSync(path, content, "utf8"),
21
+ mkdirp: (path) => mkdirSync(path, { recursive: true }),
22
+ chmodExec: (path) => chmodSync(path, 493)
23
+ };
24
+ function buildHookScript() {
25
+ return `#!/bin/sh
26
+ # ${HOOK_MARKER} \u2014 stamps Conveyor card/driver/session provenance on each commit.
27
+ # Generated by @rallycry/conveyor-agent (boot/commit-trailers.ts); edits are
28
+ # overwritten on the next pod boot.
29
+ #
30
+ # Every path exits 0 on purpose: losing a commit to attribution bookkeeping
31
+ # would be a far worse outcome than losing the attribution.
32
+ MSG="$1"
33
+ [ -n "$MSG" ] || exit 0
34
+
35
+ stamp() {
36
+ [ -n "$2" ] || return 0
37
+ git interpret-trailers --in-place --if-exists doNothing --trailer "$1: $2" "$MSG" 2>/dev/null || true
38
+ }
39
+
40
+ stamp "Conveyor-Card" "$CONVEYOR_CARD_SLUG"
41
+ stamp "Conveyor-Driver" "$CONVEYOR_DRIVER_ID"
42
+ stamp "Conveyor-Session" "$CONVEYOR_SESSION_ID"
43
+ exit 0
44
+ `;
45
+ }
46
+ function resolveHookTargets(repoDir, hooksPath, hasHusky) {
47
+ const huskyTarget = { hookPath: join(repoDir, ".husky", "commit-msg"), huskyManaged: true };
48
+ if (hooksPath?.replace(/\/+$/, "").endsWith(".husky/_")) return [huskyTarget];
49
+ const dir = hooksPath ? isAbsolute(hooksPath) ? hooksPath : join(repoDir, hooksPath) : join(repoDir, ".git", "hooks");
50
+ const resolved = { hookPath: join(dir, "commit-msg"), huskyManaged: false };
51
+ return hasHusky ? [resolved, huskyTarget] : [resolved];
52
+ }
53
+ function excludeFromRepo(repoDir, relPath, fs) {
54
+ const excludePath = join(repoDir, ".git", "info", "exclude");
55
+ const existing = fs.exists(excludePath) ? fs.read(excludePath) : "";
56
+ if (existing.split("\n").some((line) => line.trim() === relPath)) return;
57
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
58
+ fs.write(excludePath, `${existing}${separator}${relPath}
59
+ `);
60
+ }
61
+ function declaresHusky(repoDir, fs) {
62
+ if (fs.exists(join(repoDir, ".husky"))) return true;
63
+ const pkgPath = join(repoDir, "package.json");
64
+ if (!fs.exists(pkgPath)) return false;
65
+ try {
66
+ const pkg = JSON.parse(fs.read(pkgPath));
67
+ if (pkg.devDependencies?.husky ?? pkg.dependencies?.husky) return true;
68
+ return Boolean(pkg.scripts?.prepare?.includes("husky"));
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ async function installCommitTrailerHook(deps) {
74
+ const { git, repoDir, log } = deps;
75
+ const env = deps.env ?? process.env;
76
+ const fs = deps.fs ?? realFs;
77
+ if (!env.CONVEYOR_CARD_SLUG) return { state: "skipped", reason: "no-card" };
78
+ try {
79
+ let hooksPath = null;
80
+ try {
81
+ const { stdout } = await git(["config", "--get", "core.hooksPath"], { cwd: repoDir });
82
+ hooksPath = stdout.trim() || null;
83
+ } catch {
84
+ }
85
+ const targets = resolveHookTargets(repoDir, hooksPath, declaresHusky(repoDir, fs));
86
+ const written = [];
87
+ for (const { hookPath, huskyManaged } of targets) {
88
+ if (fs.exists(hookPath) && !fs.read(hookPath).includes(HOOK_MARKER)) {
89
+ log.warn(`[boot] WARN: commit-msg hook already present at ${hookPath}; leaving it alone`);
90
+ continue;
91
+ }
92
+ fs.mkdirp(dirname(hookPath));
93
+ fs.write(hookPath, buildHookScript());
94
+ fs.chmodExec(hookPath);
95
+ if (huskyManaged) excludeFromRepo(repoDir, ".husky/commit-msg", fs);
96
+ written.push(hookPath);
97
+ }
98
+ if (written.length === 0) return { state: "skipped", reason: "project-hook" };
99
+ log.info(`[boot] Commit trailers installed (${written.join(", ")})`);
100
+ return { state: "installed", paths: written };
101
+ } catch (err) {
102
+ const reason = err instanceof Error ? err.message : String(err);
103
+ log.warn(`[boot] WARN: commit trailer hook not installed: ${reason}`);
104
+ return { state: "failed", reason };
105
+ }
106
+ }
107
+
108
+ // src/boot/git-prep.ts
12
109
  var QUICK_GIT_TIMEOUT_MS = 6e4;
13
110
  var FETCH_TIMEOUT_MS = 3e5;
14
111
  var CLONE_TIMEOUT_MS = 6e5;
15
112
  var execFileAsync = promisify(execFile);
16
113
  function ensureDir(dir) {
17
- mkdirSync(dir, { recursive: true });
114
+ mkdirSync2(dir, { recursive: true });
18
115
  }
19
116
  function defaultGit(args, opts = {}) {
20
117
  return execFileAsync("git", args, {
@@ -143,10 +240,26 @@ async function clonePostAssignment(deps, paths) {
143
240
  }
144
241
  return syncTaskBranchToRepo(deps, paths);
145
242
  }
243
+ async function withCommitTrailers(deps, paths, state) {
244
+ if (state.state !== "ready") return state;
245
+ await installCommitTrailerHook({
246
+ git: deps.git,
247
+ repoDir: paths.repoDir,
248
+ log: deps.log,
249
+ // The bundle, NOT process.env: git prep runs inside the workbench sidecar,
250
+ // whose own env holds only CONVEYOR_API_URL and POD_BOOTSTRAP_TOKEN.
251
+ // `bundle.envVars` (where CONVEYOR_CARD_SLUG rides) is merged into
252
+ // `ctx.childEnv` for the separate agent container and is never written back
253
+ // onto this process — so reading process.env here saw nothing and skipped
254
+ // the install on every real pod.
255
+ env: { ...process.env, ...deps.bundle.envVars }
256
+ });
257
+ return state;
258
+ }
146
259
  async function prepareWorkspaceGit(deps) {
147
260
  const { bundle, log } = deps;
148
261
  const workspacesDir = deps.workspacesDir ?? "/workspaces";
149
- const repoDir = join(workspacesDir, "repo");
262
+ const repoDir = join2(workspacesDir, "repo");
150
263
  const { branch, cloneUrl, repoOwner, repoName } = bundle.gitPlan;
151
264
  const credential = bundle.gitCredential;
152
265
  const paths = {
@@ -165,17 +278,17 @@ async function prepareWorkspaceGit(deps) {
165
278
  if (bundle.gitPlan.provider === "github") {
166
279
  syncGithubTokenFiles(bundle.githubToken ?? credential.secret);
167
280
  }
168
- if (existsSync(join(repoDir, ".git"))) {
281
+ if (existsSync2(join2(repoDir, ".git"))) {
169
282
  log.info(
170
283
  deps.podImage ? `[boot] Pod image \u2014 updating repo to latest (branch=${branch})...` : `[boot] Repo present (non-pod-image) \u2014 updating repo to latest (branch=${branch})...`
171
284
  );
172
- return await syncTaskBranchToRepo(deps, paths);
285
+ return await withCommitTrailers(deps, paths, await syncTaskBranchToRepo(deps, paths));
173
286
  }
174
287
  try {
175
- mkdirSync(workspacesDir, { recursive: true });
288
+ mkdirSync2(workspacesDir, { recursive: true });
176
289
  } catch {
177
290
  }
178
- return await clonePostAssignment(deps, paths);
291
+ return await withCommitTrailers(deps, paths, await clonePostAssignment(deps, paths));
179
292
  } catch (err) {
180
293
  log.error(`[boot] ERROR: git prep failed unexpectedly: ${errText(err)}`);
181
294
  return { state: "failed", reason: "git prep failed unexpectedly" };
@@ -5,10 +5,10 @@ import {
5
5
  readWorkspaceBytes,
6
6
  statWorkspacePath,
7
7
  workspacePathExists
8
- } from "./chunk-LSZ2KLJY.js";
8
+ } from "./chunk-N4WSUTGV.js";
9
9
  import {
10
10
  reportBootMilestone
11
- } from "./chunk-WMMBAKPE.js";
11
+ } from "./chunk-GL2DIQEQ.js";
12
12
  import {
13
13
  getWorkbenchClient
14
14
  } from "./chunk-EXQ6AHOY.js";
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_RETRY_DELAY_MS,
4
4
  FETCH_TIMEOUT_MS,
5
5
  GIT_PREP_MAX_RETRIES
6
- } from "./chunk-WMMBAKPE.js";
6
+ } from "./chunk-GL2DIQEQ.js";
7
7
  import {
8
8
  buildConveyorSocketOptions,
9
9
  callWithAck,
@@ -278,6 +278,8 @@ var AgentConnection = class _AgentConnection {
278
278
  earlyPullBranches = [];
279
279
  spawnReviewCallback = null;
280
280
  earlySpawnReviews = [];
281
+ spawnBuilderCallback = null;
282
+ earlySpawnBuilders = [];
281
283
  spawnTuiCallback = null;
282
284
  earlySpawnTuis = [];
283
285
  probeUsageCallback = null;
@@ -455,6 +457,10 @@ var AgentConnection = class _AgentConnection {
455
457
  if (this.spawnReviewCallback) this.spawnReviewCallback(data);
456
458
  else this.earlySpawnReviews.push(data);
457
459
  });
460
+ this.socket.on("session:spawnBuilder", (data) => {
461
+ if (this.spawnBuilderCallback) this.spawnBuilderCallback(data);
462
+ else this.earlySpawnBuilders.push(data);
463
+ });
458
464
  this.socket.on("session:spawnTui", (data) => {
459
465
  if (this.spawnTuiCallback) this.spawnTuiCallback(data);
460
466
  else this.earlySpawnTuis.push(data);
@@ -752,6 +758,13 @@ var AgentConnection = class _AgentConnection {
752
758
  for (const data of this.earlySpawnReviews) callback(data);
753
759
  this.earlySpawnReviews = [];
754
760
  }
761
+ /** Mirror of onSpawnReview for the Builder handoff. Drains the early buffer
762
+ * so a Build pressed during boot is not lost. */
763
+ onSpawnBuilder(callback) {
764
+ this.spawnBuilderCallback = callback;
765
+ for (const data of this.earlySpawnBuilders) callback(data);
766
+ this.earlySpawnBuilders = [];
767
+ }
755
768
  /**
756
769
  * Report that a same-pod review child failed to spawn (fire-and-forget).
757
770
  * The server Ends the orphaned review session and falls back to a dedicated
@@ -767,6 +780,23 @@ var AgentConnection = class _AgentConnection {
767
780
  }).catch(() => {
768
781
  });
769
782
  }
783
+ /**
784
+ * Report that this (planner) pod could not spawn the Builder child, so the
785
+ * server can End the orphaned build session and reopen the card instead of
786
+ * leaving it InProgress with a Builder tab that never appears.
787
+ *
788
+ * sessionId is OUR (planner) session — the task-identity guard runs on it;
789
+ * the build session is identified separately.
790
+ */
791
+ reportBuilderSpawnFailure(buildSessionId, error) {
792
+ if (!this.socket) return;
793
+ void this.call("reportBuilderSpawnFailure", {
794
+ sessionId: this.config.sessionId,
795
+ buildSessionId,
796
+ ...error ? { error: error.slice(0, 2e3) } : {}
797
+ }).catch(() => {
798
+ });
799
+ }
770
800
  /**
771
801
  * Report that this pod's git credential is dead and refreshing did not fix
772
802
  * it (fire-and-forget).
@@ -2056,11 +2086,18 @@ var Lifecycle = class {
2056
2086
  }
2057
2087
  }
2058
2088
  // ── Idle timer ─────────────────────────────────────────────────────
2059
- startIdleTimer() {
2089
+ /** Start (or restart) the idle timer.
2090
+ * @param overrideMs Optional custom delay in ms, mirroring
2091
+ * `startDormantTimer`. SessionRunner passes a short delay when it DEFERS a
2092
+ * shutdown because a spawned child is still working: the pod must re-check
2093
+ * soon after that child exits, rather than granting itself a fresh full idle
2094
+ * window every time it defers. */
2095
+ startIdleTimer(overrideMs) {
2060
2096
  this.clearIdleTimers();
2097
+ const delay2 = Math.max(0, overrideMs ?? this.config.idleTimeoutMs);
2061
2098
  this.idleTimer = setTimeout(() => {
2062
2099
  this.callbacks.onIdleTimeout();
2063
- }, this.config.idleTimeoutMs);
2100
+ }, delay2);
2064
2101
  }
2065
2102
  cancelIdleTimer() {
2066
2103
  this.clearIdleTimers();